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

Compare commits

...

17 Commits

Author SHA1 Message Date
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
Michael Muller
c7e6192929 Add replay extension to more domain tests (#893)
* Add replay extension to more domain tests

Add replay to DomainRenewFlowTest and DomainUpdateFlowTest.

* Formatting fix
2020-12-04 14:15:24 -05:00
gbrodman
a8effe8a1e Add an action to replay commit logs to SQL (#887)
* Add an action to replay commit logs to SQL

- We import from the commit-log GCS files so that we are sure that we
have a consistent snapshot.
- We use the object weighting (moved to ObjectWeights) to verify that we
are replaying objects in the correct order.
- We add a config setting (default false) of whether or not to replay
the logs.
- The action is triggered after the export if the aforementioned config
setting is on.

* Responses to CR

- Remove triggering of replay from the export action and remove the test
changes
- Add a method to load commit log diffs by transaction
- Replay one Datastore transaction at a time, per SQL transaction
- Minor logging / comment changes
- Change ObjectWeights to EntityWritePriorities and flesh out javadoc

* More CR responses

- Use one transaction per GCS diff file
- Fix up comments minutiae

* Add a class-level javadoc

* Add a log message and some periods

* bit of formatting

* Merge remote-tracking branch 'origin/master' into replayAction

* Handle toSqlEntity rather than toSqlEntities
2020-12-03 16:50:41 -05:00
gbrodman
4f0189c162 Convert DatastoreEntity/SqlEntity to return Optional, not List (#894)
* Convert DatastoreEntity/SqlEntity to return Optional, not List

We don't have any entities that convert to more than one entity, so we
can use an Optional instead for clarity and simplicity.
2020-12-02 17:29:01 -05:00
sarahcaseybot
59c852d812 Add an HTTP header to response from Nomulus after successful login (#879)
* Add a logged-in response header

* small fixes

* Refactor EPP test cases to check for headers

* small change
2020-12-01 19:24:56 -05:00
sarahcaseybot
2621448f5e Remove ability to set only the certificate hash for a registrar (#891) 2020-12-01 14:28:45 -05:00
Weimin Yu
94ef81dca4 Script to rolling-start Nomulus (#888)
* Script to rolling-start Nomulus

Add a script to restart Nomulus non-disruptively. This can be used after
a configuration change to external resources (e.g.,  Cloud SQL
credential) to make Nomulus pick up the latest config.

Also added proper support to paging based List api methods, replacing the
current hack that forces the server to return everything in one response.
The List method for instances has a lower limit on page size than others
which is not sufficient for our project.
2020-12-01 10:14:05 -05:00
Michael Muller
64e1a4b345 Make Domain -> BillingEvent FK deferred (#890)
* Make Domain -> BillingEvent FK deferred

It appears that Hibernate can sporadically introduce FK constraint failures
when updating a Domain to reference a new BillingEvent and then deleting the
old BillingEvent, causing a flakey test failure in DomainDeleteFlowTest.  This
may be due to the fact that this FK relationships is not known to hibernate.

An alternate solution appears to be to flush after every update, but that
likely has some pretty serious performance implications.
2020-11-30 18:06:07 -05:00
115 changed files with 7925 additions and 5184 deletions

View File

@@ -15,10 +15,10 @@
package google.registry.backup;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.backup.BackupUtils.createDeserializingIterator;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Streams;
import google.registry.model.ImmutableObject;
import google.registry.model.ofy.CommitLogCheckpoint;
import google.registry.model.ofy.CommitLogManifest;
@@ -31,7 +31,6 @@ import java.io.InputStream;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.util.Iterator;
import java.util.stream.Stream;
/**
* Helpers for reading CommitLog records from a file.
@@ -43,6 +42,56 @@ public final class CommitLogImports {
private CommitLogImports() {}
/**
* Returns entities in an {@code inputStream} (from a single CommitLog file) as an {@link
* ImmutableList} of {@link ImmutableList}s of {@link VersionedEntity} records where the inner
* lists each consist of one transaction. Upon completion the {@code inputStream} is closed.
*
* <p>The returned list may be empty, since CommitLogs are written at fixed intervals regardless
* if actual changes exist. Each sublist, however, will not be empty.
*
* <p>A CommitLog file starts with a {@link CommitLogCheckpoint}, followed by (repeated)
* subsequences of [{@link CommitLogManifest}, [{@link CommitLogMutation}] ...]. Each subsequence
* represents the changes in one transaction. The {@code CommitLogManifest} contains deleted
* entity keys, whereas each {@code CommitLogMutation} contains one whole entity.
*/
public static ImmutableList<ImmutableList<VersionedEntity>> loadEntitiesByTransaction(
InputStream inputStream) {
try (AppEngineEnvironment appEngineEnvironment = new AppEngineEnvironment();
InputStream input = new BufferedInputStream(inputStream)) {
Iterator<ImmutableObject> commitLogs = createDeserializingIterator(input);
checkState(commitLogs.hasNext());
checkState(commitLogs.next() instanceof CommitLogCheckpoint);
ImmutableList.Builder<ImmutableList<VersionedEntity>> resultBuilder =
new ImmutableList.Builder<>();
ImmutableList.Builder<VersionedEntity> currentTransactionBuilder =
new ImmutableList.Builder<>();
while (commitLogs.hasNext()) {
ImmutableObject currentObject = commitLogs.next();
if (currentObject instanceof CommitLogManifest) {
// CommitLogManifest means we are starting a new transaction
addIfNonempty(resultBuilder, currentTransactionBuilder);
currentTransactionBuilder = new ImmutableList.Builder<>();
VersionedEntity.fromManifest((CommitLogManifest) currentObject)
.forEach(currentTransactionBuilder::add);
} else if (currentObject instanceof CommitLogMutation) {
currentTransactionBuilder.add(
VersionedEntity.fromMutation((CommitLogMutation) currentObject));
} else {
throw new IllegalStateException(
String.format("Unknown entity type %s in commit logs", currentObject.getClass()));
}
}
// Add the last transaction in (if it's not empty)
addIfNonempty(resultBuilder, currentTransactionBuilder);
return resultBuilder.build();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Returns entities in an {@code inputStream} (from a single CommitLog file) as an {@link
* ImmutableList} of {@link VersionedEntity} records. Upon completion the {@code inputStream} is
@@ -57,23 +106,9 @@ public final class CommitLogImports {
* entity keys, whereas each {@code CommitLogMutation} contains one whole entity.
*/
public static ImmutableList<VersionedEntity> loadEntities(InputStream inputStream) {
try (AppEngineEnvironment appEngineEnvironment = new AppEngineEnvironment();
InputStream input = new BufferedInputStream(inputStream)) {
Iterator<ImmutableObject> commitLogs = createDeserializingIterator(input);
checkState(commitLogs.hasNext());
checkState(commitLogs.next() instanceof CommitLogCheckpoint);
return Streams.stream(commitLogs)
.map(
e ->
e instanceof CommitLogManifest
? VersionedEntity.fromManifest((CommitLogManifest) e)
: Stream.of(VersionedEntity.fromMutation((CommitLogMutation) e)))
.flatMap(s -> s)
.collect(ImmutableList.toImmutableList());
} catch (IOException e) {
throw new RuntimeException(e);
}
return loadEntitiesByTransaction(inputStream).stream()
.flatMap(ImmutableList::stream)
.collect(toImmutableList());
}
/** Covenience method that adapts {@link #loadEntities(InputStream)} to a {@link File}. */
@@ -92,4 +127,13 @@ public final class CommitLogImports {
public static ImmutableList<VersionedEntity> loadEntities(ReadableByteChannel channel) {
return loadEntities(Channels.newInputStream(channel));
}
private static void addIfNonempty(
ImmutableList.Builder<ImmutableList<VersionedEntity>> resultBuilder,
ImmutableList.Builder<VersionedEntity> currentTransactionBuilder) {
ImmutableList<VersionedEntity> currentTransaction = currentTransactionBuilder.build();
if (!currentTransaction.isEmpty()) {
resultBuilder.add(currentTransaction);
}
}
}

View File

@@ -122,7 +122,7 @@ class GcsDiffFileLister {
// Reconstruct the sequence of files by traversing backwards from "lastUpperBoundTime" (i.e. the
// last file that we found) and finding its previous file until we either run out of files or
// get to one that preceeds "fromTime".
// get to one that precedes "fromTime".
//
// GCS file listing is eventually consistent, so it's possible that we are missing a file. The
// metadata of a file is sufficient to identify the preceding file, so if we start from the

View File

@@ -0,0 +1,188 @@
// 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.backup;
import static google.registry.backup.ExportCommitLogDiffAction.DIFF_FILE_PREFIX;
import static google.registry.model.ofy.EntityWritePriorities.getEntityPriority;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static org.joda.time.Duration.standardHours;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.tools.cloudstorage.GcsFileMetadata;
import com.google.appengine.tools.cloudstorage.GcsService;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig;
import google.registry.model.server.Lock;
import google.registry.model.translators.VKeyTranslatorFactory;
import google.registry.persistence.VKey;
import google.registry.request.Action;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.schema.replay.DatastoreEntity;
import google.registry.schema.replay.DatastoreOnlyEntity;
import google.registry.schema.replay.NonReplicatedEntity;
import google.registry.schema.replay.SqlReplayCheckpoint;
import google.registry.util.RequestStatusChecker;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.Channels;
import java.util.Optional;
import javax.inject.Inject;
import javax.servlet.http.HttpServletResponse;
import org.joda.time.DateTime;
import org.joda.time.Duration;
/** Action that replays commit logs to Cloud SQL to keep it up to date. */
@Action(
service = Action.Service.BACKEND,
path = ReplayCommitLogsToSqlAction.PATH,
method = Action.Method.POST,
automaticallyPrintOk = true,
auth = Auth.AUTH_INTERNAL_OR_ADMIN)
public class ReplayCommitLogsToSqlAction implements Runnable {
static final String PATH = "/_dr/task/replayCommitLogsToSql";
private static final int BLOCK_SIZE =
1024 * 1024; // Buffer 1mb at a time, for no particular reason.
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final Duration LEASE_LENGTH = standardHours(1);
@Inject GcsService gcsService;
@Inject Response response;
@Inject RequestStatusChecker requestStatusChecker;
@Inject GcsDiffFileLister diffLister;
@Inject
ReplayCommitLogsToSqlAction() {}
@Override
public void run() {
if (!RegistryConfig.getCloudSqlReplayCommitLogs()) {
String message = "ReplayCommitLogsToSqlAction was called but disabled in the config.";
logger.atWarning().log(message);
// App Engine will retry on any non-2xx status code, which we don't want in this case.
response.setStatus(SC_NO_CONTENT);
response.setPayload(message);
return;
}
Optional<Lock> lock =
Lock.acquire(
this.getClass().getSimpleName(), null, LEASE_LENGTH, requestStatusChecker, false);
if (lock.isEmpty()) {
String message = "Can't acquire SQL commit log replay lock, aborting.";
logger.atSevere().log(message);
// App Engine will retry on any non-2xx status code, which we don't want in this case.
// Let the next run after the next export happen naturally.
response.setStatus(SC_NO_CONTENT);
response.setPayload(message);
return;
}
try {
replayFiles();
response.setStatus(HttpServletResponse.SC_OK);
logger.atInfo().log("ReplayCommitLogsToSqlAction completed successfully.");
} finally {
lock.ifPresent(Lock::release);
}
}
private void replayFiles() {
// Start at the first millisecond we haven't seen yet
DateTime fromTime = jpaTm().transact(() -> SqlReplayCheckpoint.get().plusMillis(1));
// If there's an inconsistent file set, this will throw IllegalStateException and the job
// will try later -- this is likely because an export hasn't finished yet.
ImmutableList<GcsFileMetadata> commitLogFiles =
diffLister.listDiffFiles(fromTime, /* current time */ null);
for (GcsFileMetadata metadata : commitLogFiles) {
// One transaction per GCS file
jpaTm().transact(() -> processFile(metadata));
}
logger.atInfo().log("Replayed %d commit log files to SQL successfully.", commitLogFiles.size());
}
private void processFile(GcsFileMetadata metadata) {
try (InputStream input =
Channels.newInputStream(
gcsService.openPrefetchingReadChannel(metadata.getFilename(), 0, BLOCK_SIZE))) {
// Load and process the Datastore transactions one at a time
ImmutableList<ImmutableList<VersionedEntity>> allTransactions =
CommitLogImports.loadEntitiesByTransaction(input);
allTransactions.forEach(this::replayTransaction);
// if we succeeded, set the last-seen time
DateTime checkpoint =
DateTime.parse(
metadata.getFilename().getObjectName().substring(DIFF_FILE_PREFIX.length()));
SqlReplayCheckpoint.set(checkpoint);
logger.atInfo().log("Replayed %d transactions from commit log file.", allTransactions.size());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void replayTransaction(ImmutableList<VersionedEntity> transaction) {
transaction.stream()
.sorted(ReplayCommitLogsToSqlAction::compareByWeight)
.forEach(
versionedEntity ->
versionedEntity
.getEntity()
.ifPresentOrElse(
this::handleEntityPut, () -> handleEntityDelete(versionedEntity)));
}
private void handleEntityPut(Entity entity) {
Object ofyPojo = ofy().toPojo(entity);
if (ofyPojo instanceof DatastoreEntity) {
DatastoreEntity datastoreEntity = (DatastoreEntity) ofyPojo;
datastoreEntity.toSqlEntity().ifPresent(jpaTm()::put);
} else {
// this should never happen, but we shouldn't fail on it
logger.atSevere().log(
"%s does not implement DatastoreEntity, which is necessary for SQL replay.",
ofyPojo.getClass());
}
}
private void handleEntityDelete(VersionedEntity entityToDelete) {
Key key = entityToDelete.key();
VKey<?> entityVKey;
try {
entityVKey = VKeyTranslatorFactory.createVKey(key);
} catch (RuntimeException e) {
// This means that the key wasn't convertible to VKey through the standard methods or via
// a createVKey method. This means that the object isn't persisted in SQL so we ignore it.
logger.atInfo().log(
"Skipping SQL delete for kind %s since it is not convertible.", key.getKind());
return;
}
Class<?> entityClass = entityVKey.getKind();
// Delete the key iff the class represents a JPA entity that is replicated
if (!NonReplicatedEntity.class.isAssignableFrom(entityClass)
&& !DatastoreOnlyEntity.class.isAssignableFrom(entityClass)
&& entityClass.getAnnotation(javax.persistence.Entity.class) != null) {
jpaTm().delete(entityVKey);
}
}
private static int compareByWeight(VersionedEntity a, VersionedEntity b) {
return getEntityPriority(a.key().getKind(), a.getEntity().isEmpty())
- getEntityPriority(b.key().getKind(), b.getEntity().isEmpty());
}
}

View File

@@ -50,11 +50,21 @@ public class ResaveAllEppResourcesAction implements Runnable {
@Inject Response response;
@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(),
ImmutableList.of(EppResourceInputs.createKeyInput(EppResource.class)))

View File

@@ -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;
@@ -68,6 +69,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 +266,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 +284,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 +424,22 @@ 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
@@ -429,13 +472,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)));
}

View File

@@ -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 =

View File

@@ -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) {
@@ -1592,6 +1600,22 @@ public final class RegistryConfig {
CONFIG_SETTINGS.get().cloudSql.replicateTransactions = replicateTransactions;
}
/**
* Returns whether or not to replay commit logs to the SQL database after export to GCS.
*
* <p>If true, we will trigger the {@link google.registry.backup.ReplayCommitLogsToSqlAction}
* after the {@link google.registry.backup.ExportCommitLogDiffAction} to load the commit logs and
* replay them to SQL.
*/
public static boolean getCloudSqlReplayCommitLogs() {
return CONFIG_SETTINGS.get().cloudSql.replayCommitLogs;
}
@VisibleForTesting
public static void overrideCloudSqlReplayCommitLogs(boolean replayCommitLogs) {
CONFIG_SETTINGS.get().cloudSql.replayCommitLogs = replayCommitLogs;
}
/** Returns the roid suffix to be used for the roids of all contacts and hosts. */
public static String getContactAndHostRoidSuffix() {
return CONFIG_SETTINGS.get().registryPolicy.contactAndHostRoidSuffix;

View File

@@ -126,6 +126,7 @@ public class RegistryConfigSettings {
public String username;
public String instanceConnectionName;
public boolean replicateTransactions;
public boolean replayCommitLogs;
}
/** Configuration for Apache Beam (Cloud Dataflow). */

View File

@@ -233,6 +233,8 @@ cloudSql:
# Set this to true to replicate cloud SQL transactions to datastore in the
# background.
replicateTransactions: false
# Set this to true to enable replay of commit logs to SQL
replayCommitLogs: false
cloudDns:
# Set both properties to null in Production.

View File

@@ -208,6 +208,12 @@
<max-concurrent-requests>5</max-concurrent-requests>
</queue>
<!-- Queue for replaying commit logs to SQL during the transition from Datastore -> SQL. -->
<queue>
<name>replay-commit-logs-to-sql</name>
<rate>1/s</rate>
</queue>
<!-- The load[0-9] queues are used for load-testing, and can be safely deleted
in any environment that doesn't require load-testing. -->
<queue>

View File

@@ -74,6 +74,14 @@ public class EppRequestHandler {
&& eppOutput.getResponse().getResult().getCode() == SUCCESS_AND_CLOSE) {
response.setHeader("Epp-Session", "close");
}
// If a login request returns a success, a logged-in header is added to the response to inform
// the proxy that it is no longer necessary to send the full client certificate to the backend
// for this connection.
if (eppOutput.isResponse()
&& eppOutput.getResponse().isLoginResponse()
&& eppOutput.isSuccess()) {
response.setHeader("Logged-In", "true");
}
} catch (Exception e) {
logger.atWarning().withCause(e).log("handleEppCommand general exception");
response.setStatus(SC_BAD_REQUEST);

View File

@@ -141,7 +141,7 @@ public class LoginFlow implements Flow {
sessionMetadata.resetFailedLoginAttempts();
sessionMetadata.setClientId(login.getClientId());
sessionMetadata.setServiceExtensionUris(serviceExtensionUrisBuilder.build());
return responseBuilder.build();
return responseBuilder.setIsLoginResponse().build();
}
/** Registrar with this client ID could not be found. */

View File

@@ -211,11 +211,6 @@ public final class OteAccountBuilder {
return transformRegistrars(builder -> builder.setPassword(password));
}
/** Sets the client certificate hash to all the OT&amp;E Registrars. */
public OteAccountBuilder setCertificateHash(String certHash) {
return transformRegistrars(builder -> builder.setClientCertificateHash(certHash));
}
/** Sets the client certificate to all the OT&amp;E Registrars. */
public OteAccountBuilder setCertificate(String asciiCert, DateTime now) {
return transformRegistrars(builder -> builder.setClientCertificate(asciiCert, now));

View File

@@ -28,6 +28,11 @@ import org.joda.time.DateTime;
*/
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).
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();
}
}

View File

@@ -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. */

View File

@@ -14,7 +14,6 @@
package google.registry.model.contact;
import com.google.common.collect.ImmutableList;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.EntitySubclass;
import google.registry.model.ImmutableObject;
@@ -111,8 +110,8 @@ public class ContactHistory extends HistoryEntry implements SqlEntity {
// In Datastore, save as a HistoryEntry object regardless of this object's type
@Override
public ImmutableList<DatastoreEntity> toDatastoreEntities() {
return ImmutableList.of(asHistoryEntry());
public Optional<DatastoreEntity> toDatastoreEntity() {
return Optional.of(asHistoryEntry());
}
/** Class to represent the composite primary key of {@link ContactHistory} entity. */

View File

@@ -17,7 +17,6 @@ package google.registry.model.domain;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.EntitySubclass;
@@ -250,8 +249,8 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
// In Datastore, save as a HistoryEntry object regardless of this object's type
@Override
public ImmutableList<DatastoreEntity> toDatastoreEntities() {
return ImmutableList.of(asHistoryEntry());
public Optional<DatastoreEntity> toDatastoreEntity() {
return Optional.of(asHistoryEntry());
}
/** Class to represent the composite primary key of {@link DomainHistory} entity. */

View File

@@ -14,11 +14,11 @@
package google.registry.model.domain.secdns;
import com.google.common.collect.ImmutableList;
import google.registry.model.domain.DomainHistory;
import google.registry.model.ofy.ObjectifyService;
import google.registry.schema.replay.DatastoreEntity;
import google.registry.schema.replay.SqlEntity;
import java.util.Optional;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Column;
@@ -86,7 +86,7 @@ public class DomainDsDataHistory extends DomainDsDataBase implements SqlEntity {
}
@Override
public ImmutableList<DatastoreEntity> toDatastoreEntities() {
return ImmutableList.of(); // not persisted in Datastore
public Optional<DatastoreEntity> toDatastoreEntity() {
return Optional.empty(); // Not persisted in Datastore
}
}

View File

@@ -65,6 +65,7 @@ import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementRef;
import javax.xml.bind.annotation.XmlElementRefs;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.XmlType;
/**
@@ -87,6 +88,9 @@ public class EppResponse extends ImmutableObject implements ResponseOrGreeting {
/** The command result. The RFC allows multiple failure results, but we always return one. */
Result result;
/** Indicates if this response is for a login request. */
@XmlTransient boolean isLoginResponse = false;
/**
* Information about messages queued for retrieval. This may appear in response to any EPP message
* (if messages are queued), but in practice this will only be set in response to a poll request.
@@ -178,6 +182,10 @@ public class EppResponse extends ImmutableObject implements ResponseOrGreeting {
return result;
}
public boolean isLoginResponse() {
return isLoginResponse;
}
/** Marker interface for types that can go in the {@link #resData} field. */
public interface ResponseData {}
@@ -222,5 +230,10 @@ public class EppResponse extends ImmutableObject implements ResponseOrGreeting {
getInstance().extensions = forceEmptyToNull(extensions);
return this;
}
public Builder setIsLoginResponse() {
getInstance().isLoginResponse = true;
return this;
}
}
}

View File

@@ -14,7 +14,6 @@
package google.registry.model.host;
import com.google.common.collect.ImmutableList;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.EntitySubclass;
import google.registry.model.ImmutableObject;
@@ -112,8 +111,8 @@ public class HostHistory extends HistoryEntry implements SqlEntity {
// In Datastore, save as a HistoryEntry object regardless of this object's type
@Override
public ImmutableList<DatastoreEntity> toDatastoreEntities() {
return ImmutableList.of(asHistoryEntry());
public Optional<DatastoreEntity> toDatastoreEntity() {
return Optional.of(asHistoryEntry());
}
/** Class to represent the composite primary key of {@link HostHistory} entity. */

View File

@@ -0,0 +1,60 @@
// 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.ofy;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
/**
* Contains the mapping from class names to SQL-replay-write priorities.
*
* <p>When replaying Datastore commit logs to SQL (asynchronous replication), in order to avoid
* issues with foreign keys, we should replay entity writes so that foreign key references are
* always written after the entity that they reference. This class represents that DAG, where lower
* values represent an earlier write (and later delete). Higher-valued classes can have foreign keys
* on lower-valued classes, but not vice versa.
*/
public class EntityWritePriorities {
/**
* Mapping from class name to "priority".
*
* <p>Here, "priority" means the order in which the class should be inserted / updated in a
* transaction with respect to instances of other classes. By default, all classes have a priority
* number of zero.
*
* <p>For each transaction, classes should be written in priority order from the lowest number to
* the highest, in order to maintain foreign-key write consistency. For the same reason, deletes
* should happen after all writes.
*/
static final ImmutableMap<String, Integer> CLASS_PRIORITIES =
ImmutableMap.of(
"HistoryEntry", -10,
"AllocationToken", -9,
"ContactResource", 5,
"DomainBase", 10);
// The beginning of the range of priority numbers reserved for delete. This must be greater than
// any of the values in CLASS_PRIORITIES by enough overhead to accommodate any negative values in
// it. Note: by design, deletions will happen in the opposite order of insertions, which is
// necessary to make sure foreign keys aren't violated during deletion.
@VisibleForTesting static final int DELETE_RANGE = Integer.MAX_VALUE / 2;
/** Returns the priority of the entity type in the map entry. */
public static int getEntityPriority(String kind, boolean isDelete) {
int priority = CLASS_PRIORITIES.getOrDefault(kind, 0);
return isDelete ? DELETE_RANGE - priority : priority;
}
}

View File

@@ -20,6 +20,7 @@ import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Maps.filterValues;
import static com.google.common.collect.Maps.toMap;
import static google.registry.model.ofy.CommitLogBucket.getArbitraryBucketId;
import static google.registry.model.ofy.EntityWritePriorities.getEntityPriority;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
@@ -29,7 +30,6 @@ import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.Key;
import google.registry.persistence.VKey;
import google.registry.schema.replay.DatastoreEntity;
import google.registry.schema.replay.SqlEntity;
import java.util.Map;
import org.joda.time.DateTime;
@@ -61,7 +61,7 @@ class TransactionInfo {
TransactionInfo(DateTime now) {
this.transactionTime = now;
ofy().load().key(bucketKey); // Asynchronously load value into session cache.
ofy().load().key(bucketKey); // Asynchronously load value into session cache.
}
TransactionInfo setReadOnly() {
@@ -101,23 +101,10 @@ class TransactionInfo {
.collect(toImmutableSet());
}
// Mapping from class name to "weight" (which in this case is the order in which the class must
// be "put" in a transaction with respect to instances of other classes). Lower weight classes
// are put first, by default all classes have a weight of zero.
static final ImmutableMap<String, Integer> CLASS_WEIGHTS =
ImmutableMap.of(
"HistoryEntry", -1,
"DomainBase", 1);
// The beginning of the range of weights reserved for delete. This must be greater than any of
// the values in CLASS_WEIGHTS by enough overhead to accomodate any negative values in it.
@VisibleForTesting static final int DELETE_RANGE = Integer.MAX_VALUE / 2;
/** Returns the weight of the entity type in the map entry. */
@VisibleForTesting
static int getWeight(ImmutableMap.Entry<Key<?>, Object> entry) {
int weight = CLASS_WEIGHTS.getOrDefault(entry.getKey().getKind(), 0);
return entry.getValue().equals(Delete.SENTINEL) ? DELETE_RANGE - weight : weight;
return getEntityPriority(entry.getKey().getKind(), entry.getValue().equals(Delete.SENTINEL));
}
private static int compareByWeight(
@@ -137,10 +124,9 @@ class TransactionInfo {
if (entry.getValue().equals(Delete.SENTINEL)) {
jpaTm().delete(VKey.from(entry.getKey()));
} else {
for (SqlEntity entity :
((DatastoreEntity) entry.getValue()).toSqlEntities()) {
jpaTm().put(entity);
}
((DatastoreEntity) entry.getValue())
.toSqlEntity()
.ifPresent(jpaTm()::put);
}
});
});

View File

@@ -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. */

View File

@@ -856,26 +856,6 @@ public class Registrar extends ImmutableObject
}
}
/**
* Sets client certificate hash, but not the certificate.
*
* <p><b>Warning:</b> {@link #setClientCertificate(String, DateTime)} sets the hash for you and
* is preferred. Calling this method will nullify the {@code clientCertificate} field.
*/
public Builder setClientCertificateHash(String clientCertificateHash) {
if (clientCertificateHash != null) {
checkArgument(
Pattern.matches("[A-Za-z0-9+/]+", clientCertificateHash),
"--cert_hash not a valid base64 (no padding) value");
checkArgument(
base64().decode(clientCertificateHash).length == 256 / 8,
"--cert_hash base64 does not decode to 256 bits");
}
getInstance().clientCertificate = null;
getInstance().clientCertificateHash = clientCertificateHash;
return this;
}
public Builder setContactsRequireSyncing(boolean contactsRequireSyncing) {
getInstance().contactsRequireSyncing = contactsRequireSyncing;
return this;

View File

@@ -18,7 +18,6 @@ import static com.googlecode.objectify.Key.getKind;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
@@ -45,6 +44,7 @@ import google.registry.model.host.HostResource;
import google.registry.persistence.VKey;
import google.registry.schema.replay.DatastoreEntity;
import google.registry.schema.replay.SqlEntity;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nullable;
import javax.persistence.Access;
@@ -314,8 +314,8 @@ public class HistoryEntry extends ImmutableObject implements Buildable, Datastor
// In SQL, save the child type
@Override
public ImmutableList<SqlEntity> toSqlEntities() {
return ImmutableList.of((SqlEntity) toChildHistoryEntity());
public Optional<SqlEntity> toSqlEntity() {
return Optional.of((SqlEntity) toChildHistoryEntity());
}
/** Creates a {@link VKey} instance from a {@link Key} instance. */

View File

@@ -18,13 +18,13 @@ import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.util.CollectionUtils.isNullOrEmpty;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.schema.replay.DatastoreEntity;
import google.registry.schema.replay.SqlEntity;
import google.registry.util.DomainNameUtils;
import java.util.Optional;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.Entity;
@@ -111,8 +111,8 @@ public class Spec11ThreatMatch extends ImmutableObject implements Buildable, Sql
}
@Override
public ImmutableList<DatastoreEntity> toDatastoreEntities() {
return ImmutableList.of(); // not stored in Datastore
public Optional<DatastoreEntity> toDatastoreEntity() {
return Optional.empty(); // Not persisted in Datastore
}
@Override

View File

@@ -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 {

View File

@@ -20,7 +20,6 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableList;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
import google.registry.model.annotations.NotBackedUp;
@@ -28,9 +27,7 @@ import google.registry.model.annotations.NotBackedUp.Reason;
import google.registry.model.common.CrossTldSingleton;
import google.registry.model.tmch.TmchCrl.TmchCrlId;
import google.registry.persistence.VKey;
import google.registry.schema.replay.DatastoreEntity;
import google.registry.schema.replay.NonReplicatedEntity;
import google.registry.schema.replay.SqlEntity;
import java.io.Serializable;
import java.util.Optional;
import javax.annotation.concurrent.Immutable;
@@ -106,16 +103,6 @@ public final class TmchCrl extends CrossTldSingleton implements NonReplicatedEnt
return updated;
}
@Override
public ImmutableList<SqlEntity> toSqlEntities() {
return ImmutableList.of(); // dually-written
}
@Override
public ImmutableList<DatastoreEntity> toDatastoreEntities() {
return ImmutableList.of(); // dually-written
}
static class TmchCrlId implements Serializable {
@Column(name = "certificateRevocations")

View File

@@ -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();
}
};
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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. */

View File

@@ -14,9 +14,9 @@
package google.registry.persistence.transaction;
import com.google.common.collect.ImmutableList;
import google.registry.schema.replay.DatastoreEntity;
import google.registry.schema.replay.SqlEntity;
import java.util.Optional;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
@@ -45,7 +45,7 @@ public class TransactionEntity implements SqlEntity {
}
@Override
public ImmutableList<DatastoreEntity> toDatastoreEntities() {
return ImmutableList.of(); // not stored in Datastore per se
public Optional<DatastoreEntity> toDatastoreEntity() {
return Optional.empty(); // Not persisted in Datastore per se
}
}

View File

@@ -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.
*

View File

@@ -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");

View File

@@ -14,12 +14,12 @@
package google.registry.privileges.secretmanager;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.cloud.secretmanager.v1.SecretManagerServiceClient;
import dagger.Component;
import dagger.Module;
import dagger.Provides;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.util.Retrier;
import google.registry.util.UtilsModule;
@@ -28,19 +28,16 @@ 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 SecretManagerClient provideSecretManagerClient(
@Config("projectId") String project, Retrier retrier) {
try {
return new SecretManagerClientImpl(project, SecretManagerServiceClient.create(), retrier);
SecretManagerServiceClient stub = SecretManagerServiceClient.create();
Runtime.getRuntime().addShutdownHook(new Thread(stub::close));
return new SecretManagerClientImpl(project, stub, retrier);
} catch (IOException e) {
throw new RuntimeException(e);
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,62 @@
// 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;
}
/** 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()));
}
}
}

View File

@@ -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,25 @@ 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();
}
ImmutableSet.Builder<RegistrarThreatMatches> builder = ImmutableSet.builder();
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 +103,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);

View File

@@ -16,7 +16,6 @@ package google.registry.schema.cursor;
import static com.google.appengine.api.search.checkers.Preconditions.checkNotNull;
import com.google.common.collect.ImmutableList;
import google.registry.model.ImmutableObject;
import google.registry.model.UpdateAutoTimestamp;
import google.registry.model.common.Cursor.CursorType;
@@ -26,6 +25,7 @@ import google.registry.schema.replay.SqlEntity;
import google.registry.util.DateTimeUtils;
import java.io.Serializable;
import java.time.ZonedDateTime;
import java.util.Optional;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
@@ -104,8 +104,8 @@ public class Cursor implements SqlEntity {
}
@Override
public ImmutableList<DatastoreEntity> toDatastoreEntities() {
return ImmutableList.of(); // Cursors are not converted since they are ephemeral
public Optional<DatastoreEntity> toDatastoreEntity() {
return Optional.empty(); // Cursors are not converted since they are ephemeral
}
static class CursorId extends ImmutableObject implements Serializable {

View File

@@ -19,7 +19,6 @@ import static google.registry.util.DateTimeUtils.isBeforeOrAt;
import static google.registry.util.DateTimeUtils.toZonedDateTime;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.common.collect.ImmutableList;
import google.registry.model.Buildable;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.ImmutableObject;
@@ -234,8 +233,8 @@ public final class RegistryLock extends ImmutableObject implements Buildable, Sq
}
@Override
public ImmutableList<DatastoreEntity> toDatastoreEntities() {
return ImmutableList.of(); // not stored in Datastore
public Optional<DatastoreEntity> toDatastoreEntity() {
return Optional.empty(); // Not persisted in Datastore
}
/** Builder for {@link google.registry.schema.domain.RegistryLock}. */

View File

@@ -14,18 +14,18 @@
package google.registry.schema.replay;
import com.google.common.collect.ImmutableList;
import java.util.Optional;
/** An entity that has the same Java object representation in SQL and Datastore. */
public interface DatastoreAndSqlEntity extends DatastoreEntity, SqlEntity {
@Override
default ImmutableList<DatastoreEntity> toDatastoreEntities() {
return ImmutableList.of(this);
default Optional<DatastoreEntity> toDatastoreEntity() {
return Optional.of(this);
}
@Override
default ImmutableList<SqlEntity> toSqlEntities() {
return ImmutableList.of(this);
default Optional<SqlEntity> toSqlEntity() {
return Optional.of(this);
}
}

View File

@@ -14,7 +14,7 @@
package google.registry.schema.replay;
import com.google.common.collect.ImmutableList;
import java.util.Optional;
/**
* An object that can be stored in Datastore and serialized using Objectify's {@link
@@ -26,5 +26,5 @@ import com.google.common.collect.ImmutableList;
*/
public interface DatastoreEntity {
ImmutableList<SqlEntity> toSqlEntities();
Optional<SqlEntity> toSqlEntity();
}

View File

@@ -14,11 +14,12 @@
package google.registry.schema.replay;
import com.google.common.collect.ImmutableList;
import java.util.Optional;
/** An entity that is only stored in Datastore, that should not be replayed to SQL. */
public interface DatastoreOnlyEntity extends DatastoreEntity {
@Override
default ImmutableList<SqlEntity> toSqlEntities() {
return ImmutableList.of();
default Optional<SqlEntity> toSqlEntity() {
return Optional.empty();
}
}

View File

@@ -14,7 +14,7 @@
package google.registry.schema.replay;
import com.google.common.collect.ImmutableList;
import java.util.Optional;
/**
* Represents an entity that should not participate in asynchronous replication.
@@ -24,12 +24,12 @@ import com.google.common.collect.ImmutableList;
public interface NonReplicatedEntity extends DatastoreEntity, SqlEntity {
@Override
default ImmutableList<DatastoreEntity> toDatastoreEntities() {
return ImmutableList.of();
default Optional<DatastoreEntity> toDatastoreEntity() {
return Optional.empty();
}
@Override
default ImmutableList<SqlEntity> toSqlEntities() {
return ImmutableList.of();
default Optional<SqlEntity> toSqlEntity() {
return Optional.empty();
}
}

View File

@@ -14,7 +14,7 @@
package google.registry.schema.replay;
import com.google.common.collect.ImmutableList;
import java.util.Optional;
/**
* An object that can be stored in Cloud SQL using {@link
@@ -25,5 +25,5 @@ import com.google.common.collect.ImmutableList;
*/
public interface SqlEntity {
ImmutableList<DatastoreEntity> toDatastoreEntities();
Optional<DatastoreEntity> toDatastoreEntity();
}

View File

@@ -18,7 +18,7 @@ import static google.registry.model.common.CrossTldSingleton.SINGLETON_ID;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import com.google.common.collect.ImmutableList;
import java.util.Optional;
import javax.persistence.Entity;
import javax.persistence.Id;
import org.joda.time.DateTime;
@@ -32,8 +32,8 @@ public class SqlReplayCheckpoint implements SqlEntity {
private DateTime lastReplayTime;
@Override
public ImmutableList<DatastoreEntity> toDatastoreEntities() {
return ImmutableList.of(); // not necessary to persist in Datastore
public Optional<DatastoreEntity> toDatastoreEntity() {
return Optional.empty(); // Not necessary to persist in Datastore
}
public static DateTime get() {

View File

@@ -16,7 +16,6 @@ package google.registry.schema.server;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.common.collect.ImmutableList;
import google.registry.model.ImmutableObject;
import google.registry.schema.replay.DatastoreEntity;
import google.registry.schema.replay.SqlEntity;
@@ -24,6 +23,7 @@ import google.registry.schema.server.Lock.LockId;
import google.registry.util.DateTimeUtils;
import java.io.Serializable;
import java.time.ZonedDateTime;
import java.util.Optional;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
@@ -120,8 +120,8 @@ public class Lock implements SqlEntity {
}
@Override
public ImmutableList<DatastoreEntity> toDatastoreEntities() {
return ImmutableList.of(); // Locks are not converted since they are ephemeral
public Optional<DatastoreEntity> toDatastoreEntity() {
return Optional.empty(); // Locks are not converted since they are ephemeral
}
static class LockId extends ImmutableObject implements Serializable {

View File

@@ -14,13 +14,13 @@
package google.registry.schema.tld;
import com.google.common.collect.ImmutableList;
import google.registry.model.ImmutableObject;
import google.registry.model.registry.label.PremiumList;
import google.registry.schema.replay.DatastoreEntity;
import google.registry.schema.replay.SqlEntity;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Optional;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
@@ -47,7 +47,7 @@ public class PremiumEntry extends ImmutableObject implements Serializable, SqlEn
private PremiumEntry() {}
@Override
public ImmutableList<DatastoreEntity> toDatastoreEntities() {
return ImmutableList.of(); // PremiumList is dually-written
public Optional<DatastoreEntity> toDatastoreEntity() {
return Optional.empty(); // PremiumList is dually-written
}
}

View File

@@ -138,15 +138,6 @@ abstract class CreateOrUpdateRegistrarCommand extends MutatingCommand {
validateWith = PathParameter.InputFile.class)
Path clientCertificateFilename;
@Nullable
@Parameter(
names = "--cert_hash",
description =
"Hash of client certificate (SHA256 base64 no padding). Do not use this unless "
+ "you want to store ONLY the hash and not the full certificate"
)
String clientCertificateHash;
@Nullable
@Parameter(
names = "--failover_cert_file",
@@ -375,14 +366,6 @@ abstract class CreateOrUpdateRegistrarCommand extends MutatingCommand {
}
builder.setFailoverClientCertificate(asciiCert, now);
}
if (!isNullOrEmpty(clientCertificateHash)) {
checkArgument(clientCertificateFilename == null,
"Can't specify both --cert_hash and --cert_file");
if ("null".equals(clientCertificateHash)) {
clientCertificateHash = null;
}
builder.setClientCertificateHash(clientCertificateHash);
}
if (ianaId != null) {
builder.setIanaIdentifier(ianaId.orElse(null));
}

View File

@@ -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();

View File

@@ -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));

View File

@@ -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));
}
}
}

View File

@@ -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);
}

View File

@@ -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)

View File

@@ -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);

View File

@@ -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");
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -65,13 +65,6 @@ final class SetupOteCommand extends ConfirmingCommand implements CommandWithRemo
validateWith = PathParameter.InputFile.class)
private Path certFile;
@Parameter(
names = {"-h", "--certhash"},
description =
"Hash of client certificate (SHA256 base64 no padding). Do not use this unless "
+ "you want to store ONLY the hash and not the full certificate.")
private String certHash;
@Parameter(
names = {"--overwrite"},
description = "Whether to replace existing entities if we encounter any, instead of failing.")
@@ -89,9 +82,7 @@ final class SetupOteCommand extends ConfirmingCommand implements CommandWithRemo
/** Run any pre-execute command checks */
@Override
protected void init() throws Exception {
checkArgument(
certFile == null ^ certHash == null,
"Must specify exactly one of client certificate file or client certificate hash.");
checkArgument(certFile != null, "Must specify exactly one client certificate file.");
password = passwordGenerator.createString(PASSWORD_LENGTH);
oteAccountBuilder =
@@ -101,16 +92,10 @@ final class SetupOteCommand extends ConfirmingCommand implements CommandWithRemo
.setIpAllowList(ipAllowList)
.setReplaceExisting(overwrite);
if (certFile != null) {
String asciiCert = MoreFiles.asCharSource(certFile, US_ASCII).read();
// Don't wait for create_registrar to fail if it's a bad certificate file.
loadCertificate(asciiCert);
oteAccountBuilder.setCertificate(asciiCert, clock.nowUtc());
}
if (certHash != null) {
oteAccountBuilder.setCertificateHash(certHash);
}
}
@Override

View File

@@ -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()));
}
}

View File

@@ -0,0 +1,458 @@
// 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.backup;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService;
import static google.registry.backup.RestoreCommitLogsActionTest.GCS_BUCKET;
import static google.registry.backup.RestoreCommitLogsActionTest.createCheckpoint;
import static google.registry.backup.RestoreCommitLogsActionTest.saveDiffFile;
import static google.registry.backup.RestoreCommitLogsActionTest.saveDiffFileNotToRestore;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.model.ofy.CommitLogBucket.getBucketKey;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.newDomainBase;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import com.google.appengine.tools.cloudstorage.GcsService;
import com.google.appengine.tools.cloudstorage.GcsServiceFactory;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.truth.Truth8;
import com.googlecode.objectify.Key;
import google.registry.config.RegistryConfig;
import google.registry.model.common.Cursor;
import google.registry.model.common.Cursor.CursorType;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.secdns.DelegationSignerData;
import google.registry.model.ofy.CommitLogBucket;
import google.registry.model.ofy.CommitLogManifest;
import google.registry.model.ofy.CommitLogMutation;
import google.registry.model.registrar.RegistrarContact;
import google.registry.model.registry.label.ReservedList;
import google.registry.model.server.Lock;
import google.registry.model.tmch.ClaimsListShard;
import google.registry.model.translators.VKeyTranslatorFactory;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.persistence.transaction.TransactionManagerFactory;
import google.registry.schema.replay.SqlReplayCheckpoint;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeResponse;
import google.registry.testing.TestObject;
import google.registry.util.RequestStatusChecker;
import java.io.IOException;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
/** Tests for {@link ReplayCommitLogsToSqlAction}. */
@ExtendWith(MockitoExtension.class)
public class ReplayCommitLogsToSqlActionTest {
private final FakeClock fakeClock = new FakeClock(DateTime.parse("2000-01-01TZ"));
@RegisterExtension
public final AppEngineExtension appEngine =
AppEngineExtension.builder()
.withDatastoreAndCloudSql()
.withClock(fakeClock)
.withOfyTestEntities(TestObject.class)
.withJpaUnitTestEntities(
RegistrarContact.class,
TestObject.class,
SqlReplayCheckpoint.class,
ContactResource.class,
DomainBase.class,
GracePeriod.class,
DelegationSignerData.class)
.build();
/** Local GCS service. */
private final GcsService gcsService = GcsServiceFactory.createGcsService();
private final ReplayCommitLogsToSqlAction action = new ReplayCommitLogsToSqlAction();
private final FakeResponse response = new FakeResponse();
@Mock private RequestStatusChecker requestStatusChecker;
@BeforeAll
static void beforeAll() {
VKeyTranslatorFactory.addTestEntityClass(TestObject.class);
}
@BeforeEach
void beforeEach() {
action.gcsService = gcsService;
action.response = response;
action.requestStatusChecker = requestStatusChecker;
action.diffLister = new GcsDiffFileLister();
action.diffLister.gcsService = gcsService;
action.diffLister.gcsBucket = GCS_BUCKET;
action.diffLister.executor = newDirectExecutorService();
RegistryConfig.overrideCloudSqlReplayCommitLogs(true);
}
@Test
void testReplay_multipleDiffFiles() throws Exception {
jpaTm()
.transact(
() -> {
jpaTm().insertWithoutBackup(TestObject.create("previous to keep"));
jpaTm().insertWithoutBackup(TestObject.create("previous to delete"));
});
DateTime now = fakeClock.nowUtc();
// Create 3 transactions, across two diff files.
// Before: {"previous to keep", "previous to delete"}
// 1a: Add {"a", "b"}, Delete {"previous to delete"}
// 1b: Add {"c", "d"}, Delete {"a"}
// 2: Add {"e", "f"}, Delete {"c"}
// After: {"previous to keep", "b", "d", "e", "f"}
Key<CommitLogManifest> manifest1aKey =
CommitLogManifest.createKey(getBucketKey(1), now.minusMinutes(3));
Key<CommitLogManifest> manifest1bKey =
CommitLogManifest.createKey(getBucketKey(2), now.minusMinutes(2));
Key<CommitLogManifest> manifest2Key =
CommitLogManifest.createKey(getBucketKey(1), now.minusMinutes(1));
saveDiffFileNotToRestore(gcsService, now.minusMinutes(2));
saveDiffFile(
gcsService,
createCheckpoint(now.minusMinutes(1)),
CommitLogManifest.create(
getBucketKey(1),
now.minusMinutes(3),
ImmutableSet.of(Key.create(TestObject.create("previous to delete")))),
CommitLogMutation.create(manifest1aKey, TestObject.create("a")),
CommitLogMutation.create(manifest1aKey, TestObject.create("b")),
CommitLogManifest.create(
getBucketKey(2),
now.minusMinutes(2),
ImmutableSet.of(Key.create(TestObject.create("a")))),
CommitLogMutation.create(manifest1bKey, TestObject.create("c")),
CommitLogMutation.create(manifest1bKey, TestObject.create("d")));
saveDiffFile(
gcsService,
createCheckpoint(now),
CommitLogManifest.create(
getBucketKey(1),
now.minusMinutes(1),
ImmutableSet.of(Key.create(TestObject.create("c")))),
CommitLogMutation.create(manifest2Key, TestObject.create("e")),
CommitLogMutation.create(manifest2Key, TestObject.create("f")));
jpaTm().transact(() -> SqlReplayCheckpoint.set(now.minusMinutes(1).minusMillis(1)));
fakeClock.advanceOneMilli();
runAndAssertSuccess(now);
assertExpectedIds("previous to keep", "b", "d", "e", "f");
}
@Test
void testReplay_noManifests() throws Exception {
DateTime now = fakeClock.nowUtc();
jpaTm().transact(() -> jpaTm().insertWithoutBackup(TestObject.create("previous to keep")));
saveDiffFileNotToRestore(gcsService, now.minusMinutes(1));
saveDiffFile(gcsService, createCheckpoint(now.minusMillis(2)));
jpaTm().transact(() -> SqlReplayCheckpoint.set(now.minusMillis(1)));
runAndAssertSuccess(now.minusMillis(1));
assertExpectedIds("previous to keep");
}
@Test
void testReplay_manifestWithNoDeletions() throws Exception {
DateTime now = fakeClock.nowUtc();
jpaTm().transact(() -> jpaTm().insertWithoutBackup(TestObject.create("previous to keep")));
Key<CommitLogBucket> bucketKey = getBucketKey(1);
Key<CommitLogManifest> manifestKey = CommitLogManifest.createKey(bucketKey, now);
saveDiffFileNotToRestore(gcsService, now.minusMinutes(2));
jpaTm().transact(() -> SqlReplayCheckpoint.set(now.minusMinutes(1).minusMillis(1)));
saveDiffFile(
gcsService,
createCheckpoint(now.minusMinutes(1)),
CommitLogManifest.create(bucketKey, now, null),
CommitLogMutation.create(manifestKey, TestObject.create("a")),
CommitLogMutation.create(manifestKey, TestObject.create("b")));
runAndAssertSuccess(now.minusMinutes(1));
assertExpectedIds("previous to keep", "a", "b");
}
@Test
void testReplay_manifestWithNoMutations() throws Exception {
DateTime now = fakeClock.nowUtc();
jpaTm()
.transact(
() -> {
jpaTm().insertWithoutBackup(TestObject.create("previous to keep"));
jpaTm().insertWithoutBackup(TestObject.create("previous to delete"));
});
saveDiffFileNotToRestore(gcsService, now.minusMinutes(2));
jpaTm().transact(() -> SqlReplayCheckpoint.set(now.minusMinutes(1).minusMillis(1)));
saveDiffFile(
gcsService,
createCheckpoint(now.minusMinutes(1)),
CommitLogManifest.create(
getBucketKey(1),
now,
ImmutableSet.of(Key.create(TestObject.create("previous to delete")))));
runAndAssertSuccess(now.minusMinutes(1));
assertExpectedIds("previous to keep");
}
@Test
void wtestReplay_mutateExistingEntity() throws Exception {
DateTime now = fakeClock.nowUtc();
jpaTm().transact(() -> jpaTm().put(TestObject.create("existing", "a")));
Key<CommitLogManifest> manifestKey = CommitLogManifest.createKey(getBucketKey(1), now);
saveDiffFileNotToRestore(gcsService, now.minusMinutes(1).minusMillis(1));
jpaTm().transact(() -> SqlReplayCheckpoint.set(now.minusMinutes(1)));
saveDiffFile(
gcsService,
createCheckpoint(now.minusMillis(1)),
CommitLogManifest.create(getBucketKey(1), now, null),
CommitLogMutation.create(manifestKey, TestObject.create("existing", "b")));
action.run();
TestObject fromDatabase =
jpaTm().transact(() -> jpaTm().load(VKey.createSql(TestObject.class, "existing")));
assertThat(fromDatabase.getField()).isEqualTo("b");
}
// This should be harmless
@Test
void testReplay_deleteMissingEntity() throws Exception {
DateTime now = fakeClock.nowUtc();
jpaTm().transact(() -> jpaTm().put(TestObject.create("previous to keep", "a")));
saveDiffFileNotToRestore(gcsService, now.minusMinutes(1).minusMillis(1));
jpaTm().transact(() -> SqlReplayCheckpoint.set(now.minusMinutes(1)));
saveDiffFile(
gcsService,
createCheckpoint(now.minusMillis(1)),
CommitLogManifest.create(
getBucketKey(1),
now,
ImmutableSet.of(Key.create(TestObject.create("previous to delete")))));
action.run();
assertExpectedIds("previous to keep");
}
@Test
@SuppressWarnings({"unchecked", "rawtypes"})
void testReplay_properlyWeighted() throws Exception {
DateTime now = fakeClock.nowUtc();
Key<CommitLogManifest> manifestKey =
CommitLogManifest.createKey(getBucketKey(1), now.minusMinutes(1));
// Create (but don't save to SQL) a domain + contact
createTld("tld");
DomainBase domain = newDomainBase("example.tld");
CommitLogMutation domainMutation =
tm().transact(() -> CommitLogMutation.create(manifestKey, domain));
ContactResource contact = tm().transact(() -> tm().load(domain.getRegistrant()));
CommitLogMutation contactMutation =
tm().transact(() -> CommitLogMutation.create(manifestKey, contact));
// Create and save to SQL a registrar contact that we will delete
RegistrarContact toDelete = AppEngineExtension.makeRegistrarContact1();
jpaTm().transact(() -> jpaTm().put(toDelete));
jpaTm().transact(() -> SqlReplayCheckpoint.set(now.minusMinutes(1).minusMillis(1)));
// spy the txn manager so we can see what order things were inserted/removed
JpaTransactionManager spy = spy(jpaTm());
TransactionManagerFactory.setJpaTm(() -> spy);
// Save in the commit logs the domain and contact (in that order) and the token deletion
saveDiffFile(
gcsService,
createCheckpoint(now.minusMinutes(1)),
CommitLogManifest.create(
getBucketKey(1), now.minusMinutes(1), ImmutableSet.of(Key.create(toDelete))),
domainMutation,
contactMutation);
runAndAssertSuccess(now.minusMinutes(1));
// Verify two things:
// 1. that the contact insert occurred before the domain insert (necessary for FK ordering)
// even though the domain came first in the file
// 2. that the allocation token delete occurred after the insertions
InOrder inOrder = Mockito.inOrder(spy);
inOrder.verify(spy).put(any(ContactResource.class));
inOrder.verify(spy).put(any(DomainBase.class));
inOrder.verify(spy).delete(toDelete.createVKey());
inOrder.verify(spy).put(any(SqlReplayCheckpoint.class));
}
@Test
@SuppressWarnings({"unchecked", "rawtypes"})
void testReplay_properlyWeighted_doesNotApplyCrossTransactions() throws Exception {
DateTime now = fakeClock.nowUtc();
Key<CommitLogManifest> manifestKey =
CommitLogManifest.createKey(getBucketKey(1), now.minusMinutes(1));
// Create and save the standard contact
ContactResource contact = persistActiveContact("contact1234");
jpaTm().transact(() -> jpaTm().put(contact));
// Simulate a Datastore transaction with a new version of the contact
ContactResource contactWithEdit =
contact.asBuilder().setEmailAddress("replay@example.tld").build();
CommitLogMutation contactMutation =
tm().transact(() -> CommitLogMutation.create(manifestKey, contactWithEdit));
jpaTm().transact(() -> SqlReplayCheckpoint.set(now.minusMinutes(1).minusMillis(1)));
// spy the txn manager so we can see what order things were inserted
JpaTransactionManager spy = spy(jpaTm());
TransactionManagerFactory.setJpaTm(() -> spy);
// Save two commits -- the deletion, then the new version of the contact
saveDiffFile(
gcsService,
createCheckpoint(now.minusMinutes(1).plusMillis(1)),
CommitLogManifest.create(
getBucketKey(1), now.minusMinutes(1), ImmutableSet.of(Key.create(contact))),
CommitLogManifest.create(
getBucketKey(1), now.minusMinutes(1).plusMillis(1), ImmutableSet.of()),
contactMutation);
runAndAssertSuccess(now.minusMinutes(1).plusMillis(1));
// Verify that the delete occurred first (because it was in the first transaction) even though
// deletes have higher weight
ArgumentCaptor<Object> putCaptor = ArgumentCaptor.forClass(Object.class);
InOrder inOrder = Mockito.inOrder(spy);
inOrder.verify(spy).delete(contact.createVKey());
inOrder.verify(spy).put(putCaptor.capture());
assertThat(putCaptor.getValue().getClass()).isEqualTo(ContactResource.class);
assertThat(jpaTm().transact(() -> jpaTm().load(contact.createVKey()).getEmailAddress()))
.isEqualTo("replay@example.tld");
}
@Test
void testSuccess_nonReplicatedEntity_isNotReplayed() {
DateTime now = fakeClock.nowUtc();
// spy the txn manager so we can verify it's never called
JpaTransactionManager spy = spy(jpaTm());
TransactionManagerFactory.setJpaTm(() -> spy);
jpaTm().transact(() -> SqlReplayCheckpoint.set(now.minusMinutes(1).minusMillis(1)));
Key<CommitLogManifest> manifestKey =
CommitLogManifest.createKey(getBucketKey(1), now.minusMinutes(1));
// Have a commit log with a couple objects that shouldn't be replayed
ReservedList reservedList =
new ReservedList.Builder().setReservedListMap(ImmutableMap.of()).setName("name").build();
Cursor cursor = Cursor.createGlobal(CursorType.RECURRING_BILLING, now.minusHours(1));
tm().transact(
() -> {
try {
saveDiffFile(
gcsService,
createCheckpoint(now.minusMinutes(1)),
CommitLogManifest.create(
getBucketKey(1), now.minusMinutes(1), ImmutableSet.of()),
// Reserved list is dually-written non-replicated
CommitLogMutation.create(manifestKey, reservedList),
// Cursors aren't replayed to SQL at all
CommitLogMutation.create(manifestKey, cursor));
} catch (IOException e) {
throw new RuntimeException(e);
}
});
runAndAssertSuccess(now.minusMinutes(1));
// jpaTm()::put should only have been called with the checkpoint
verify(spy, times(2)).put(any(SqlReplayCheckpoint.class));
verify(spy, times(2)).put(any());
}
@Test
void testSuccess_nonReplicatedEntity_isNotDeleted() throws Exception {
DateTime now = fakeClock.nowUtc();
// spy the txn manager so we can verify it's never called
JpaTransactionManager spy = spy(jpaTm());
TransactionManagerFactory.setJpaTm(() -> spy);
jpaTm().transact(() -> SqlReplayCheckpoint.set(now.minusMinutes(1).minusMillis(1)));
// Save a couple deletes that aren't propagated to SQL (the objects deleted are irrelevant)
Key<ClaimsListShard> claimsListKey = Key.create(ClaimsListShard.class, 1L);
saveDiffFile(
gcsService,
createCheckpoint(now.minusMinutes(1)),
CommitLogManifest.create(
getBucketKey(1),
now.minusMinutes(1),
// one object only exists in Datastore, one is dually-written (so isn't replicated)
ImmutableSet.of(getCrossTldKey(), claimsListKey)));
runAndAssertSuccess(now.minusMinutes(1));
verify(spy, times(0)).delete(any(VKey.class));
}
@Test
void testFailure_notEnabled() {
RegistryConfig.overrideCloudSqlReplayCommitLogs(false);
action.run();
assertThat(response.getStatus()).isEqualTo(SC_NO_CONTENT);
assertThat(response.getPayload())
.isEqualTo("ReplayCommitLogsToSqlAction was called but disabled in the config.");
}
@Test
void testFailure_cannotAcquireLock() {
Truth8.assertThat(
Lock.acquire(
ReplayCommitLogsToSqlAction.class.getSimpleName(),
null,
Duration.standardHours(1),
requestStatusChecker,
false))
.isPresent();
fakeClock.advanceOneMilli();
action.run();
assertThat(response.getStatus()).isEqualTo(SC_NO_CONTENT);
assertThat(response.getPayload())
.isEqualTo("Can't acquire SQL commit log replay lock, aborting.");
}
private void runAndAssertSuccess(DateTime expectedCheckpointTime) {
action.run();
assertThat(response.getStatus()).isEqualTo(SC_OK);
assertThat(jpaTm().transact(SqlReplayCheckpoint::get)).isEqualTo(expectedCheckpointTime);
}
private void assertExpectedIds(String... expectedIds) {
ImmutableList<String> actualIds =
jpaTm()
.transact(
() ->
jpaTm().loadAll(TestObject.class).stream()
.map(TestObject::getId)
.collect(toImmutableList()));
assertThat(actualIds).containsExactlyElementsIn(expectedIds);
}
}

View File

@@ -61,7 +61,7 @@ import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link RestoreCommitLogsAction}. */
public class RestoreCommitLogsActionTest {
private static final String GCS_BUCKET = "gcs bucket";
static final String GCS_BUCKET = "gcs bucket";
private final DateTime now = DateTime.now(UTC);
private final RestoreCommitLogsAction action = new RestoreCommitLogsAction();
@@ -89,9 +89,10 @@ public class RestoreCommitLogsActionTest {
@Test
void testRestore_multipleDiffFiles() throws Exception {
ofy().saveWithoutBackup().entities(
TestObject.create("previous to keep"),
TestObject.create("previous to delete")).now();
ofy()
.saveWithoutBackup()
.entities(TestObject.create("previous to keep"), TestObject.create("previous to delete"))
.now();
// Create 3 transactions, across two diff files.
// Before: {"previous to keep", "previous to delete"}
// 1a: Add {"a", "b"}, Delete {"previous to delete"}
@@ -104,29 +105,33 @@ public class RestoreCommitLogsActionTest {
CommitLogManifest.createKey(getBucketKey(2), now.minusMinutes(2));
Key<CommitLogManifest> manifest2Key =
CommitLogManifest.createKey(getBucketKey(1), now.minusMinutes(1));
saveDiffFileNotToRestore(now.minusMinutes(2));
Iterable<ImmutableObject> file1CommitLogs = saveDiffFile(
createCheckpoint(now.minusMinutes(1)),
CommitLogManifest.create(
getBucketKey(1),
now.minusMinutes(3),
ImmutableSet.of(Key.create(TestObject.create("previous to delete")))),
CommitLogMutation.create(manifest1aKey, TestObject.create("a")),
CommitLogMutation.create(manifest1aKey, TestObject.create("b")),
CommitLogManifest.create(
getBucketKey(2),
now.minusMinutes(2),
ImmutableSet.of(Key.create(TestObject.create("a")))),
CommitLogMutation.create(manifest1bKey, TestObject.create("c")),
CommitLogMutation.create(manifest1bKey, TestObject.create("d")));
Iterable<ImmutableObject> file2CommitLogs = saveDiffFile(
createCheckpoint(now),
CommitLogManifest.create(
getBucketKey(1),
now.minusMinutes(1),
ImmutableSet.of(Key.create(TestObject.create("c")))),
CommitLogMutation.create(manifest2Key, TestObject.create("e")),
CommitLogMutation.create(manifest2Key, TestObject.create("f")));
saveDiffFileNotToRestore(gcsService, now.minusMinutes(2));
Iterable<ImmutableObject> file1CommitLogs =
saveDiffFile(
gcsService,
createCheckpoint(now.minusMinutes(1)),
CommitLogManifest.create(
getBucketKey(1),
now.minusMinutes(3),
ImmutableSet.of(Key.create(TestObject.create("previous to delete")))),
CommitLogMutation.create(manifest1aKey, TestObject.create("a")),
CommitLogMutation.create(manifest1aKey, TestObject.create("b")),
CommitLogManifest.create(
getBucketKey(2),
now.minusMinutes(2),
ImmutableSet.of(Key.create(TestObject.create("a")))),
CommitLogMutation.create(manifest1bKey, TestObject.create("c")),
CommitLogMutation.create(manifest1bKey, TestObject.create("d")));
Iterable<ImmutableObject> file2CommitLogs =
saveDiffFile(
gcsService,
createCheckpoint(now),
CommitLogManifest.create(
getBucketKey(1),
now.minusMinutes(1),
ImmutableSet.of(Key.create(TestObject.create("c")))),
CommitLogMutation.create(manifest2Key, TestObject.create("e")),
CommitLogMutation.create(manifest2Key, TestObject.create("f")));
action.fromTime = now.minusMinutes(1).minusMillis(1);
action.run();
ofy().clearSessionCache();
@@ -139,10 +144,9 @@ public class RestoreCommitLogsActionTest {
@Test
void testRestore_noManifests() throws Exception {
ofy().saveWithoutBackup().entity(
TestObject.create("previous to keep")).now();
saveDiffFileNotToRestore(now.minusMinutes(1));
Iterable<ImmutableObject> commitLogs = saveDiffFile(createCheckpoint(now));
ofy().saveWithoutBackup().entity(TestObject.create("previous to keep")).now();
saveDiffFileNotToRestore(gcsService, now.minusMinutes(1));
Iterable<ImmutableObject> commitLogs = saveDiffFile(gcsService, createCheckpoint(now));
action.run();
ofy().clearSessionCache();
assertExpectedIds("previous to keep");
@@ -156,32 +160,37 @@ public class RestoreCommitLogsActionTest {
ofy().saveWithoutBackup().entity(TestObject.create("previous to keep")).now();
Key<CommitLogBucket> bucketKey = getBucketKey(1);
Key<CommitLogManifest> manifestKey = CommitLogManifest.createKey(bucketKey, now);
saveDiffFileNotToRestore(now.minusMinutes(1));
Iterable<ImmutableObject> commitLogs = saveDiffFile(
createCheckpoint(now),
CommitLogManifest.create(bucketKey, now, null),
CommitLogMutation.create(manifestKey, TestObject.create("a")),
CommitLogMutation.create(manifestKey, TestObject.create("b")));
saveDiffFileNotToRestore(gcsService, now.minusMinutes(1));
Iterable<ImmutableObject> commitLogs =
saveDiffFile(
gcsService,
createCheckpoint(now),
CommitLogManifest.create(bucketKey, now, null),
CommitLogMutation.create(manifestKey, TestObject.create("a")),
CommitLogMutation.create(manifestKey, TestObject.create("b")));
action.run();
ofy().clearSessionCache();
assertExpectedIds("previous to keep", "a", "b");
assertInDatastore(commitLogs);
assertInDatastore(CommitLogCheckpointRoot.create(now));
assertCommitLogBuckets(ImmutableMap.of(1, now));
}
}
@Test
void testRestore_manifestWithNoMutations() throws Exception {
ofy().saveWithoutBackup().entities(
TestObject.create("previous to keep"),
TestObject.create("previous to delete")).now();
saveDiffFileNotToRestore(now.minusMinutes(1));
Iterable<ImmutableObject> commitLogs = saveDiffFile(
createCheckpoint(now),
CommitLogManifest.create(
getBucketKey(1),
now,
ImmutableSet.of(Key.create(TestObject.create("previous to delete")))));
ofy()
.saveWithoutBackup()
.entities(TestObject.create("previous to keep"), TestObject.create("previous to delete"))
.now();
saveDiffFileNotToRestore(gcsService, now.minusMinutes(1));
Iterable<ImmutableObject> commitLogs =
saveDiffFile(
gcsService,
createCheckpoint(now),
CommitLogManifest.create(
getBucketKey(1),
now,
ImmutableSet.of(Key.create(TestObject.create("previous to delete")))));
action.run();
ofy().clearSessionCache();
assertExpectedIds("previous to keep");
@@ -193,12 +202,13 @@ public class RestoreCommitLogsActionTest {
// This is a pathological case that shouldn't be possible, but we should be robust to it.
@Test
void testRestore_manifestWithNoMutationsOrDeletions() throws Exception {
ofy().saveWithoutBackup().entities(
TestObject.create("previous to keep")).now();
saveDiffFileNotToRestore(now.minusMinutes(1));
Iterable<ImmutableObject> commitLogs = saveDiffFile(
createCheckpoint(now),
CommitLogManifest.create(getBucketKey(1), now, null));
ofy().saveWithoutBackup().entities(TestObject.create("previous to keep")).now();
saveDiffFileNotToRestore(gcsService, now.minusMinutes(1));
Iterable<ImmutableObject> commitLogs =
saveDiffFile(
gcsService,
createCheckpoint(now),
CommitLogManifest.create(getBucketKey(1), now, null));
action.run();
ofy().clearSessionCache();
assertExpectedIds("previous to keep");
@@ -211,11 +221,13 @@ public class RestoreCommitLogsActionTest {
void testRestore_mutateExistingEntity() throws Exception {
ofy().saveWithoutBackup().entity(TestObject.create("existing", "a")).now();
Key<CommitLogManifest> manifestKey = CommitLogManifest.createKey(getBucketKey(1), now);
saveDiffFileNotToRestore(now.minusMinutes(1));
Iterable<ImmutableObject> commitLogs = saveDiffFile(
createCheckpoint(now),
CommitLogManifest.create(getBucketKey(1), now, null),
CommitLogMutation.create(manifestKey, TestObject.create("existing", "b")));
saveDiffFileNotToRestore(gcsService, now.minusMinutes(1));
Iterable<ImmutableObject> commitLogs =
saveDiffFile(
gcsService,
createCheckpoint(now),
CommitLogManifest.create(getBucketKey(1), now, null),
CommitLogMutation.create(manifestKey, TestObject.create("existing", "b")));
action.run();
ofy().clearSessionCache();
assertThat(ofy().load().entity(TestObject.create("existing")).now().getField()).isEqualTo("b");
@@ -228,13 +240,15 @@ public class RestoreCommitLogsActionTest {
@Test
void testRestore_deleteMissingEntity() throws Exception {
ofy().saveWithoutBackup().entity(TestObject.create("previous to keep", "a")).now();
saveDiffFileNotToRestore(now.minusMinutes(1));
Iterable<ImmutableObject> commitLogs = saveDiffFile(
createCheckpoint(now),
CommitLogManifest.create(
getBucketKey(1),
now,
ImmutableSet.of(Key.create(TestObject.create("previous to delete")))));
saveDiffFileNotToRestore(gcsService, now.minusMinutes(1));
Iterable<ImmutableObject> commitLogs =
saveDiffFile(
gcsService,
createCheckpoint(now),
CommitLogManifest.create(
getBucketKey(1),
now,
ImmutableSet.of(Key.create(TestObject.create("previous to delete")))));
action.run();
ofy().clearSessionCache();
assertExpectedIds("previous to keep");
@@ -243,12 +257,13 @@ public class RestoreCommitLogsActionTest {
assertInDatastore(CommitLogCheckpointRoot.create(now));
}
private CommitLogCheckpoint createCheckpoint(DateTime now) {
static CommitLogCheckpoint createCheckpoint(DateTime now) {
return CommitLogCheckpoint.create(now, toMap(getBucketIds(), x -> now));
}
private Iterable<ImmutableObject> saveDiffFile(
CommitLogCheckpoint checkpoint, ImmutableObject... entities) throws IOException {
static Iterable<ImmutableObject> saveDiffFile(
GcsService gcsService, CommitLogCheckpoint checkpoint, ImmutableObject... entities)
throws IOException {
DateTime now = checkpoint.getCheckpointTime();
List<ImmutableObject> allEntities = Lists.asList(checkpoint, entities);
ByteArrayOutputStream output = new ByteArrayOutputStream();
@@ -264,8 +279,9 @@ public class RestoreCommitLogsActionTest {
return allEntities;
}
private void saveDiffFileNotToRestore(DateTime now) throws Exception {
static void saveDiffFileNotToRestore(GcsService gcsService, DateTime now) throws Exception {
saveDiffFile(
gcsService,
createCheckpoint(now),
CommitLogManifest.create(getBucketKey(1), now, null),
CommitLogMutation.create(

View File

@@ -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));
}
/**

View File

@@ -501,7 +501,7 @@ class EppLifecycleDomainTest extends EppTestCase {
@Test
void testEapDomainDeletion_withinAddGracePeriod_eapFeeIsNotRefunded() throws Exception {
assertThatCommand("login_valid_fee_extension.xml").hasResponse("generic_success_response.xml");
assertThatCommand("login_valid_fee_extension.xml").hasSuccessfulLogin();
createContacts(DateTime.parse("2000-06-01T00:00:00Z"));
// Set the EAP schedule.
@@ -718,7 +718,7 @@ class EppLifecycleDomainTest extends EppTestCase {
START_OF_TIME, PREDELEGATION,
gaDate, GENERAL_AVAILABILITY));
assertThatCommand("login_valid_fee_extension.xml").hasResponse("generic_success_response.xml");
assertThatCommand("login_valid_fee_extension.xml").hasSuccessfulLogin();
assertThatCommand("domain_check_fee_premium.xml")
.atTime(gaDate.plusDays(1))
@@ -1196,7 +1196,7 @@ class EppLifecycleDomainTest extends EppTestCase {
assertThatLogin("NewRegistrar", "foo-BAR2")
.atTime(sunriseDate.minusDays(3))
.hasResponse("generic_success_response.xml");
.hasSuccessfulLogin();
createContactsAndHosts();
@@ -1292,7 +1292,7 @@ class EppLifecycleDomainTest extends EppTestCase {
assertThatLogin("NewRegistrar", "foo-BAR2")
.atTime(sunriseDate.minusDays(3))
.hasResponse("generic_success_response.xml");
.hasSuccessfulLogin();
createContactsAndHosts();

View File

@@ -44,13 +44,13 @@ class EppLoginTlsTest extends EppTestCase {
persistResource(
loadRegistrar("NewRegistrar")
.asBuilder()
.setClientCertificateHash(CertificateSamples.SAMPLE_CERT_HASH)
.setClientCertificate(CertificateSamples.SAMPLE_CERT, DateTime.now(UTC))
.build());
// Set a cert for the second registrar, or else any cert will be allowed for login.
persistResource(
loadRegistrar("TheRegistrar")
.asBuilder()
.setClientCertificateHash(CertificateSamples.SAMPLE_CERT2_HASH)
.setClientCertificate(CertificateSamples.SAMPLE_CERT2, DateTime.now(UTC))
.build());
}

View File

@@ -121,6 +121,10 @@ public class EppTestCase {
return assertCommandAndResponse(
inputFilename, inputSubstitutions, outputFilename, outputSubstitutions, now);
}
public String hasSuccessfulLogin() throws Exception {
return assertLoginCommandAndResponse(inputFilename, inputSubstitutions, null, now);
}
}
protected CommandAsserter assertThatCommand(String inputFilename) {
@@ -137,13 +141,33 @@ public class EppTestCase {
}
protected void assertThatLoginSucceeds(String clientId, String password) throws Exception {
assertThatLogin(clientId, password).hasResponse("generic_success_response.xml");
assertThatLogin(clientId, password).hasSuccessfulLogin();
}
protected void assertThatLogoutSucceeds() throws Exception {
assertThatCommand("logout.xml").hasResponse("logout_response.xml");
}
private String assertLoginCommandAndResponse(
String inputFilename,
@Nullable Map<String, String> inputSubstitutions,
@Nullable Map<String, String> outputSubstitutions,
DateTime now)
throws Exception {
String outputFilename = "generic_success_response.xml";
clock.setTo(now);
String input = loadFile(EppTestCase.class, inputFilename, inputSubstitutions);
String expectedOutput = loadFile(EppTestCase.class, outputFilename, outputSubstitutions);
setUpSession();
FakeResponse response = executeXmlCommand(input);
// Check that the logged-in header was added to the response
assertThat(response.getHeaders()).isEqualTo(ImmutableMap.of("Logged-In", "true"));
return verifyAndReturnOutput(
response.getPayload(), expectedOutput, inputFilename, outputFilename);
}
private String assertCommandAndResponse(
String inputFilename,
@Nullable Map<String, String> inputSubstitutions,
@@ -154,6 +178,18 @@ public class EppTestCase {
clock.setTo(now);
String input = loadFile(EppTestCase.class, inputFilename, inputSubstitutions);
String expectedOutput = loadFile(EppTestCase.class, outputFilename, outputSubstitutions);
setUpSession();
FakeResponse response = executeXmlCommand(input);
// Checks that the Logged-In header is not in the response. If testing the login command, use
// assertLoginCommandAndResponse instead of this method.
assertThat(response.getHeaders()).doesNotContainEntry("Logged-In", "true");
return verifyAndReturnOutput(
response.getPayload(), expectedOutput, inputFilename, outputFilename);
}
private void setUpSession() {
if (sessionMetadata == null) {
sessionMetadata =
new HttpSessionMetadata(new FakeHttpSession()) {
@@ -165,7 +201,13 @@ public class EppTestCase {
}
};
}
String actualOutput = executeXmlCommand(input);
}
private String verifyAndReturnOutput(
String actualOutput, String expectedOutput, String inputFilename, String outputFilename)
throws Exception {
// Run the resulting xml through the unmarshaller to verify that it was valid.
EppXmlTransformer.validateOutput(actualOutput);
assertXmlEqualsWithMessage(
expectedOutput,
actualOutput,
@@ -176,7 +218,7 @@ public class EppTestCase {
return actualOutput;
}
private String executeXmlCommand(String inputXml) throws Exception {
private FakeResponse executeXmlCommand(String inputXml) throws Exception {
EppRequestHandler handler = new EppRequestHandler();
FakeResponse response = new FakeResponse();
handler.response = response;
@@ -195,10 +237,7 @@ public class EppTestCase {
inputXml.getBytes(UTF_8));
assertThat(response.getStatus()).isEqualTo(SC_OK);
assertThat(response.getContentType()).isEqualTo(APPLICATION_EPP_XML_UTF8);
String result = response.getPayload();
// Run the resulting xml through the unmarshaller to verify that it was valid.
EppXmlTransformer.validateOutput(result);
return result;
return response;
}
EppMetric getRecordedEppMetric() {

View File

@@ -26,6 +26,7 @@ import static google.registry.testing.DatabaseHelper.newDomainBase;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistDeletedDomain;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.DatabaseHelper.persistResources;
import static google.registry.testing.DomainBaseSubject.assertAboutDomains;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static google.registry.testing.HistoryEntrySubject.assertAboutHistoryEntries;
@@ -69,12 +70,17 @@ import google.registry.model.registry.Registry;
import google.registry.model.reporting.DomainTransactionRecord;
import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField;
import google.registry.model.reporting.HistoryEntry;
import google.registry.testing.ReplayExtension;
import java.util.Map;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link DomainRenewFlow}. */
class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, DomainBase> {
@@ -96,50 +102,66 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, DomainBa
private final DateTime expirationTime = DateTime.parse("2000-04-03T22:00:00.0Z");
@Order(value = Order.DEFAULT - 3)
@RegisterExtension
final SetClockExtension setClockExtension = new SetClockExtension();
@Order(value = Order.DEFAULT - 2)
@RegisterExtension
final ReplayExtension replayExtension = new ReplayExtension(clock);
@BeforeEach
void initDomainTest() {
createTld("tld");
clock.setTo(expirationTime.minusMillis(2));
setEppInput("domain_renew.xml", ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "5"));
}
private void persistDomain(StatusValue... statusValues) throws Exception {
DomainBase domain = newDomainBase(getUniqueIdFromCommand());
HistoryEntry historyEntryDomainCreate =
persistResource(
new HistoryEntry.Builder()
.setParent(domain)
.setType(HistoryEntry.Type.DOMAIN_CREATE)
.build());
BillingEvent.Recurring autorenewEvent =
persistResource(
new BillingEvent.Recurring.Builder()
.setReason(Reason.RENEW)
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
.setTargetId(getUniqueIdFromCommand())
.setClientId("TheRegistrar")
.setEventTime(expirationTime)
.setRecurrenceEndTime(END_OF_TIME)
.setParent(historyEntryDomainCreate)
.build());
PollMessage.Autorenew autorenewPollMessage =
persistResource(
new PollMessage.Autorenew.Builder()
.setTargetId(getUniqueIdFromCommand())
.setClientId("TheRegistrar")
.setEventTime(expirationTime)
.setAutorenewEndTime(END_OF_TIME)
.setMsg("Domain was auto-renewed.")
.setParent(historyEntryDomainCreate)
.build());
persistResource(
domain
.asBuilder()
.setRegistrationExpirationTime(expirationTime)
.setStatusValues(ImmutableSet.copyOf(statusValues))
.setAutorenewBillingEvent(autorenewEvent.createVKey())
.setAutorenewPollMessage(autorenewPollMessage.createVKey())
.build());
tm().transact(
() -> {
try {
HistoryEntry historyEntryDomainCreate =
new HistoryEntry.Builder()
.setParent(domain)
.setType(HistoryEntry.Type.DOMAIN_CREATE)
.setModificationTime(clock.nowUtc())
.build();
BillingEvent.Recurring autorenewEvent =
new BillingEvent.Recurring.Builder()
.setReason(Reason.RENEW)
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
.setTargetId(getUniqueIdFromCommand())
.setClientId("TheRegistrar")
.setEventTime(expirationTime)
.setRecurrenceEndTime(END_OF_TIME)
.setParent(historyEntryDomainCreate)
.build();
PollMessage.Autorenew autorenewPollMessage =
new PollMessage.Autorenew.Builder()
.setTargetId(getUniqueIdFromCommand())
.setClientId("TheRegistrar")
.setEventTime(expirationTime)
.setAutorenewEndTime(END_OF_TIME)
.setMsg("Domain was auto-renewed.")
.setParent(historyEntryDomainCreate)
.build();
DomainBase newDomain =
domain
.asBuilder()
.setRegistrationExpirationTime(expirationTime)
.setStatusValues(ImmutableSet.copyOf(statusValues))
.setAutorenewBillingEvent(autorenewEvent.createVKey())
.setAutorenewPollMessage(autorenewPollMessage.createVKey())
.build();
persistResources(
ImmutableSet.of(
historyEntryDomainCreate, autorenewEvent,
autorenewPollMessage, newDomain));
} catch (Exception e) {
throw new RuntimeException("Error persisting domain", e);
}
});
clock.advanceOneMilli();
}
@@ -255,6 +277,7 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, DomainBa
@Test
void testSuccess() throws Exception {
clock.advanceOneMilli();
persistDomain();
doSuccessfulTest(
"domain_renew_response.xml",
@@ -789,4 +812,21 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, DomainBa
TransactionReportField.netRenewsFieldFromYears(5),
1));
}
/**
* Local extension so we can set the clock correctly prior to using it to define basic entities in
* AppEngineExtension.
*
* <p>This has to happen first, we'll get timestamp inversions if we try to set the clock after
* these objects are created.
*/
class SetClockExtension implements BeforeEachCallback {
@Override
public void beforeEach(ExtensionContext context) {
// we have to go far enough back before the expiration time so that the clock advances we do
// after each persist don't move us into a grace period. The current value is likely beyond
// what is necessary, but this doesn't hurt.
clock.setTo(expirationTime.minusMillis(20));
}
}
}

View File

@@ -90,11 +90,14 @@ import google.registry.model.host.HostResource;
import google.registry.model.registry.Registry;
import google.registry.model.reporting.HistoryEntry;
import google.registry.persistence.VKey;
import google.registry.testing.ReplayExtension;
import java.util.Optional;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link DomainUpdateFlow}. */
class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, DomainBase> {
@@ -112,6 +115,10 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
private ContactResource mak21Contact;
private ContactResource unusedContact;
@Order(value = Order.DEFAULT - 2)
@RegisterExtension
final ReplayExtension replayExtension = new ReplayExtension(clock);
@BeforeEach
void initDomainTest() {
createTld("tld");
@@ -146,6 +153,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
persistResource(
new HistoryEntry.Builder()
.setType(HistoryEntry.Type.DOMAIN_CREATE)
.setModificationTime(clock.nowUtc())
.setParent(domain)
.build());
clock.advanceOneMilli();
@@ -168,6 +176,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
persistResource(
new HistoryEntry.Builder()
.setType(HistoryEntry.Type.DOMAIN_CREATE)
.setModificationTime(clock.nowUtc())
.setParent(domain)
.build());
clock.advanceOneMilli();

View File

@@ -14,6 +14,7 @@
package google.registry.flows.session;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.DatabaseHelper.deleteResource;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.persistResource;
@@ -32,6 +33,7 @@ import google.registry.flows.session.LoginFlow.PasswordChangesNotSupportedExcept
import google.registry.flows.session.LoginFlow.RegistrarAccountNotActiveException;
import google.registry.flows.session.LoginFlow.TooManyFailedLoginsException;
import google.registry.flows.session.LoginFlow.UnsupportedLanguageException;
import google.registry.model.eppoutput.EppOutput;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.Registrar.State;
import org.junit.jupiter.api.BeforeEach;
@@ -74,6 +76,14 @@ public abstract class LoginFlowTestCase extends FlowTestCase<LoginFlow> {
doSuccessfulTest("login_valid.xml");
}
@Test
void testSuccess_setsIsLoginResponse() throws Exception {
setEppInput("login_valid.xml");
assertTransactionalFlow(false);
EppOutput output = runFlow();
assertThat(output.getResponse().isLoginResponse()).isTrue();
}
@Test
void testSuccess_suspendedRegistrar() throws Exception {
persistResource(getRegistrarBuilder().setState(State.SUSPENDED).build());

View File

@@ -15,6 +15,7 @@
package google.registry.flows.session;
import static google.registry.testing.DatabaseHelper.persistResource;
import static org.joda.time.DateTimeZone.UTC;
import com.google.common.collect.ImmutableList;
import com.google.common.net.InetAddresses;
@@ -26,6 +27,7 @@ import google.registry.model.registrar.Registrar;
import google.registry.testing.CertificateSamples;
import google.registry.util.CidrAddressBlock;
import java.util.Optional;
import org.joda.time.DateTime;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link LoginFlow} when accessed via a TLS transport. */
@@ -41,7 +43,7 @@ public class LoginFlowViaTlsTest extends LoginFlowTestCase {
@Override
protected Registrar.Builder getRegistrarBuilder() {
return super.getRegistrarBuilder()
.setClientCertificateHash(GOOD_CERT)
.setClientCertificate(CertificateSamples.SAMPLE_CERT, DateTime.now(UTC))
.setIpAddressAllowList(
ImmutableList.of(CidrAddressBlock.create(InetAddresses.forString(GOOD_IP.get()), 32)));
}

View File

@@ -156,16 +156,6 @@ public final class OteAccountBuilderTest {
.isTrue();
}
@Test
void testCreateOteEntities_setCertificateHash() {
OteAccountBuilder.forClientId("myclientid")
.setCertificateHash(SAMPLE_CERT_HASH)
.buildAndPersist();
assertThat(Registrar.loadByClientId("myclientid-3").get().getClientCertificateHash())
.isEqualTo(SAMPLE_CERT_HASH);
}
@Test
void testCreateOteEntities_setCertificate() {
OteAccountBuilder.forClientId("myclientid")

View File

@@ -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);
}
}

View File

@@ -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()
@@ -696,6 +696,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));

View File

@@ -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());
});
}

View File

@@ -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.

View File

@@ -18,19 +18,20 @@ import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.ImmutableMap;
import com.googlecode.objectify.Key;
import google.registry.model.ofy.TransactionInfo.Delete;
import google.registry.model.registrar.Registrar;
import google.registry.model.reporting.HistoryEntry;
import google.registry.testing.AppEngineExtension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
class TransactionInfoTest {
class EntityWritePrioritiesTest {
@RegisterExtension
AppEngineExtension appEngine = new AppEngineExtension.Builder().withDatastore().build();
@Test
void testGetWeight() {
void testGetPriority() {
// just verify that the lowest is what we expect for both save and delete and verify that the
// Registrar class is zero.
ImmutableMap<Key<?>, Object> actions =
@@ -39,10 +40,12 @@ class TransactionInfoTest {
Key.create(HistoryEntry.class, 200), "fake history entry",
Key.create(Registrar.class, 300), "fake registrar");
ImmutableMap<Long, Integer> expectedValues =
ImmutableMap.of(100L, TransactionInfo.DELETE_RANGE + 1, 200L, -1, 300L, 0);
ImmutableMap.of(100L, EntityWritePriorities.DELETE_RANGE + 10, 200L, -10, 300L, 0);
for (ImmutableMap.Entry<Key<?>, Object> entry : actions.entrySet()) {
assertThat(TransactionInfo.getWeight(entry))
assertThat(
EntityWritePriorities.getEntityPriority(
entry.getKey().getKind(), Delete.SENTINEL.equals(entry.getValue())))
.isEqualTo(expectedValues.get(entry.getKey().getId()));
}
}

View File

@@ -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";
}
}

View File

@@ -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";
}
}

View File

@@ -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();
}
}
}

View File

@@ -18,12 +18,17 @@ import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
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 +48,50 @@ 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() {
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(
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 +99,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 +126,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 +201,6 @@ public class SecretManagerClientTest {
@Test
void deleteSecret() {
String secretId = nextSecretId();
secretManagerClient.createSecret(secretId);
assertThat(secretManagerClient.listSecrets()).contains(secretId);
secretManagerClient.deleteSecret(secretId);

View File

@@ -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();
}
}

View File

@@ -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());
}

View File

@@ -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(
() ->

View File

@@ -24,16 +24,19 @@ import google.registry.model.ImmutableObject;
import google.registry.model.annotations.VirtualEntity;
import google.registry.model.common.EntityGroupRoot;
import google.registry.persistence.VKey;
import google.registry.schema.replay.DatastoreAndSqlEntity;
import google.registry.schema.replay.EntityTest.EntityForTesting;
import javax.persistence.Transient;
/** A test model object that can be persisted in any entity group. */
@Entity
@javax.persistence.Entity
@EntityForTesting
public class TestObject extends ImmutableObject {
public class TestObject extends ImmutableObject implements DatastoreAndSqlEntity {
@Parent Key<EntityGroupRoot> parent;
@Parent @Transient Key<EntityGroupRoot> parent;
@Id String id;
@Id @javax.persistence.Id String id;
String field;

View File

@@ -21,7 +21,6 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
import static google.registry.testing.CertificateSamples.SAMPLE_CERT;
import static google.registry.testing.CertificateSamples.SAMPLE_CERT3;
import static google.registry.testing.CertificateSamples.SAMPLE_CERT3_HASH;
import static google.registry.testing.CertificateSamples.SAMPLE_CERT_HASH;
import static google.registry.testing.DatabaseHelper.createTlds;
import static google.registry.testing.DatabaseHelper.persistNewRegistrar;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
@@ -447,29 +446,6 @@ class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarCommand>
assertThat(registrar).isEmpty();
}
@Test
void testSuccess_clientCertHashFlag() throws Exception {
runCommandForced(
"--name=blobio",
"--password=some_password",
"--registrar_type=REAL",
"--iana_id=8",
"--cert_hash=" + SAMPLE_CERT_HASH,
"--passcode=01234",
"--icann_referral_email=foo@bar.test",
"--street=\"123 Fake St\"",
"--city Fakington",
"--state MA",
"--zip 00351",
"--cc US",
"clientz");
Optional<Registrar> registrar = Registrar.loadByClientId("clientz");
assertThat(registrar).isPresent();
assertThat(registrar.get().getClientCertificate()).isNull();
assertThat(registrar.get().getClientCertificateHash()).isEqualTo(SAMPLE_CERT_HASH);
}
@Test
void testSuccess_failoverClientCertFileFlag() throws Exception {
fakeClock.setTo(DateTime.parse("2020-11-01T00:00:00Z"));
@@ -1182,74 +1158,6 @@ class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarCommand>
"clientz"));
}
@Test
void testFailure_certHashAndCertFile() {
assertThrows(
IllegalArgumentException.class,
() ->
runCommandForced(
"--name=blobio",
"--password=some_password",
"--registrar_type=REAL",
"--iana_id=8",
"--cert_file=" + getCertFilename(),
"--cert_hash=ABCDEF",
"--passcode=01234",
"--icann_referral_email=foo@bar.test",
"--street=\"123 Fake St\"",
"--city Fakington",
"--state MA",
"--zip 00351",
"--cc US",
"clientz"));
}
@Test
void testFailure_certHashNotBase64() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
runCommandForced(
"--name=blobio",
"--password=some_password",
"--registrar_type=REAL",
"--iana_id=8",
"--cert_hash=!",
"--passcode=01234",
"--icann_referral_email=foo@bar.test",
"--street=\"123 Fake St\"",
"--city Fakington",
"--state MA",
"--zip 00351",
"--cc US",
"clientz"));
assertThat(thrown).hasMessageThat().contains("base64");
}
@Test
void testFailure_certHashNotA256BitValue() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
runCommandForced(
"--name=blobio",
"--password=some_password",
"--registrar_type=REAL",
"--iana_id=8",
"--cert_hash=abc",
"--passcode=01234",
"--icann_referral_email=foo@bar.test",
"--street=\"123 Fake St\"",
"--city Fakington",
"--state MA",
"--zip 00351",
"--cc US",
"clientz"));
assertThat(thrown).hasMessageThat().contains("256");
}
@Test
void testFailure_missingName() {
IllegalArgumentException thrown =

View File

@@ -0,0 +1,122 @@
// 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.truth.Truth.assertThat;
import static google.registry.model.ImmutableObjectSubject.immutableObjectCorrespondence;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.newDomainBase;
import static google.registry.testing.DatabaseHelper.persistResource;
import com.google.common.collect.ImmutableList;
import com.google.common.truth.Correspondence;
import com.googlecode.objectify.Key;
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.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry;
import google.registry.persistence.VKey;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Unit test for {@link RemoveRegistryOneKeyCommand}. */
public class RemoveRegistryOneKeyCommandTest extends CommandTestCase<RemoveRegistryOneKeyCommand> {
DomainBase domain;
HistoryEntry historyEntry;
@BeforeEach
void beforeEach() {
createTld("foobar");
domain =
newDomainBase("foo.foobar")
.asBuilder()
.setDeletionTime(DateTime.parse("2016-01-01T00:00:00Z"))
.setAutorenewBillingEvent(createRegistryOneVKey(BillingEvent.Recurring.class, 100L))
.setAutorenewPollMessage(createRegistryOneVKey(PollMessage.Autorenew.class, 200L))
.setDeletePollMessage(createRegistryOneVKey(PollMessage.OneTime.class, 300L))
.build();
}
@Test
void removeRegistryOneKeyInDomainBase_succeeds() throws Exception {
DomainBase origin = persistResource(domain);
runCommand(
"--force",
"--key_paths_file",
writeToNamedTmpFile("keypath.txt", getKeyPathLiteral(domain)));
DomainBase persisted = ofy().load().key(domain.createVKey().getOfyKey()).now();
assertThat(ImmutableList.of(persisted))
.comparingElementsUsing(getDomainBaseCorrespondence())
.containsExactly(origin);
assertThat(persisted.getAutorenewBillingEvent()).isNull();
assertThat(persisted.getAutorenewPollMessage()).isNull();
assertThat(persisted.getDeletePollMessage()).isNull();
}
@Test
void removeRegistryOneKeyInDomainBase_notModifyRegistryTwoKey() throws Exception {
DomainBase origin =
persistResource(
domain
.asBuilder()
.setAutorenewBillingEvent(
createRegistryTwoVKey(BillingEvent.Recurring.class, domain, 300L))
.build());
runCommand(
"--force",
"--key_paths_file",
writeToNamedTmpFile("keypath.txt", getKeyPathLiteral(domain)));
DomainBase persisted = ofy().load().key(domain.createVKey().getOfyKey()).now();
assertThat(ImmutableList.of(persisted))
.comparingElementsUsing(getDomainBaseCorrespondence())
.containsExactly(origin);
assertThat(persisted.getAutorenewBillingEvent())
.isEqualTo(createRegistryTwoVKey(BillingEvent.Recurring.class, domain, 300L));
assertThat(persisted.getAutorenewPollMessage()).isNull();
assertThat(persisted.getDeletePollMessage()).isNull();
}
private static String getKeyPathLiteral(Object entity) {
Key<?> key = Key.create(entity);
return String.format("\"DomainBase\", \"%s\"", key.getName());
}
private static <T> VKey<T> createRegistryOneVKey(Class<T> clazz, long id) {
Key<?> parent = Key.create(EntityGroupRoot.class, "per-tld");
return VKey.create(clazz, id, Key.create(parent, clazz, id));
}
private static <T> VKey<T> createRegistryTwoVKey(Class<T> clazz, DomainBase domain, long id) {
Key<?> parent = Key.create(domain.createVKey().getOfyKey(), HistoryEntry.class, 1000L);
return VKey.create(clazz, id, Key.create(parent, clazz, id));
}
private static Correspondence<ImmutableObject, ImmutableObject> getDomainBaseCorrespondence() {
return immutableObjectCorrespondence(
"revisions",
"updateTimestamp",
"autorenewBillingEvent",
"autorenewPollMessage",
"deletePollMessage");
}
}

View File

@@ -0,0 +1,42 @@
// 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.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.junit.Assert.assertThrows;
import google.registry.schema.replay.SqlReplayCheckpoint;
import org.joda.time.DateTime;
import org.junit.jupiter.api.Test;
class SetSqlReplayCheckpointCommandTest extends CommandTestCase<SetSqlReplayCheckpointCommand> {
@Test
void testSuccess() throws Exception {
assertThat(jpaTm().transact(SqlReplayCheckpoint::get)).isEqualTo(START_OF_TIME);
DateTime timeToSet = DateTime.parse("2000-06-06T22:00:00.0Z");
runCommandForced(timeToSet.toString());
assertThat(jpaTm().transact(SqlReplayCheckpoint::get)).isEqualTo(timeToSet);
}
@Test
void testFailure_multipleParams() throws Exception {
DateTime one = DateTime.parse("2000-06-06T22:00:00.0Z");
DateTime two = DateTime.parse("2001-06-06T22:00:00.0Z");
assertThrows(IllegalArgumentException.class, () -> runCommand(one.toString(), two.toString()));
}
}

View File

@@ -18,7 +18,6 @@ import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.registrar.Registrar.State.ACTIVE;
import static google.registry.model.registry.Registry.TldState.GENERAL_AVAILABILITY;
import static google.registry.model.registry.Registry.TldState.START_DATE_SUNRISE;
import static google.registry.testing.CertificateSamples.SAMPLE_CERT;
import static google.registry.testing.CertificateSamples.SAMPLE_CERT_HASH;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
@@ -98,8 +97,7 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
String registrarName,
String allowedTld,
String password,
ImmutableList<CidrAddressBlock> ipAllowList,
boolean hashOnly) {
ImmutableList<CidrAddressBlock> ipAllowList) {
Registrar registrar = loadRegistrar(registrarName);
assertThat(registrar).isNotNull();
assertThat(registrar.getAllowedTlds()).containsExactlyElementsIn(ImmutableSet.of(allowedTld));
@@ -108,18 +106,6 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
assertThat(registrar.verifyPassword(password)).isTrue();
assertThat(registrar.getIpAddressAllowList()).isEqualTo(ipAllowList);
assertThat(registrar.getClientCertificateHash()).isEqualTo(SAMPLE_CERT_HASH);
// If certificate hash is provided, there's no certificate file stored with the registrar.
if (!hashOnly) {
assertThat(registrar.getClientCertificate()).isEqualTo(SAMPLE_CERT);
}
}
private void verifyRegistrarCreation(
String registrarName,
String allowedTld,
String password,
ImmutableList<CidrAddressBlock> ipAllowList) {
verifyRegistrarCreation(registrarName, allowedTld, password, ipAllowList, false);
}
private void verifyRegistrarContactCreation(String registrarName, String email) {
@@ -184,24 +170,6 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
verifyRegistrarContactCreation("abc-5", "abc@email.com");
}
@Test
void testSuccess_certificateHash() throws Exception {
runCommandForced(
"--ip_allow_list=1.1.1.1",
"--registrar=blobio",
"--email=contact@email.com",
"--certhash=" + SAMPLE_CERT_HASH);
verifyTldCreation("blobio-eap", "BLOBIOE3", GENERAL_AVAILABILITY, true);
ImmutableList<CidrAddressBlock> ipAddress =
ImmutableList.of(CidrAddressBlock.create("1.1.1.1"));
verifyRegistrarCreation("blobio-5", "blobio-eap", PASSWORD, ipAddress, true);
verifyRegistrarContactCreation("blobio-5", "contact@email.com");
}
@Test
void testSuccess_multipleIps() throws Exception {
runCommandForced(
@@ -256,7 +224,7 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
}
@Test
void testFailure_missingCertificateFileAndCertificateHash() {
void testFailure_missingCertificateFile() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
@@ -265,26 +233,7 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
"--ip_allow_list=1.1.1.1", "--email=contact@email.com", "--registrar=blobio"));
assertThat(thrown)
.hasMessageThat()
.contains(
"Must specify exactly one of client certificate file or client certificate hash.");
}
@Test
void testFailure_suppliedCertificateFileAndCertificateHash() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
runCommandForced(
"--ip_allow_list=1.1.1.1",
"--email=contact@email.com",
"--registrar=blobio",
"--certfile=" + getCertFilename(),
"--certhash=" + SAMPLE_CERT_HASH));
assertThat(thrown)
.hasMessageThat()
.contains(
"Must specify exactly one of client certificate file or client certificate hash.");
.contains("Must specify exactly one client certificate file.");
}
@Test

View File

@@ -21,7 +21,6 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
import static google.registry.testing.CertificateSamples.SAMPLE_CERT;
import static google.registry.testing.CertificateSamples.SAMPLE_CERT3;
import static google.registry.testing.CertificateSamples.SAMPLE_CERT3_HASH;
import static google.registry.testing.CertificateSamples.SAMPLE_CERT_HASH;
import static google.registry.testing.DatabaseHelper.createTlds;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.persistResource;
@@ -339,14 +338,6 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
assertThat(registrar.getFailoverClientCertificate()).isEqualTo(SAMPLE_CERT3);
}
@Test
void testSuccess_certHash() throws Exception {
assertThat(loadRegistrar("NewRegistrar").getClientCertificateHash()).isNull();
runCommand("--cert_hash=" + SAMPLE_CERT_HASH, "--force", "NewRegistrar");
assertThat(loadRegistrar("NewRegistrar").getClientCertificateHash())
.isEqualTo(SAMPLE_CERT_HASH);
}
@Test
void testSuccess_clearCert() throws Exception {
persistResource(
@@ -359,18 +350,6 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
assertThat(loadRegistrar("NewRegistrar").getClientCertificate()).isNull();
}
@Test
void testSuccess_clearCertHash() throws Exception {
persistResource(
loadRegistrar("NewRegistrar")
.asBuilder()
.setClientCertificateHash(SAMPLE_CERT_HASH)
.build());
assertThat(isNullOrEmpty(loadRegistrar("NewRegistrar").getClientCertificateHash())).isFalse();
runCommand("--cert_hash=null", "--force", "NewRegistrar");
assertThat(loadRegistrar("NewRegistrar").getClientCertificateHash()).isNull();
}
@Test
void testSuccess_ianaId() throws Exception {
assertThat(loadRegistrar("NewRegistrar").getIanaIdentifier()).isEqualTo(8);
@@ -762,18 +741,6 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
() -> runCommand("--cert_file=" + writeToTmpFile("ABCDEF"), "--force", "NewRegistrar"));
}
@Test
void testFailure_certHashAndCertFile() {
assertThrows(
IllegalArgumentException.class,
() ->
runCommand(
"--cert_file=" + getCertFilename(SAMPLE_CERT3),
"--cert_hash=ABCDEF",
"--force",
"NewRegistrar"));
}
@Test
void testFailure_missingClientId() {
assertThrows(ParameterException.class, () -> runCommand("--force"));

View File

@@ -20,6 +20,7 @@ import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static org.joda.time.DateTimeZone.UTC;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.beust.jcommander.ParameterException;
@@ -33,6 +34,7 @@ import google.registry.testing.CertificateSamples;
import google.registry.util.CidrAddressBlock;
import java.nio.file.Files;
import java.nio.file.Path;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -50,7 +52,7 @@ class ValidateLoginCredentialsCommandTest extends CommandTestCase<ValidateLoginC
loadRegistrar("NewRegistrar")
.asBuilder()
.setPassword(PASSWORD)
.setClientCertificateHash(CERT_HASH)
.setClientCertificate(CertificateSamples.SAMPLE_CERT, DateTime.now(UTC))
.setIpAddressAllowList(ImmutableList.of(new CidrAddressBlock(CLIENT_IP)))
.setState(ACTIVE)
.setAllowedTlds(ImmutableSet.of("tld"))

View File

@@ -0,0 +1,271 @@
// 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.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;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.reporting.spec11.Spec11RegistrarThreatMatchesParserTest.sampleThreatMatches;
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.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistResource;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import google.registry.model.domain.DomainBase;
import google.registry.model.reporting.Spec11ThreatMatch;
import google.registry.model.reporting.Spec11ThreatMatch.ThreatType;
import google.registry.reporting.spec11.Spec11RegistrarThreatMatchesParser;
import google.registry.tools.CommandTestCase;
import java.io.IOException;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Tests for {@link BackfillSpec11ThreatMatchesCommand}. */
public class BackfillSpec11ThreatMatchesCommandTest
extends CommandTestCase<BackfillSpec11ThreatMatchesCommand> {
private static final LocalDate CURRENT_DATE = DateTime.parse("2020-11-22").toLocalDate();
private final Spec11RegistrarThreatMatchesParser threatMatchesParser =
mock(Spec11RegistrarThreatMatchesParser.class);
private DomainBase domainA;
@BeforeEach
void beforeEach() throws Exception {
createTld("com");
domainA = persistActiveDomain("a.com");
persistActiveDomain("b.com");
persistActiveDomain("c.com");
fakeClock.setTo(CURRENT_DATE.toDateTimeAtStartOfDay());
command.threatMatchesParser = threatMatchesParser;
command.clock = fakeClock;
when(threatMatchesParser.getRegistrarThreatMatches(any(LocalDate.class)))
.thenReturn(ImmutableSet.of());
}
@Test
void testSuccess_singleFile() throws Exception {
when(threatMatchesParser.getRegistrarThreatMatches(CURRENT_DATE))
.thenReturn(sampleThreatMatches());
runCommandForced();
assertInStdout("Backfill Spec11 results from 692 files?");
assertInStdout("Successfully parsed through 692 files with 3 threats.");
verifyExactlyThreeEntriesInDbFromLastDay();
}
@Test
void testSuccess_sameDomain_multipleDays() throws Exception {
// If the same domains show up on multiple days, there should be multiple entries for them
when(threatMatchesParser.getRegistrarThreatMatches(CURRENT_DATE))
.thenReturn(sampleThreatMatches());
when(threatMatchesParser.getRegistrarThreatMatches(LocalDate.parse("2019-01-01")))
.thenReturn(sampleThreatMatches());
runCommandForced();
assertInStdout("Backfill Spec11 results from 692 files?");
assertInStdout("Successfully parsed through 692 files with 6 threats.");
jpaTm()
.transact(
() -> {
ImmutableList<Spec11ThreatMatch> threatMatches =
jpaTm().loadAll(Spec11ThreatMatch.class);
assertThat(threatMatches).hasSize(6);
assertThat(
threatMatches.stream()
.map(Spec11ThreatMatch::getDomainName)
.collect(toImmutableSet()))
.containsExactly("a.com", "b.com", "c.com");
assertThat(
threatMatches.stream()
.map(Spec11ThreatMatch::getCheckDate)
.collect(toImmutableSet()))
.containsExactly(CURRENT_DATE, LocalDate.parse("2019-01-01"));
});
}
@Test
void testSuccess_empty() throws Exception {
runCommandForced();
assertInStdout("Backfill Spec11 results from 692 files?");
assertInStdout("Successfully parsed through 692 files with 0 threats.");
}
@Test
void testSuccess_sameDayTwice() throws Exception {
when(threatMatchesParser.getRegistrarThreatMatches(CURRENT_DATE))
.thenReturn(sampleThreatMatches());
runCommandForced();
runCommandForced();
verifyExactlyThreeEntriesInDbFromLastDay();
}
@Test
void testSuccess_threeDomainsForDomainName() throws Exception {
// We should use the repo ID from the proper DomainBase object at the scan's point in time.
// First, domain was created at START_OF_TIME and deleted one year ago
DateTime now = fakeClock.nowUtc();
domainA = persistResource(domainA.asBuilder().setDeletionTime(now.minusYears(1)).build());
// Next, domain was created six months ago and deleted two months ago
DomainBase secondSave =
persistResource(
newDomainBase("a.com")
.asBuilder()
.setCreationTimeForTest(now.minusMonths(6))
.setDeletionTime(now.minusMonths(2))
.build());
// Lastly, domain was created one month ago and is still valid
DomainBase thirdSave =
persistResource(
newDomainBase("a.com").asBuilder().setCreationTimeForTest(now.minusMonths(1)).build());
// If the scan result was from three months ago, we should use the second save
when(threatMatchesParser.getRegistrarThreatMatches(now.toLocalDate().minusMonths(3)))
.thenReturn(sampleThreatMatches());
runCommandForced();
String threatMatchRepoId =
jpaTm()
.transact(
() ->
jpaTm().loadAll(Spec11ThreatMatch.class).stream()
.filter((match) -> match.getDomainName().equals("a.com"))
.findFirst()
.get()
.getDomainRepoId());
assertThat(threatMatchRepoId).isNotEqualTo(domainA.getRepoId());
assertThat(threatMatchRepoId).isEqualTo(secondSave.getRepoId());
assertThat(threatMatchRepoId).isNotEqualTo(thirdSave.getRepoId());
}
@Test
void testSuccess_skipsExistingDatesWithoutOverwrite() throws Exception {
when(threatMatchesParser.getRegistrarThreatMatches(CURRENT_DATE))
.thenReturn(sampleThreatMatches());
Spec11ThreatMatch previous =
new Spec11ThreatMatch.Builder()
.setCheckDate(CURRENT_DATE)
.setDomainName("previous.tld")
.setDomainRepoId("1-DOMAIN")
.setRegistrarId("TheRegistrar")
.setThreatTypes(ImmutableSet.of(ThreatType.MALWARE))
.build();
jpaTm().transact(() -> jpaTm().put(previous));
runCommandForced();
ImmutableList<Spec11ThreatMatch> threatMatches =
jpaTm().transact(() -> jpaTm().loadAll(Spec11ThreatMatch.class));
assertAboutImmutableObjects()
.that(Iterables.getOnlyElement(threatMatches))
.isEqualExceptFields(previous, "id");
}
@Test
void testSuccess_overwritesExistingDatesWhenSpecified() throws Exception {
when(threatMatchesParser.getRegistrarThreatMatches(CURRENT_DATE))
.thenReturn(sampleThreatMatches());
Spec11ThreatMatch previous =
new Spec11ThreatMatch.Builder()
.setCheckDate(CURRENT_DATE)
.setDomainName("previous.tld")
.setDomainRepoId("1-DOMAIN")
.setRegistrarId("TheRegistrar")
.setThreatTypes(ImmutableSet.of(ThreatType.MALWARE))
.build();
jpaTm().transact(() -> jpaTm().put(previous));
runCommandForced("--overwrite_existing_dates");
verifyExactlyThreeEntriesInDbFromLastDay();
}
@Test
void testFailure_oneFileFails() throws Exception {
// If there are any exceptions, we should fail loud and fast
when(threatMatchesParser.getRegistrarThreatMatches(CURRENT_DATE))
.thenReturn(sampleThreatMatches());
when(threatMatchesParser.getRegistrarThreatMatches(CURRENT_DATE.minusDays(1)))
.thenThrow(new IOException("hi"));
RuntimeException runtimeException =
assertThrows(RuntimeException.class, this::runCommandForced);
assertThat(runtimeException.getCause().getClass()).isEqualTo(IOException.class);
assertThat(runtimeException).hasCauseThat().hasMessageThat().isEqualTo("hi");
jpaTm().transact(() -> assertThat(jpaTm().loadAll(Spec11ThreatMatch.class)).isEmpty());
}
@Test
void testFailure_noDomainForDomainName() throws Exception {
deleteResource(domainA);
when(threatMatchesParser.getRegistrarThreatMatches(CURRENT_DATE))
.thenReturn(sampleThreatMatches());
assertThat(assertThrows(IllegalStateException.class, this::runCommandForced))
.hasMessageThat()
.isEqualTo("Domain name a.com had no associated DomainBase objects.");
}
@Test
void testFailure_noDomainAtTimeOfScan() throws Exception {
// If the domain existed at some point(s) in time but not the time of the scan, fail.
// First, domain was created at START_OF_TIME and deleted one year ago
DateTime now = fakeClock.nowUtc();
domainA = persistResource(domainA.asBuilder().setDeletionTime(now.minusYears(1)).build());
// Second, domain was created one month ago and is still valid
persistResource(
newDomainBase("a.com").asBuilder().setCreationTimeForTest(now.minusMonths(1)).build());
// If we have a result for this domain from 3 months ago when it didn't exist, fail.
when(threatMatchesParser.getRegistrarThreatMatches(now.toLocalDate().minusMonths(3)))
.thenReturn(sampleThreatMatches());
assertThat(assertThrows(IllegalStateException.class, this::runCommandForced))
.hasMessageThat()
.isEqualTo("Could not find a DomainBase valid for a.com on day 2020-08-22.");
}
private void verifyExactlyThreeEntriesInDbFromLastDay() {
jpaTm()
.transact(
() -> {
ImmutableList<Spec11ThreatMatch> threatMatches =
jpaTm().loadAll(Spec11ThreatMatch.class);
assertThat(threatMatches)
.comparingElementsUsing(immutableObjectCorrespondence("id", "domainRepoId"))
.containsExactly(
expectedThreatMatch("TheRegistrar", "a.com"),
expectedThreatMatch("NewRegistrar", "b.com"),
expectedThreatMatch("NewRegistrar", "c.com"));
});
}
private Spec11ThreatMatch expectedThreatMatch(String registrarId, String domainName) {
return new Spec11ThreatMatch.Builder()
.setDomainRepoId("ignored")
.setDomainName(domainName)
.setRegistrarId(registrarId)
.setCheckDate(CURRENT_DATE)
.setThreatTypes(ImmutableSet.of(ThreatType.MALWARE))
.build();
}
}

View File

@@ -342,26 +342,6 @@ class RegistrarConsoleScreenshotTest extends WebDriverTestCase {
driver.diffPage("edit");
}
@RetryingTest(3)
void settingsSecurityWithHashOnly() throws Throwable {
server.runInAppEngineEnvironment(
() -> {
persistResource(
loadRegistrar("TheRegistrar")
.asBuilder()
.setClientCertificateHash(CertificateSamples.SAMPLE_CERT_HASH)
.build());
return null;
});
driver.manage().window().setSize(new Dimension(1050, 2000));
driver.get(server.getUrl("/registrar#security-settings"));
driver.waitForDisplayedElement(By.tagName("h1"));
driver.diffPage("view");
driver.waitForDisplayedElement(By.id("reg-app-btn-edit")).click();
driver.waitForDisplayedElement(By.tagName("h1"));
driver.diffPage("edit");
}
@RetryingTest(3)
void index_registrarDisabled() throws Throwable {
server.runInAppEngineEnvironment(

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -77,3 +77,5 @@ V76__change_history_nullability.sql
V77__fixes_for_replay.sql
V78__add_history_id_for_redemption_history_entry.sql
V79__drop_foreign_keys_on_pollmessage.sql
V80__defer_bill_event_key.sql
V81__drop_spec11_fkeys.sql

View File

@@ -0,0 +1,27 @@
-- 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.
-- We have to make this foreign key initially deferred even though
-- BillingEvent is saved before Domain (and deleted after). If we don't, it
-- appears that Hibernate can sporadically introduce FK constraint failures
-- when updating a Domain to reference a new BillingEvent and then deleting
-- the old BillingEvent. This may be due to the fact that this FK
-- relationships is not known to hibernate.
ALTER TABLE "Domain" DROP CONSTRAINT fk_domain_transfer_billing_event_id;
ALTER TABLE if exists "Domain"
ADD CONSTRAINT fk_domain_transfer_billing_event_id
FOREIGN KEY (transfer_billing_event_id)
REFERENCES "BillingEvent"
DEFERRABLE INITIALLY DEFERRED;

View File

@@ -0,0 +1,18 @@
-- 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.
-- Some objects haven't been fully populated in SQL yet, so don't depend on them.
ALTER TABLE "Spec11ThreatMatch" DROP CONSTRAINT "fk_spec11_threat_match_domain_repo_id";
ALTER TABLE "Spec11ThreatMatch" DROP CONSTRAINT "fk_spec11_threat_match_registrar_id";
ALTER TABLE "Spec11ThreatMatch" DROP CONSTRAINT "fk_spec11_threat_match_tld";

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