Compare commits

..
Author SHA1 Message Date
Weimin YuandGitHub 73ba96a5d4 A better Datastore bulk delete tool (#947)
* A better Datastore bulk delete tool

Implements an alternative to GCP builtin tool with better performance
and usability.
2021-02-02 22:46:38 -05:00
Ben McIlwainandGitHub 90db60643e Add delete expired domains action (#836)
* Add delete expired domains action

This will be scheduled to run daily via App Engine cron in a subsequent PR.

* Add test
2021-02-02 18:46:15 -05:00
Ben McIlwainandGitHub 98283a67ac Use END_OF_TIME as sentinel value for domain's autorenewEndTime (#949)
* Use END_OF_TIME as sentinel value for domain's autorenewEndTime

Datastore inequality queries don't work correctly for null; null is treated as
the lowest value possible which is definitely the opposite of the intended
meaning here.

This includes an @OnLoad for backfilling purposes using the ResaveAll mapreduce.
2021-02-01 15:29:51 -05:00
Michael MullerandGitHub e70f14001c Make cross database comparison recursive (#942)
* Make cross database comparison recursive

Cross-database comparison was previously just a shallow check: fields marked
with DoNotCompare on nested objects were still compared.  This causes problems
in some cases where there are nested immutable objects.

This change introduces recursive comparison.  It also provides a
hasCorrectHashCode() method that verifies that an object has not been mutated
since the hash code was calculated, which has been a problem in certain cases.

Finally, this also fixes the problem of objects that are mutated in multiple
transactions: we were previously comparing against the value in datastore, but
this doesn't work in these cases because the object in datastore may have
changed since the transaction that we are verifying.  Instead, check against
the value that we would have persisted in the original transaction.

* Changes requested in review

* Converted check method interfaces

Per review discussion, converted check method interface so that they
consistently return a ComparisonResult object which encapsulates a success
indicator and an optional error message.

* Another round of changes on ImmutableObjectSubject

* Final changes for review

Removed unnecessary null check, minor reformatting.

(this also removes an obsolete nullness assertion from an earlier commit that
should have been fixed in the rebase)

* Try removing that nullness check import again....
2021-01-29 18:57:20 -05:00
sarahcaseybotandGitHub 22d3612be3 Convert Strings to X509 Certificates before validating (#948)
* Convert certificate strings to certificates

* Format fixes

* Revert "Format fixes"

This reverts commit 26f88bd313.

* Revert "Convert certificate strings to certificates"

This reverts commit 6d47ed2861.

* Convert strings to certs for validation

* Add clarification comments

* Add test to verify endoded cert from proxy

* Add some helper methods

* add tests for PEM with metadata

* small changes

* replace .com with .test
2021-01-29 16:59:57 -05:00
sarahcaseybotandGitHub ad8bc05877 Fix typo in header name in Client Certificate Provider (#946)
* Fix typo in header name

* fix test
2021-01-26 20:10:41 -05:00
Ben McIlwainandGitHub a3537447ef Add clientCertificate to TlsCredentials.toString() (#945)
* Add clientCertificate to TlsCredentials.toString()

FlowRunner.run() logs these credentials to the GAE logs by implicitly using the
toString() method, so we need to add it if we want it to appear in the logs.
2021-01-26 17:20:21 -05:00
Ben McIlwainandGitHub 4e66fed497 Use nullness parity helper (#944)
* Use nullness parity helper
2021-01-26 13:20:48 -05:00
gbrodmanandGitHub 886cdfa39b Update NPM dependency based on Github security warning (#941) 2021-01-25 23:04:30 -05:00
sarahcaseybotandGitHub beefa9364b Use CertificateChecker on login (#936)
* Use CertificateChecker on login

* Add actual enforcement of requirements in sandbox

* Add new Exceptions

* add validation command to RegistryToolComponent

* Fix error messages

* Add a test for production behavior

* check logs in test

* move loghandler
2021-01-22 16:32:15 -05:00
gbrodmanandGitHub 73210e4b09 Convert (most) HistoryEntry ofy calls to tm (#933)
* Convert (most) HistoryEntry ofy calls to tm

As part of this change, it was necessary to do changes in the JPATM that
are similar (but the opposite) of the changes that we did in
DatastoreTM with regards to converting HistoryEntries to and from the
*History classes.

We leave the ofy() calls in the MapReduce ResaveAllHistoryEntriesAction
for now; that can be converted during the Beam pipeline transition.

Some other tests required registrar-name fixes as well -- because
*History objects have a foreign key on the Registrar table, we have to
use a "real" registrar name in tests.

* Add simple HistoryEntryDaoTest
2021-01-22 14:43:34 -05:00
Ben McIlwainandGitHub 08cec96a93 Correct containsMatch() -> contains() for non-regexes (#940)
* Correct containsMatch() -> contains() for non-regexes
2021-01-22 14:31:38 -05:00
Ben McIlwainandGitHub 31ef402c50 Require an override flag to allow updating pending delete domains (#939)
* Require an override flag to allow updating pending delete domains

Needing to update pending delete domains is an uncommon situation, yet currently
we are allowing superusers to do so without any extra validation (which has led
to errors). This adds a new override flag to gate the update of pending delete
domains; without it, the update will fail.
2021-01-22 14:31:13 -05:00
Michael MullerandGitHub e89cc4406a Fix another "extra parens" warning (#938)
* Fix another "extra parens" warning

Same place as the last one, but I missed it :-(
2021-01-22 13:39:30 -05:00
Shicong HuangandGitHub 48de5d8375 Convert ofy() to tm() for all contact transfer flows (#937)
* Convert ofy() to tm() for all contact transfer flows

* Resolve comments
2021-01-22 09:38:51 -05:00
Ben McIlwainandGitHub 59abc1d154 Put else if on same line to fix build style warning (#935)
* Put else if on same line to fix build style warning
2021-01-21 10:50:29 -05:00
Shicong HuangandGitHub 6794c6fbd7 Resolve remaining TODO(shicong) (#932) 2021-01-20 19:27:48 -05:00
Ben McIlwainandGitHub 0c384adc22 Change java.util.Optional.isEmpty() to !isPresent() (#934)
isEmpty() is not available in the version of Java GAE uses and is throwing
runtime errors (!!). I think these got into our codebases because people don't
have the language version set correctly in IntelliJ; they show as outright
errors for me (I'm on language level 8).
2021-01-20 09:38:52 -05:00
sarahcaseybotandGitHub 3b679058b0 Validate Certificate on Login (#919)
* Check certificate matches saved one on login

* Add tests

* refactoring

* fix warning messages
2021-01-19 17:06:26 -05:00
Shicong HuangandGitHub 9b5805f145 Resolve TODOs under Shicong's name (#930) 2021-01-15 14:22:31 -05:00
Michael MullerandGitHub 9e6f99face Add object comparison to replay tests (#925)
* Add object comparison to replay tests

Allow optional object comparison in the replay test extension and enable it
for the DomainCreateFlow test.

To faciliate this, add two new field annotations to ImmutableObject:
DoNotCompare, to be used for fields that are not relevant to either database,
and Insignificant, to be used for fields that are mutated after they have been
accessed and therefore violate immutability (there is currently only one of
these, however we might discover more in the course of adding more comparisons
to the replay test.

* Revert commented out premium price error log

* Added static create methods for ReplayExtension
2021-01-15 14:20:55 -05:00
107 changed files with 7249 additions and 3326 deletions
+11 -1
View File
@@ -172,6 +172,8 @@ dependencies {
compile deps['com.beust:jcommander']
compile deps['com.google.api:gax']
compile deps['com.google.api.grpc:proto-google-cloud-datastore-v1']
compile deps['com.google.api.grpc:proto-google-common-protos']
compile deps['com.google.api.grpc:proto-google-cloud-secretmanager-v1']
compile deps['com.google.api-client:google-api-client']
compile deps['com.google.api-client:google-api-client-appengine']
@@ -196,6 +198,8 @@ dependencies {
compile deps['com.google.appengine:appengine-remote-api']
compile deps['com.google.auth:google-auth-library-credentials']
compile deps['com.google.auth:google-auth-library-oauth2-http']
compile deps['com.google.cloud.bigdataoss:util']
compile deps['com.google.cloud.datastore:datastore-v1-proto-client']
compile deps['com.google.cloud.sql:jdbc-socket-factory-core']
runtimeOnly deps['com.google.cloud.sql:postgres-socket-factory']
compile deps['com.google.cloud:google-cloud-secretmanager']
@@ -600,7 +604,6 @@ task compileProdJS(type: JavaExec) {
closureArgs << "--js=${jsDir}/soyutils_usegoog.js"
closureArgs << "--js=${cssSourceDir}/registrar_bin.css.js"
closureArgs << "--js=${jsSourceDir}/**.js"
// TODO(shicong) Verify the compiled JS file works in Alpha
closureArgs << "--js=${externsDir}/json.js"
closureArgs << "--js=${soySourceDir}/**.js"
args closureArgs
@@ -737,6 +740,13 @@ project.tasks.create('initSqlPipeline', JavaExec) {
}
}
// Caller must provide projectId, GCP region, runner, and the kinds to delete
// (comma-separated kind names or '*' for all). E.g.:
// nom_build :core:bulkDeleteDatastore --args="--project=domain-registry-crash \
// --region=us-central1 --runner=DataflowRunner --kindsToDelete=*"
createToolTask(
'bulkDeleteDatastore', 'google.registry.beam.datastore.BulkDeletePipeline')
project.tasks.create('generateSqlSchema', JavaExec) {
classpath = sourceSets.nonprod.runtimeClasspath
main = 'google.registry.tools.DevTool'
@@ -85,7 +85,7 @@ public class ReplayCommitLogsToSqlAction implements Runnable {
Optional<Lock> lock =
Lock.acquire(
this.getClass().getSimpleName(), null, LEASE_LENGTH, requestStatusChecker, false);
if (lock.isEmpty()) {
if (!lock.isPresent()) {
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.
@@ -182,7 +182,7 @@ public class ReplayCommitLogsToSqlAction implements Runnable {
}
private static int compareByWeight(VersionedEntity a, VersionedEntity b) {
return getEntityPriority(a.key().getKind(), a.getEntity().isEmpty())
- getEntityPriority(b.key().getKind(), b.getEntity().isEmpty());
return getEntityPriority(a.key().getKind(), !a.getEntity().isPresent())
- getEntityPriority(b.key().getKind(), !b.getEntity().isPresent());
}
}
@@ -0,0 +1,193 @@
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.batch;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
import static google.registry.flows.FlowUtils.marshalWithLenientRetry;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.ResourceUtils.readResourceUtf8;
import static java.nio.charset.StandardCharsets.UTF_8;
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig.Config;
import google.registry.flows.EppController;
import google.registry.flows.EppRequestSource;
import google.registry.flows.PasswordOnlyTransportCredentials;
import google.registry.flows.StatelessRequestSessionMetadata;
import google.registry.model.domain.DomainBase;
import google.registry.model.eppcommon.ProtocolDefinition;
import google.registry.model.eppoutput.EppOutput;
import google.registry.request.Action;
import google.registry.request.Action.Method;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.request.lock.LockHandler;
import google.registry.util.Clock;
import java.util.Optional;
import java.util.concurrent.Callable;
import javax.inject.Inject;
import org.joda.time.DateTime;
import org.joda.time.Duration;
/**
* An action that deletes all non-renewing domains whose expiration dates have now passed.
*
* <p>The registry runs on an autorenew domain model, so domains don't ever expire naturally; they
* are only ever autorenewed. However, in some situations (such as URS) we don't want this to
* happen. Thus, the domains are tagged as non-renewing and are deleted by the next daily invocation
* of this action once they are past the date at which they were to expire.
*
* <p>Note that this action works by running a superuser EPP domain delete command, and as a side
* effect of when domains are deleted (just past their expiration date), they are invariably in the
* autorenew grace period when this happens.
*/
@Action(
service = Action.Service.BACKEND,
path = DeleteExpiredDomainsAction.PATH,
auth = Auth.AUTH_INTERNAL_OR_ADMIN,
method = Method.POST)
public class DeleteExpiredDomainsAction implements Runnable {
public static final String PATH = "/_dr/task/deleteExpiredDomains";
private static final String LOCK_NAME = "Delete expired domains";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final EppController eppController;
private final String registryAdminClientId;
private final Clock clock;
private final LockHandler lockHandler;
private final Response response;
private final String deleteXmlTmpl;
@Inject
DeleteExpiredDomainsAction(
EppController eppController,
@Config("registryAdminClientId") String registryAdminClientId,
Clock clock,
LockHandler lockHandler,
Response response) {
this.eppController = eppController;
this.registryAdminClientId = registryAdminClientId;
this.clock = clock;
this.lockHandler = lockHandler;
this.response = response;
this.deleteXmlTmpl =
readResourceUtf8(DeleteExpiredDomainsAction.class, "delete_expired_domain.xml");
}
@Override
public void run() {
response.setContentType(PLAIN_TEXT_UTF_8);
Callable<Void> runner =
() -> {
try {
runLocked();
response.setStatus(SC_OK);
} catch (Exception e) {
response.setStatus(SC_INTERNAL_SERVER_ERROR);
response.setPayload("Encountered error; see GCP logs for full details.");
}
return null;
};
if (!lockHandler.executeWithLocks(runner, null, Duration.standardHours(1), LOCK_NAME)) {
// Send a 200-series status code to prevent this conflicting action from retrying.
response.setStatus(SC_NO_CONTENT);
response.setPayload("Could not acquire lock; already running?");
}
}
private void runLocked() {
DateTime runTime = clock.nowUtc();
logger.atInfo().log(
"Deleting non-renewing domains with autorenew end times up through %s.", runTime);
// Note: This query is (and must be) non-transactional, and thus, is only eventually consistent.
ImmutableList<DomainBase> domainsToDelete =
ofy().load().type(DomainBase.class).filter("autorenewEndTime <=", runTime).list().stream()
// Datastore can't do two inequalities in one query, so the second happens in-memory.
.filter(d -> d.getDeletionTime().isEqual(END_OF_TIME))
.collect(toImmutableList());
if (domainsToDelete.isEmpty()) {
logger.atInfo().log("Found 0 domains to delete.");
response.setPayload("Found 0 domains to delete.");
return;
}
logger.atInfo().log(
"Found %d domains to delete: %s.",
domainsToDelete.size(),
String.join(
", ",
domainsToDelete.stream().map(DomainBase::getDomainName).collect(toImmutableList())));
domainsToDelete.forEach(this::runDomainDeleteFlow);
logger.atInfo().log("Finished deleting domains.");
response.setPayload("Finished deleting domains.");
}
private void runDomainDeleteFlow(DomainBase domain) {
logger.atInfo().log("Attempting to delete domain %s", domain.getDomainName());
// Create a new transaction that the flow's execution will be enlisted in that loads the domain
// transactionally. This way we can ensure that nothing else has modified the domain in question
// in the intervening period since the query above found it.
Optional<EppOutput> eppOutput =
tm().transact(
() -> {
DomainBase transDomain = tm().loadByKey(domain.createVKey());
if (!domain.getAutorenewEndTime().isPresent()
|| domain.getAutorenewEndTime().get().isAfter(tm().getTransactionTime())) {
logger.atSevere().log(
"Failed to delete domain %s because of its autorenew end time: %s.",
transDomain.getDomainName(), transDomain.getAutorenewEndTime());
return Optional.empty();
} else if (domain.getDeletionTime().isBefore(END_OF_TIME)) {
logger.atSevere().log(
"Failed to delete domain %s because it was already deleted on %s.",
transDomain.getDomainName(), transDomain.getDeletionTime());
return Optional.empty();
}
return Optional.of(
eppController.handleEppCommand(
new StatelessRequestSessionMetadata(
registryAdminClientId,
ProtocolDefinition.getVisibleServiceExtensionUris()),
new PasswordOnlyTransportCredentials(),
EppRequestSource.BACKEND,
false,
true,
deleteXmlTmpl
.replace("%DOMAIN%", transDomain.getDomainName())
.getBytes(UTF_8)));
});
if (eppOutput.isPresent()) {
if (eppOutput.get().isSuccess()) {
logger.atInfo().log("Successfully deleted domain %s", domain.getDomainName());
} else {
logger.atWarning().log(
"Failed to delete domain %s; EPP response:\n\n%s",
domain.getDomainName(), new String(marshalWithLenientRetry(eppOutput.get()), UTF_8));
}
}
}
}
@@ -0,0 +1,17 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<delete>
<domain:delete
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>%DOMAIN%</domain:name>
</domain:delete>
</delete>
<extension>
<metadata:metadata xmlns:metadata="urn:google:params:xml:ns:metadata-1.0">
<metadata:reason>Non-renewing domain has reached expiration date.</metadata:reason>
<metadata:requestedByRegistrar>false</metadata:requestedByRegistrar>
</metadata:metadata>
</extension>
<clTRID>ABC-12345</clTRID>
</command>
</epp>
@@ -0,0 +1,330 @@
// 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.beam.datastore;
import static com.google.common.base.Preconditions.checkState;
import static org.apache.beam.sdk.values.TypeDescriptors.kvs;
import static org.apache.beam.sdk.values.TypeDescriptors.strings;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.flogger.FluentLogger;
import com.google.datastore.v1.Entity;
import java.util.Iterator;
import java.util.Map;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.extensions.gcp.options.GcpOptions;
import org.apache.beam.sdk.io.gcp.datastore.DatastoreIO;
import org.apache.beam.sdk.options.Default;
import org.apache.beam.sdk.options.Description;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.options.Validation;
import org.apache.beam.sdk.transforms.Create;
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.PTransform;
import org.apache.beam.sdk.transforms.ParDo;
import org.apache.beam.sdk.transforms.Reshuffle;
import org.apache.beam.sdk.transforms.View;
import org.apache.beam.sdk.values.KV;
import org.apache.beam.sdk.values.PBegin;
import org.apache.beam.sdk.values.PCollection;
import org.apache.beam.sdk.values.PCollectionTuple;
import org.apache.beam.sdk.values.PCollectionView;
import org.apache.beam.sdk.values.TupleTag;
import org.apache.beam.sdk.values.TupleTagList;
/**
* A BEAM pipeline that deletes Datastore entities in bulk.
*
* <p>This pipeline provides an alternative to the <a
* href="https://cloud.google.com/datastore/docs/bulk-delete">GCP builtin template</a> that performs
* the same task. It solves the following performance and usability problems in the builtin
* template:
*
* <ul>
* <li>When deleting all data (by using the {@code select __key__} or {@code select *} queries),
* the builtin template cannot parallelize the query, therefore has to query with a single
* worker.
* <li>When deleting all data, the builtin template also attempts to delete Datastore internal
* tables which would cause permission-denied errors, which in turn MAY cause the pipeline to
* abort before all data has been deleted.
* <li>With the builtin template, it is possible to delete multiple entity types in one pipeline
* ONLY if the user can come up with a single literal query that covers all of them. This is
* not the case with most Nomulus entity types.
* </ul>
*
* <p>A user of this pipeline must specify the types of entities to delete using the {@code
* --kindsToDelete} command line argument. To delete specific entity types, give a comma-separated
* string of their kind names; to delete all data, give {@code "*"}.
*
* <p>When deleting all data, it is recommended for the user to specify the number of user entity
* types in the Datastore using the {@code --numOfKindsHint} argument. If the default value for this
* parameter is too low, performance will suffer.
*/
public class BulkDeletePipeline {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
// This tool is not for use in our critical projects.
private static final ImmutableSet<String> FORBIDDEN_PROJECTS =
ImmutableSet.of("domain-registry", "domain-registry-sandbox");
private final BulkDeletePipelineOptions options;
private final Pipeline pipeline;
BulkDeletePipeline(BulkDeletePipelineOptions options) {
this.options = options;
pipeline = Pipeline.create(options);
}
public void run() {
setupPipeline();
pipeline.run();
}
@SuppressWarnings("deprecation") // org.apache.beam.sdk.transforms.Reshuffle
private void setupPipeline() {
checkState(
!FORBIDDEN_PROJECTS.contains(options.getProject()),
"Bulk delete is forbidden in %s",
options.getProject());
// Pre-allocated tags to label entities by kind. In the case of delete-all, we must use a guess.
TupleTagList deletionTags;
PCollection<String> kindsToDelete;
if (options.getKindsToDelete().equals("*")) {
deletionTags = getDeletionTags(options.getNumOfKindsHint());
kindsToDelete =
pipeline.apply("DiscoverEntityKinds", discoverEntityKinds(options.getProject()));
} else {
ImmutableList<String> kindsToDeleteParam = parseKindsToDelete(options);
checkState(
!kindsToDeleteParam.contains("*"),
"The --kindsToDelete argument should not contain both '*' and other kinds.");
deletionTags = getDeletionTags(kindsToDeleteParam.size());
kindsToDelete = pipeline.apply("UseProvidedKinds", Create.of(kindsToDeleteParam));
}
// Map each kind to a tag. The "SplitByKind" stage below will group entities by kind using
// this mapping. In practice, this has been effective at avoiding entity group contentions.
PCollectionView<Map<String, TupleTag<Entity>>> kindToTagMapping =
mapKindsToDeletionTags(kindsToDelete, deletionTags).apply("GetKindsToTagMap", View.asMap());
PCollectionTuple entities =
kindsToDelete
.apply("GenerateQueries", ParDo.of(new GenerateQueries()))
.apply("ReadEntities", DatastoreV1.read().withProjectId(options.getProject()))
.apply(
"SplitByKind",
ParDo.of(new SplitEntities(kindToTagMapping))
.withSideInputs(kindToTagMapping)
.withOutputTags(getOneDeletionTag("placeholder"), deletionTags));
for (TupleTag<?> tag : deletionTags.getAll()) {
entities
.get((TupleTag<Entity>) tag)
// Reshuffle calls GroupByKey which is one way to trigger load rebalance in the pipeline.
// Using the deprecated "Reshuffle" for convenience given the short life of this tool.
.apply("RebalanceLoad", Reshuffle.viaRandomKey())
.apply(
"DeleteEntities_" + tag.getId(),
DatastoreIO.v1().deleteEntity().withProjectId(options.getProject()));
}
}
private static String toKeyOnlyQueryForKind(String kind) {
return "select __key__ from `" + kind + "`";
}
/**
* Returns a {@link TupleTag} that retains the generic type parameter and may be used in a
* multi-output {@link ParDo} (e.g. {@link SplitEntities}).
*
* <p>This method is NOT needed in tests when creating tags for assertions. Simply create them
* with {@code new TupleTag<Entity>(String)}.
*/
@VisibleForTesting
static TupleTag<Entity> getOneDeletionTag(String id) {
// The trailing {} is needed to retain generic param type.
return new TupleTag<Entity>(id) {};
}
@VisibleForTesting
static ImmutableList<String> parseKindsToDelete(BulkDeletePipelineOptions options) {
return ImmutableList.copyOf(
Splitter.on(",").omitEmptyStrings().trimResults().split(options.getKindsToDelete().trim()));
}
/**
* Returns a list of {@code n} {@link TupleTag TupleTags} numbered from {@code 0} to {@code n-1}.
*/
@VisibleForTesting
static TupleTagList getDeletionTags(int n) {
ImmutableList.Builder<TupleTag<?>> builder = new ImmutableList.Builder<>();
for (int i = 0; i < n; i++) {
builder.add(getOneDeletionTag(String.valueOf(i)));
}
return TupleTagList.of(builder.build());
}
/** Returns a {@link PTransform} that finds all entity kinds in Datastore. */
@VisibleForTesting
static PTransform<PBegin, PCollection<String>> discoverEntityKinds(String project) {
return new PTransform<PBegin, PCollection<String>>() {
@Override
public PCollection<String> expand(PBegin input) {
// Use the __kind__ table to discover entity kinds. Data in the more informational
// __Stat_Kind__ table may be up to 48-hour stale.
return input
.apply(
"LoadEntityMetaData",
DatastoreIO.v1()
.read()
.withProjectId(project)
.withLiteralGqlQuery("select * from __kind__"))
.apply(
"GetKindNames",
ParDo.of(
new DoFn<Entity, String>() {
@ProcessElement
public void processElement(
@Element Entity entity, OutputReceiver<String> out) {
String kind = entity.getKey().getPath(0).getName();
if (kind.startsWith("_")) {
return;
}
out.output(kind);
}
}));
}
};
}
@VisibleForTesting
static PCollection<KV<String, TupleTag<Entity>>> mapKindsToDeletionTags(
PCollection<String> kinds, TupleTagList tags) {
// The first two stages send all strings in the 'kinds' PCollection to one worker which
// performs the mapping in the last stage.
return kinds
.apply(
"AssignSingletonKeyToKinds",
MapElements.into(kvs(strings(), strings())).via(kind -> KV.of("", kind)))
.apply("GatherKindsIntoCollection", GroupByKey.create())
.apply("MapKindsToTag", ParDo.of(new MapKindsToTags(tags)));
}
/** Transforms each {@code kind} string into a Datastore query for that kind. */
@VisibleForTesting
static class GenerateQueries extends DoFn<String, String> {
@ProcessElement
public void processElement(@Element String kind, OutputReceiver<String> out) {
out.output(toKeyOnlyQueryForKind(kind));
}
}
private static class MapKindsToTags
extends DoFn<KV<String, Iterable<String>>, KV<String, TupleTag<Entity>>> {
private final TupleTagList tupleTags;
MapKindsToTags(TupleTagList tupleTags) {
this.tupleTags = tupleTags;
}
@ProcessElement
public void processElement(
@Element KV<String, Iterable<String>> kv,
OutputReceiver<KV<String, TupleTag<Entity>>> out) {
// Sort kinds so that mapping is deterministic.
ImmutableSortedSet<String> sortedKinds = ImmutableSortedSet.copyOf(kv.getValue());
Iterator<String> kinds = sortedKinds.iterator();
Iterator<TupleTag<?>> tags = tupleTags.getAll().iterator();
while (kinds.hasNext() && tags.hasNext()) {
out.output(KV.of(kinds.next(), (TupleTag<Entity>) tags.next()));
}
if (kinds.hasNext()) {
logger.atWarning().log(
"There are more kinds to delete (%s) than our estimate (%s). "
+ "Performance may suffer.",
sortedKinds.size(), tupleTags.size());
}
// Round robin assignment so that mapping is deterministic
while (kinds.hasNext()) {
tags = tupleTags.getAll().iterator();
while (kinds.hasNext() && tags.hasNext()) {
out.output(KV.of(kinds.next(), (TupleTag<Entity>) tags.next()));
}
}
}
}
/**
* {@link DoFn} that splits one {@link PCollection} of mixed kinds into multiple single-kind
* {@code PCollections}.
*/
@VisibleForTesting
static class SplitEntities extends DoFn<Entity, Entity> {
private final PCollectionView<Map<String, TupleTag<Entity>>> kindToTagMapping;
SplitEntities(PCollectionView<Map<String, TupleTag<Entity>>> kindToTagMapping) {
super();
this.kindToTagMapping = kindToTagMapping;
}
@ProcessElement
public void processElement(ProcessContext context) {
Entity entity = context.element();
com.google.datastore.v1.Key entityKey = entity.getKey();
String kind = entityKey.getPath(entityKey.getPathCount() - 1).getKind();
TupleTag<Entity> tag = context.sideInput(kindToTagMapping).get(kind);
context.output(tag, entity);
}
}
public static void main(String[] args) {
BulkDeletePipelineOptions options =
PipelineOptionsFactory.fromArgs(args).withValidation().as(BulkDeletePipelineOptions.class);
BulkDeletePipeline pipeline = new BulkDeletePipeline(options);
pipeline.run();
System.exit(0);
}
public interface BulkDeletePipelineOptions extends GcpOptions {
@Description(
"The Datastore KINDs to be deleted. The format may be:\n"
+ "\t- The list of kinds to be deleted as a comma-separated string, or\n"
+ "\t- '*', which causes all kinds to be deleted.")
@Validation.Required
String getKindsToDelete();
void setKindsToDelete(String kinds);
@Description(
"An estimate of the number of KINDs to be deleted. "
+ "This is recommended if --kindsToDelete is '*' and the default value is too low.")
@Default.Integer(30)
int getNumOfKindsHint();
void setNumOfKindsHint(int numOfKindsHint);
}
}
@@ -0,0 +1,765 @@
// 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.
// This class is adapted from the Apache BEAM SDK. The original license may
// be found at <a href="https://github.com/apache/beam/blob/master/LICENSE">
// this link</a>.
package google.registry.beam.datastore;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Verify.verify;
import static com.google.datastore.v1.PropertyFilter.Operator.EQUAL;
import static com.google.datastore.v1.PropertyOrder.Direction.DESCENDING;
import static com.google.datastore.v1.QueryResultBatch.MoreResultsType.NOT_FINISHED;
import static com.google.datastore.v1.client.DatastoreHelper.makeAndFilter;
import static com.google.datastore.v1.client.DatastoreHelper.makeFilter;
import static com.google.datastore.v1.client.DatastoreHelper.makeOrder;
import static com.google.datastore.v1.client.DatastoreHelper.makeValue;
import com.google.api.client.http.HttpRequestInitializer;
import com.google.auth.Credentials;
import com.google.auth.http.HttpCredentialsAdapter;
import com.google.auto.value.AutoValue;
import com.google.cloud.hadoop.util.ChainingHttpRequestInitializer;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.datastore.v1.Entity;
import com.google.datastore.v1.EntityResult;
import com.google.datastore.v1.GqlQuery;
import com.google.datastore.v1.PartitionId;
import com.google.datastore.v1.Query;
import com.google.datastore.v1.QueryResultBatch;
import com.google.datastore.v1.RunQueryRequest;
import com.google.datastore.v1.RunQueryResponse;
import com.google.datastore.v1.client.Datastore;
import com.google.datastore.v1.client.DatastoreException;
import com.google.datastore.v1.client.DatastoreFactory;
import com.google.datastore.v1.client.DatastoreHelper;
import com.google.datastore.v1.client.DatastoreOptions;
import com.google.datastore.v1.client.QuerySplitter;
import com.google.protobuf.Int32Value;
import com.google.rpc.Code;
import java.io.Serializable;
import java.util.List;
import java.util.NoSuchElementException;
import javax.annotation.Nullable;
import org.apache.beam.sdk.extensions.gcp.options.GcpOptions;
import org.apache.beam.sdk.extensions.gcp.util.RetryHttpRequestInitializer;
import org.apache.beam.sdk.metrics.Counter;
import org.apache.beam.sdk.metrics.Metrics;
import org.apache.beam.sdk.options.PipelineOptions;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.PTransform;
import org.apache.beam.sdk.transforms.ParDo;
import org.apache.beam.sdk.transforms.Reshuffle;
import org.apache.beam.sdk.transforms.display.DisplayData;
import org.apache.beam.sdk.transforms.display.HasDisplayData;
import org.apache.beam.sdk.util.BackOff;
import org.apache.beam.sdk.util.BackOffUtils;
import org.apache.beam.sdk.util.FluentBackoff;
import org.apache.beam.sdk.util.Sleeper;
import org.apache.beam.sdk.values.KV;
import org.apache.beam.sdk.values.PCollection;
import org.joda.time.Duration;
/**
* Contains an adaptation of {@link org.apache.beam.sdk.io.gcp.datastore.DatastoreV1.Read}. See
* {@link MultiRead} for details.
*/
public class DatastoreV1 {
// A package-private constructor to prevent direct instantiation from outside of this package
DatastoreV1() {}
/**
* Non-retryable errors. See https://cloud.google.com/datastore/docs/concepts/errors#Error_Codes .
*/
private static final ImmutableSet<Code> NON_RETRYABLE_ERRORS =
ImmutableSet.of(
Code.FAILED_PRECONDITION,
Code.INVALID_ARGUMENT,
Code.PERMISSION_DENIED,
Code.UNAUTHENTICATED);
/**
* Returns an empty {@link MultiRead} builder. Configure the source {@code projectId}, {@code
* query}, and optionally {@code namespace} and {@code numQuerySplits} using {@link
* MultiRead#withProjectId}, {@link MultiRead#withNamespace}, {@link
* MultiRead#withNumQuerySplits}.
*/
public static MultiRead read() {
return new AutoValue_DatastoreV1_MultiRead.Builder().setNumQuerySplits(0).build();
}
/**
* A {@link PTransform} that executes every Cloud SQL queries in a {@link PCollection } and reads
* their result rows as {@code Entity} objects.
*
* <p>This class is adapted from {@link org.apache.beam.sdk.io.gcp.datastore.DatastoreV1.Read}. It
* uses literal GQL queries in the input {@link PCollection} instead of a constant query provided
* to the builder. Only the {@link #expand} method is modified from the original. Everything else
* including comments have been copied verbatim.
*/
@AutoValue
public abstract static class MultiRead
extends PTransform<PCollection<String>, PCollection<Entity>> {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/** An upper bound on the number of splits for a query. */
public static final int NUM_QUERY_SPLITS_MAX = 50000;
/** A lower bound on the number of splits for a query. */
static final int NUM_QUERY_SPLITS_MIN = 12;
/** Default bundle size of 64MB. */
static final long DEFAULT_BUNDLE_SIZE_BYTES = 64L * 1024L * 1024L;
/**
* Maximum number of results to request per query.
*
* <p>Must be set, or it may result in an I/O error when querying Cloud Datastore.
*/
static final int QUERY_BATCH_LIMIT = 500;
public abstract @Nullable String getProjectId();
public abstract @Nullable String getNamespace();
public abstract int getNumQuerySplits();
public abstract @Nullable String getLocalhost();
@Override
public abstract String toString();
abstract Builder toBuilder();
@AutoValue.Builder
abstract static class Builder {
abstract Builder setProjectId(String projectId);
abstract Builder setNamespace(String namespace);
abstract Builder setNumQuerySplits(int numQuerySplits);
abstract Builder setLocalhost(String localhost);
abstract MultiRead build();
}
/**
* Computes the number of splits to be performed on the given query by querying the estimated
* size from Cloud Datastore.
*/
static int getEstimatedNumSplits(Datastore datastore, Query query, @Nullable String namespace) {
int numSplits;
try {
long estimatedSizeBytes = getEstimatedSizeBytes(datastore, query, namespace);
logger.atInfo().log("Estimated size bytes for the query is: %s", estimatedSizeBytes);
numSplits =
(int)
Math.min(
NUM_QUERY_SPLITS_MAX,
Math.round(((double) estimatedSizeBytes) / DEFAULT_BUNDLE_SIZE_BYTES));
} catch (Exception e) {
logger.atWarning().log("Failed the fetch estimatedSizeBytes for query: %s", query, e);
// Fallback in case estimated size is unavailable.
numSplits = NUM_QUERY_SPLITS_MIN;
}
return Math.max(numSplits, NUM_QUERY_SPLITS_MIN);
}
/**
* Cloud Datastore system tables with statistics are periodically updated. This method fetches
* the latest timestamp (in microseconds) of statistics update using the {@code __Stat_Total__}
* table.
*/
private static long queryLatestStatisticsTimestamp(
Datastore datastore, @Nullable String namespace) throws DatastoreException {
Query.Builder query = Query.newBuilder();
// Note: namespace either being null or empty represents the default namespace, in which
// case we treat it as not provided by the user.
if (Strings.isNullOrEmpty(namespace)) {
query.addKindBuilder().setName("__Stat_Total__");
} else {
query.addKindBuilder().setName("__Stat_Ns_Total__");
}
query.addOrder(makeOrder("timestamp", DESCENDING));
query.setLimit(Int32Value.newBuilder().setValue(1));
RunQueryRequest request = makeRequest(query.build(), namespace);
RunQueryResponse response = datastore.runQuery(request);
QueryResultBatch batch = response.getBatch();
if (batch.getEntityResultsCount() == 0) {
throw new NoSuchElementException("Datastore total statistics unavailable");
}
Entity entity = batch.getEntityResults(0).getEntity();
return entity.getProperties().get("timestamp").getTimestampValue().getSeconds() * 1000000;
}
/** Retrieve latest table statistics for a given kind, namespace, and datastore. */
private static Entity getLatestTableStats(
String ourKind, @Nullable String namespace, Datastore datastore) throws DatastoreException {
long latestTimestamp = queryLatestStatisticsTimestamp(datastore, namespace);
logger.atInfo().log("Latest stats timestamp for kind %s is %s", ourKind, latestTimestamp);
Query.Builder queryBuilder = Query.newBuilder();
if (Strings.isNullOrEmpty(namespace)) {
queryBuilder.addKindBuilder().setName("__Stat_Kind__");
} else {
queryBuilder.addKindBuilder().setName("__Stat_Ns_Kind__");
}
queryBuilder.setFilter(
makeAndFilter(
makeFilter("kind_name", EQUAL, makeValue(ourKind).build()).build(),
makeFilter("timestamp", EQUAL, makeValue(latestTimestamp).build()).build()));
RunQueryRequest request = makeRequest(queryBuilder.build(), namespace);
long now = System.currentTimeMillis();
RunQueryResponse response = datastore.runQuery(request);
logger.atFine().log(
"Query for per-kind statistics took %sms", System.currentTimeMillis() - now);
QueryResultBatch batch = response.getBatch();
if (batch.getEntityResultsCount() == 0) {
throw new NoSuchElementException(
"Datastore statistics for kind " + ourKind + " unavailable");
}
return batch.getEntityResults(0).getEntity();
}
/**
* Get the estimated size of the data returned by the given query.
*
* <p>Cloud Datastore provides no way to get a good estimate of how large the result of a query
* entity kind being queried, using the __Stat_Kind__ system table, assuming exactly 1 kind is
* specified in the query.
*
* <p>See https://cloud.google.com/datastore/docs/concepts/stats.
*/
static long getEstimatedSizeBytes(Datastore datastore, Query query, @Nullable String namespace)
throws DatastoreException {
String ourKind = query.getKind(0).getName();
Entity entity = getLatestTableStats(ourKind, namespace, datastore);
return entity.getProperties().get("entity_bytes").getIntegerValue();
}
private static PartitionId.Builder forNamespace(@Nullable String namespace) {
PartitionId.Builder partitionBuilder = PartitionId.newBuilder();
// Namespace either being null or empty represents the default namespace.
// Datastore Client libraries expect users to not set the namespace proto field in
// either of these cases.
if (!Strings.isNullOrEmpty(namespace)) {
partitionBuilder.setNamespaceId(namespace);
}
return partitionBuilder;
}
/** Builds a {@link RunQueryRequest} from the {@code query} and {@code namespace}. */
static RunQueryRequest makeRequest(Query query, @Nullable String namespace) {
return RunQueryRequest.newBuilder()
.setQuery(query)
.setPartitionId(forNamespace(namespace))
.build();
}
/** Builds a {@link RunQueryRequest} from the {@code GqlQuery} and {@code namespace}. */
private static RunQueryRequest makeRequest(GqlQuery gqlQuery, @Nullable String namespace) {
return RunQueryRequest.newBuilder()
.setGqlQuery(gqlQuery)
.setPartitionId(forNamespace(namespace))
.build();
}
/**
* A helper function to get the split queries, taking into account the optional {@code
* namespace}.
*/
private static List<Query> splitQuery(
Query query,
@Nullable String namespace,
Datastore datastore,
QuerySplitter querySplitter,
int numSplits)
throws DatastoreException {
// If namespace is set, include it in the split request so splits are calculated accordingly.
return querySplitter.getSplits(query, forNamespace(namespace).build(), numSplits, datastore);
}
/**
* Translates a Cloud Datastore gql query string to {@link Query}.
*
* <p>Currently, the only way to translate a gql query string to a Query is to run the query
* against Cloud Datastore and extract the {@code Query} from the response. To prevent reading
* any data, we set the {@code LIMIT} to 0 but if the gql query already has a limit set, we
* catch the exception with {@code INVALID_ARGUMENT} error code and retry the translation
* without the zero limit.
*
* <p>Note: This may result in reading actual data from Cloud Datastore but the service has a
* cap on the number of entities returned for a single rpc request, so this should not be a
* problem in practice.
*/
private static Query translateGqlQueryWithLimitCheck(
String gql, Datastore datastore, String namespace) throws DatastoreException {
String gqlQueryWithZeroLimit = gql + " LIMIT 0";
try {
Query translatedQuery = translateGqlQuery(gqlQueryWithZeroLimit, datastore, namespace);
// Clear the limit that we set.
return translatedQuery.toBuilder().clearLimit().build();
} catch (DatastoreException e) {
// Note: There is no specific error code or message to detect if the query already has a
// limit, so we just check for INVALID_ARGUMENT and assume that that the query might have
// a limit already set.
if (e.getCode() == Code.INVALID_ARGUMENT) {
logger.atWarning().log(
"Failed to translate Gql query '%s': %s", gqlQueryWithZeroLimit, e.getMessage());
logger.atWarning().log(
"User query might have a limit already set, so trying without zero limit");
// Retry without the zero limit.
return translateGqlQuery(gql, datastore, namespace);
} else {
throw e;
}
}
}
/** Translates a gql query string to {@link Query}. */
private static Query translateGqlQuery(String gql, Datastore datastore, String namespace)
throws DatastoreException {
logger.atInfo().log("Translating gql %s", gql);
GqlQuery gqlQuery = GqlQuery.newBuilder().setQueryString(gql).setAllowLiterals(true).build();
RunQueryRequest req = makeRequest(gqlQuery, namespace);
return datastore.runQuery(req).getQuery();
}
/**
* Returns a new {@link MultiRead} that reads from the Cloud Datastore for the specified
* project.
*/
public MultiRead withProjectId(String projectId) {
checkArgument(projectId != null, "projectId can not be null");
return toBuilder().setProjectId(projectId).build();
}
/** Returns a new {@link MultiRead} that reads from the given namespace. */
public MultiRead withNamespace(String namespace) {
return toBuilder().setNamespace(namespace).build();
}
/**
* Returns a new {@link MultiRead} that reads by splitting the given {@code query} into {@code
* numQuerySplits}.
*
* <p>The semantics for the query splitting is defined below:
*
* <ul>
* <li>Any value less than or equal to 0 will be ignored, and the number of splits will be
* chosen dynamically at runtime based on the query data size.
* <li>Any value greater than {@link MultiRead#NUM_QUERY_SPLITS_MAX} will be capped at {@code
* NUM_QUERY_SPLITS_MAX}.
* <li>If the {@code query} has a user limit set, then {@code numQuerySplits} will be ignored
* and no split will be performed.
* <li>Under certain cases Cloud Datastore is unable to split query to the requested number of
* splits. In such cases we just use whatever the Cloud Datastore returns.
* </ul>
*/
public MultiRead withNumQuerySplits(int numQuerySplits) {
return toBuilder()
.setNumQuerySplits(Math.min(Math.max(numQuerySplits, 0), NUM_QUERY_SPLITS_MAX))
.build();
}
/**
* Returns a new {@link MultiRead} that reads from a Datastore Emulator running at the given
* localhost address.
*/
public MultiRead withLocalhost(String localhost) {
return toBuilder().setLocalhost(localhost).build();
}
/** Returns Number of entities available for reading. */
public long getNumEntities(
PipelineOptions options, String ourKind, @Nullable String namespace) {
try {
V1Options v1Options = V1Options.from(getProjectId(), getNamespace(), getLocalhost());
V1DatastoreFactory datastoreFactory = new V1DatastoreFactory();
Datastore datastore =
datastoreFactory.getDatastore(
options, v1Options.getProjectId(), v1Options.getLocalhost());
Entity entity = getLatestTableStats(ourKind, namespace, datastore);
return entity.getProperties().get("count").getIntegerValue();
} catch (Exception e) {
return -1;
}
}
@Override
public PCollection<Entity> expand(PCollection<String> gqlQueries) {
checkArgument(getProjectId() != null, "projectId cannot be null");
V1Options v1Options = V1Options.from(getProjectId(), getNamespace(), getLocalhost());
/*
* This composite transform involves the following steps:
* 1. Apply a {@link ParDo} that translates each query in {@code gqlQueries} into a {@code
* query}.
*
* 2. A {@link ParDo} splits the resulting query into {@code numQuerySplits} and
* assign each split query a unique {@code Integer} as the key. The resulting output is
* of the type {@code PCollection<KV<Integer, Query>>}.
*
* If the value of {@code numQuerySplits} is less than or equal to 0, then the number of
* splits will be computed dynamically based on the size of the data for the {@code query}.
*
* 3. The resulting {@code PCollection} is sharded using a {@link GroupByKey} operation. The
* queries are extracted from they {@code KV<Integer, Iterable<Query>>} and flattened to
* output a {@code PCollection<Query>}.
*
* 4. In the third step, a {@code ParDo} reads entities for each query and outputs
* a {@code PCollection<Entity>}.
*/
PCollection<Query> inputQuery =
gqlQueries.apply(ParDo.of(new GqlQueryTranslateFn(v1Options)));
return inputQuery
.apply("Split", ParDo.of(new SplitQueryFn(v1Options, getNumQuerySplits())))
.apply("Reshuffle", Reshuffle.viaRandomKey())
.apply("Read", ParDo.of(new ReadFn(v1Options)));
}
@Override
public void populateDisplayData(DisplayData.Builder builder) {
super.populateDisplayData(builder);
builder
.addIfNotNull(DisplayData.item("projectId", getProjectId()).withLabel("ProjectId"))
.addIfNotNull(DisplayData.item("namespace", getNamespace()).withLabel("Namespace"));
}
private static class V1Options implements HasDisplayData, Serializable {
private final String project;
private final @Nullable String namespace;
private final @Nullable String localhost;
private V1Options(String project, @Nullable String namespace, @Nullable String localhost) {
this.project = project;
this.namespace = namespace;
this.localhost = localhost;
}
public static V1Options from(
String projectId, @Nullable String namespace, @Nullable String localhost) {
return new V1Options(projectId, namespace, localhost);
}
public String getProjectId() {
return project;
}
public @Nullable String getNamespace() {
return namespace;
}
public @Nullable String getLocalhost() {
return localhost;
}
@Override
public void populateDisplayData(DisplayData.Builder builder) {
builder
.addIfNotNull(DisplayData.item("projectId", getProjectId()).withLabel("ProjectId"))
.addIfNotNull(DisplayData.item("namespace", getNamespace()).withLabel("Namespace"));
}
}
/** A DoFn that translates a Cloud Datastore gql query string to {@code Query}. */
static class GqlQueryTranslateFn extends DoFn<String, Query> {
private final V1Options v1Options;
private transient Datastore datastore;
private final V1DatastoreFactory datastoreFactory;
GqlQueryTranslateFn(V1Options options) {
this(options, new V1DatastoreFactory());
}
GqlQueryTranslateFn(V1Options options, V1DatastoreFactory datastoreFactory) {
this.v1Options = options;
this.datastoreFactory = datastoreFactory;
}
@StartBundle
public void startBundle(StartBundleContext c) throws Exception {
datastore =
datastoreFactory.getDatastore(
c.getPipelineOptions(), v1Options.getProjectId(), v1Options.getLocalhost());
}
@ProcessElement
public void processElement(ProcessContext c) throws Exception {
String gqlQuery = c.element();
logger.atInfo().log("User query: '%s'", gqlQuery);
Query query =
translateGqlQueryWithLimitCheck(gqlQuery, datastore, v1Options.getNamespace());
logger.atInfo().log("User gql query translated to Query(%s)", query);
c.output(query);
}
}
/**
* A {@link DoFn} that splits a given query into multiple sub-queries, assigns them unique keys
* and outputs them as {@link KV}.
*/
private static class SplitQueryFn extends DoFn<Query, Query> {
private final V1Options options;
// number of splits to make for a given query
private final int numSplits;
private final V1DatastoreFactory datastoreFactory;
// Datastore client
private transient Datastore datastore;
// Query splitter
private transient QuerySplitter querySplitter;
public SplitQueryFn(V1Options options, int numSplits) {
this(options, numSplits, new V1DatastoreFactory());
}
private SplitQueryFn(V1Options options, int numSplits, V1DatastoreFactory datastoreFactory) {
this.options = options;
this.numSplits = numSplits;
this.datastoreFactory = datastoreFactory;
}
@StartBundle
public void startBundle(StartBundleContext c) throws Exception {
datastore =
datastoreFactory.getDatastore(
c.getPipelineOptions(), options.getProjectId(), options.getLocalhost());
querySplitter = datastoreFactory.getQuerySplitter();
}
@ProcessElement
public void processElement(ProcessContext c) throws Exception {
Query query = c.element();
// If query has a user set limit, then do not split.
if (query.hasLimit()) {
c.output(query);
return;
}
int estimatedNumSplits;
// Compute the estimated numSplits if numSplits is not specified by the user.
if (numSplits <= 0) {
estimatedNumSplits = getEstimatedNumSplits(datastore, query, options.getNamespace());
} else {
estimatedNumSplits = numSplits;
}
logger.atInfo().log("Splitting the query into %s splits", estimatedNumSplits);
List<Query> querySplits;
try {
querySplits =
splitQuery(
query, options.getNamespace(), datastore, querySplitter, estimatedNumSplits);
} catch (Exception e) {
logger.atWarning().log("Unable to parallelize the given query: %s", query, e);
querySplits = ImmutableList.of(query);
}
// assign unique keys to query splits.
for (Query subquery : querySplits) {
c.output(subquery);
}
}
@Override
public void populateDisplayData(DisplayData.Builder builder) {
super.populateDisplayData(builder);
builder.include("options", options);
if (numSplits > 0) {
builder.add(
DisplayData.item("numQuerySplits", numSplits)
.withLabel("Requested number of Query splits"));
}
}
}
/** A {@link DoFn} that reads entities from Cloud Datastore for each query. */
private static class ReadFn extends DoFn<Query, Entity> {
private final V1Options options;
private final V1DatastoreFactory datastoreFactory;
// Datastore client
private transient Datastore datastore;
private final Counter rpcErrors = Metrics.counter(ReadFn.class, "datastoreRpcErrors");
private final Counter rpcSuccesses = Metrics.counter(ReadFn.class, "datastoreRpcSuccesses");
private static final int MAX_RETRIES = 5;
private static final FluentBackoff RUNQUERY_BACKOFF =
FluentBackoff.DEFAULT
.withMaxRetries(MAX_RETRIES)
.withInitialBackoff(Duration.standardSeconds(5));
public ReadFn(V1Options options) {
this(options, new V1DatastoreFactory());
}
private ReadFn(V1Options options, V1DatastoreFactory datastoreFactory) {
this.options = options;
this.datastoreFactory = datastoreFactory;
}
@StartBundle
public void startBundle(StartBundleContext c) throws Exception {
datastore =
datastoreFactory.getDatastore(
c.getPipelineOptions(), options.getProjectId(), options.getLocalhost());
}
private RunQueryResponse runQueryWithRetries(RunQueryRequest request) throws Exception {
Sleeper sleeper = Sleeper.DEFAULT;
BackOff backoff = RUNQUERY_BACKOFF.backoff();
while (true) {
try {
RunQueryResponse response = datastore.runQuery(request);
rpcSuccesses.inc();
return response;
} catch (DatastoreException exception) {
rpcErrors.inc();
if (NON_RETRYABLE_ERRORS.contains(exception.getCode())) {
throw exception;
}
if (!BackOffUtils.next(sleeper, backoff)) {
logger.atSevere().log("Aborting after %s retries.", MAX_RETRIES);
throw exception;
}
}
}
}
/** Read and output entities for the given query. */
@ProcessElement
public void processElement(ProcessContext context) throws Exception {
Query query = context.element();
String namespace = options.getNamespace();
int userLimit = query.hasLimit() ? query.getLimit().getValue() : Integer.MAX_VALUE;
boolean moreResults = true;
QueryResultBatch currentBatch = null;
while (moreResults) {
Query.Builder queryBuilder = query.toBuilder();
queryBuilder.setLimit(
Int32Value.newBuilder().setValue(Math.min(userLimit, QUERY_BATCH_LIMIT)));
if (currentBatch != null && !currentBatch.getEndCursor().isEmpty()) {
queryBuilder.setStartCursor(currentBatch.getEndCursor());
}
RunQueryRequest request = makeRequest(queryBuilder.build(), namespace);
RunQueryResponse response = runQueryWithRetries(request);
currentBatch = response.getBatch();
// MORE_RESULTS_AFTER_LIMIT is not implemented yet:
// https://groups.google.com/forum/#!topic/gcd-discuss/iNs6M1jA2Vw, so
// use result count to determine if more results might exist.
int numFetch = currentBatch.getEntityResultsCount();
if (query.hasLimit()) {
verify(
userLimit >= numFetch,
"Expected userLimit %s >= numFetch %s, because query limit %s must be <= userLimit",
userLimit,
numFetch,
query.getLimit());
userLimit -= numFetch;
}
// output all the entities from the current batch.
for (EntityResult entityResult : currentBatch.getEntityResultsList()) {
context.output(entityResult.getEntity());
}
// Check if we have more entities to be read.
moreResults =
// User-limit does not exist (so userLimit == MAX_VALUE) and/or has not been satisfied
(userLimit > 0)
// All indications from the API are that there are/may be more results.
&& ((numFetch == QUERY_BATCH_LIMIT)
|| (currentBatch.getMoreResults() == NOT_FINISHED));
}
}
@Override
public void populateDisplayData(DisplayData.Builder builder) {
super.populateDisplayData(builder);
builder.include("options", options);
}
}
}
/**
* A wrapper factory class for Cloud Datastore singleton classes {@link DatastoreFactory} and
* {@link QuerySplitter}
*
* <p>{@link DatastoreFactory} and {@link QuerySplitter} are not java serializable, hence wrapping
* them under this class, which implements {@link Serializable}.
*/
private static class V1DatastoreFactory implements Serializable {
/** Builds a Cloud Datastore client for the given pipeline options and project. */
public Datastore getDatastore(PipelineOptions pipelineOptions, String projectId) {
return getDatastore(pipelineOptions, projectId, null);
}
/**
* Builds a Cloud Datastore client for the given pipeline options, project and an optional
* locahost.
*/
public Datastore getDatastore(
PipelineOptions pipelineOptions, String projectId, @Nullable String localhost) {
Credentials credential = pipelineOptions.as(GcpOptions.class).getGcpCredential();
HttpRequestInitializer initializer;
if (credential != null) {
initializer =
new ChainingHttpRequestInitializer(
new HttpCredentialsAdapter(credential), new RetryHttpRequestInitializer());
} else {
initializer = new RetryHttpRequestInitializer();
}
DatastoreOptions.Builder builder =
new DatastoreOptions.Builder().projectId(projectId).initializer(initializer);
if (localhost != null) {
builder.localHost(localhost);
} else {
builder.host("batch-datastore.googleapis.com");
}
return DatastoreFactory.get().create(builder.build());
}
/** Builds a Cloud Datastore {@link QuerySplitter}. */
public QuerySplitter getQuerySplitter() {
return DatastoreHelper.getQuerySplitter();
}
}
}
@@ -25,11 +25,6 @@
<property name="tld" direction="asc"/>
<property name="creationTime" direction="desc"/>
</datastore-index>
<!-- For finding non-autorenewing domains to be deleted. -->
<datastore-index kind="DomainBase" ancestor="false" source="manual">
<property name="autorenewEndTime" direction="asc"/>
<property name="deletionTime" direction="asc"/>
</datastore-index>
<!-- For finding host resources by registrar. -->
<datastore-index kind="HostResource" ancestor="false" source="manual">
<property name="currentSponsorClientId" direction="asc"/>
@@ -22,5 +22,6 @@ public enum EppRequestSource {
TLS,
TOOL,
CHECK_API,
UNIT_TEST
UNIT_TEST,
BACKEND
}
@@ -104,11 +104,14 @@ public final class ExtensionManager {
clientId, flowClass.getSimpleName(), undeclaredUris);
}
private static final ImmutableSet<EppRequestSource> ALLOWED_METADATA_EPP_REQUEST_SOURCES =
ImmutableSet.of(EppRequestSource.TOOL, EppRequestSource.BACKEND);
private void checkForRestrictedExtensions(
ImmutableSet<Class<? extends CommandExtension>> suppliedExtensions)
throws OnlyToolCanPassMetadataException, UnauthorizedForSuperuserExtensionException {
if (suppliedExtensions.contains(MetadataExtension.class)
&& !eppRequestSource.equals(EppRequestSource.TOOL)) {
&& !ALLOWED_METADATA_EPP_REQUEST_SOURCES.contains(eppRequestSource)) {
throw new OnlyToolCanPassMetadataException();
}
// Can't use suppliedExtension.contains() here because the SuperuserExtension has child classes.
@@ -16,6 +16,7 @@ package google.registry.flows;
import static com.google.common.base.MoreObjects.toStringHelper;
import static google.registry.request.RequestParameters.extractOptionalHeader;
import static google.registry.util.X509Utils.loadCertificate;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
@@ -25,11 +26,18 @@ import com.google.common.net.InetAddresses;
import dagger.Module;
import dagger.Provides;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryEnvironment;
import google.registry.flows.EppException.AuthenticationErrorException;
import google.registry.flows.certs.CertificateChecker;
import google.registry.flows.certs.CertificateChecker.InsecureCertificateException;
import google.registry.model.registrar.Registrar;
import google.registry.request.Header;
import google.registry.util.CidrAddressBlock;
import java.io.ByteArrayInputStream;
import java.net.InetAddress;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.Optional;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
@@ -42,6 +50,9 @@ import javax.servlet.http.HttpServletRequest;
* <dl>
* <dt>X-SSL-Certificate
* <dd>This field should contain a base64 encoded digest of the client's TLS certificate. It is
* used only if the validation of the full certificate fails.
* <dt>X-SSL-Full-Certificate
* <dd>This field should contain a base64 encoding of the client's TLS certificate. It is
* validated during an EPP login command against a known good value that is transmitted out of
* band.
* <dt>X-Forwarded-For
@@ -55,16 +66,22 @@ public class TlsCredentials implements TransportCredentials {
private final boolean requireSslCertificates;
private final Optional<String> clientCertificateHash;
private final Optional<String> clientCertificate;
private final Optional<InetAddress> clientInetAddr;
private final CertificateChecker certificateChecker;
@Inject
public TlsCredentials(
@Config("requireSslCertificates") boolean requireSslCertificates,
@Header("X-SSL-Certificate") Optional<String> clientCertificateHash,
@Header("X-Forwarded-For") Optional<String> clientAddress) {
@Header("X-SSL-Full-Certificate") Optional<String> clientCertificate,
@Header("X-Forwarded-For") Optional<String> clientAddress,
CertificateChecker certificateChecker) {
this.requireSslCertificates = requireSslCertificates;
this.clientCertificateHash = clientCertificateHash;
this.clientCertificate = clientCertificate;
this.clientInetAddr = clientAddress.map(TlsCredentials::parseInetAddress);
this.certificateChecker = certificateChecker;
}
static InetAddress parseInetAddress(String asciiAddr) {
@@ -91,7 +108,7 @@ public class TlsCredentials implements TransportCredentials {
ImmutableList<CidrAddressBlock> ipAddressAllowList = registrar.getIpAddressAllowList();
if (ipAddressAllowList.isEmpty()) {
logger.atInfo().log(
"Skipping IP allow list check because %s doesn't have an IP allow list",
"Skipping IP allow list check because %s doesn't have an IP allow list.",
registrar.getClientId());
return;
}
@@ -115,11 +132,80 @@ public class TlsCredentials implements TransportCredentials {
/**
* Verifies client SSL certificate is permitted to issue commands as {@code registrar}.
*
* @throws MissingRegistrarCertificateException if frontend didn't send certificate hash header
* @throws MissingRegistrarCertificateException if frontend didn't send certificate header
* @throws BadRegistrarCertificateException if registrar requires certificate and it didn't match
*/
@VisibleForTesting
void validateCertificate(Registrar registrar) throws AuthenticationErrorException {
// Check that certificate is present in registrar object
if (!registrar.getClientCertificate().isPresent()
&& !registrar.getFailoverClientCertificate().isPresent()) {
// Log an error and validate using certificate hash instead
// TODO(sarahbot): throw a RegistrarCertificateNotConfiguredException once hash is no longer
// used as failover
logger.atWarning().log(
"There is no certificate configured for registrar %s.", registrar.getClientId());
} else if (!clientCertificate.isPresent()) {
// Check that the request included the full certificate
// Log an error and validate using certificate hash instead
// TODO(sarahbot): throw a MissingRegistrarCertificateException once hash is no longer used as
// failover
logger.atWarning().log(
"Request from registrar %s did not include X-SSL-Full-Certificate.",
registrar.getClientId());
} else {
X509Certificate passedCert;
Optional<X509Certificate> storedCert;
Optional<X509Certificate> storedFailoverCert;
try {
storedCert = deserializePemCert(registrar.getClientCertificate());
storedFailoverCert = deserializePemCert(registrar.getFailoverClientCertificate());
passedCert = decodeCertString(clientCertificate.get());
} catch (Exception e) {
// TODO(Sarahbot@): remove this catch once we know it's working
logger.atWarning().log(
"Error converting certificate string to certificate for %s: %s",
registrar.getClientId(), e);
validateCertificateHash(registrar);
return;
}
// Check if the certificate is equal to the one on file for the registrar.
if (passedCert.equals(storedCert.orElse(null))
|| passedCert.equals(storedFailoverCert.orElse(null))) {
// Check certificate for any requirement violations
// TODO(Sarahbot@): Throw exceptions instead of just logging once requirement enforcement
// begins
try {
certificateChecker.validateCertificate(passedCert);
} catch (InsecureCertificateException e) {
// throw exception in unit tests and Sandbox
if (RegistryEnvironment.get().equals(RegistryEnvironment.UNITTEST)
|| RegistryEnvironment.get().equals(RegistryEnvironment.SANDBOX)) {
throw new CertificateContainsSecurityViolationsException(e);
}
logger.atWarning().log(
"Registrar certificate used for %s does not meet certificate requirements: %s",
registrar.getClientId(), e.getMessage());
} catch (Exception e) {
logger.atWarning().log(
"Error validating certificate for %s: %s", registrar.getClientId(), e);
}
// successfully validated, return here since hash validation is not necessary
return;
}
// Log an error and validate using certificate hash instead
// TODO(sarahbot): throw a BadRegistrarCertificateException once hash is no longer used as
// failover
logger.atWarning().log("Non-matching certificate for registrar %s.", registrar.getClientId());
}
validateCertificateHash(registrar);
}
private void validateCertificateHash(Registrar registrar) throws AuthenticationErrorException {
// Check the certificate hash as a failover
// TODO(sarahbot): Remove hash checks once certificate checks are working.
if (!registrar.getClientCertificateHash().isPresent()
&& !registrar.getFailoverClientCertificateHash().isPresent()) {
if (requireSslCertificates) {
@@ -130,14 +216,17 @@ public class TlsCredentials implements TransportCredentials {
return;
}
}
// Check that the request included the certificate hash
if (!clientCertificateHash.isPresent()) {
logger.atInfo().log("Request did not include X-SSL-Certificate");
logger.atInfo().log(
"Request from registrar %s did not include X-SSL-Certificate.", registrar.getClientId());
throw new MissingRegistrarCertificateException();
}
// Check if the certificate hash is equal to the one on file for the registrar.
if (!clientCertificateHash.equals(registrar.getClientCertificateHash())
&& !clientCertificateHash.equals(registrar.getFailoverClientCertificateHash())) {
logger.atWarning().log(
"bad certificate hash (%s) for %s, wanted either %s or %s",
"Non-matching certificate hash (%s) for %s, wanted either %s or %s.",
clientCertificateHash,
registrar.getClientId(),
registrar.getClientCertificateHash(),
@@ -153,9 +242,26 @@ public class TlsCredentials implements TransportCredentials {
}
}
// Converts a PEM formatted certificate string into an X509Certificate
private Optional<X509Certificate> deserializePemCert(Optional<String> certificateString)
throws CertificateException {
if (certificateString.isPresent()) {
return Optional.of(loadCertificate(certificateString.get()));
}
return Optional.empty();
}
// Decodes the string representation of an encoded certificate back into an X509Certificate
private X509Certificate decodeCertString(String encodedCertString) throws CertificateException {
byte decodedCert[] = Base64.getDecoder().decode(encodedCertString);
ByteArrayInputStream inputStream = new ByteArrayInputStream(decodedCert);
return loadCertificate(inputStream);
}
@Override
public String toString() {
return toStringHelper(getClass())
.add("clientCertificate", clientCertificate.orElse(null))
.add("clientCertificateHash", clientCertificateHash.orElse(null))
.add("clientAddress", clientInetAddr.orElse(null))
.toString();
@@ -168,6 +274,20 @@ public class TlsCredentials implements TransportCredentials {
}
}
/** Registrar certificate contains the following security violations: ... */
public static class CertificateContainsSecurityViolationsException
extends AuthenticationErrorException {
InsecureCertificateException exception;
CertificateContainsSecurityViolationsException(InsecureCertificateException exception) {
super(
String.format(
"Registrar certificate contains the following security violations:\n%s",
exception.getMessage()));
this.exception = exception;
}
}
/** Registrar certificate not present. */
public static class MissingRegistrarCertificateException extends AuthenticationErrorException {
MissingRegistrarCertificateException() {
@@ -202,6 +322,14 @@ public class TlsCredentials implements TransportCredentials {
return extractOptionalHeader(req, "X-SSL-Certificate");
}
@Provides
@Header("X-SSL-Full-Certificate")
static Optional<String> provideClientCertificate(HttpServletRequest req) {
// Note: This header is actually required, we just want to handle its absence explicitly
// by throwing an EPP exception rather than a generic Bad Request exception.
return extractOptionalHeader(req, "X-SSL-Full-Certificate");
}
@Provides
@Header("X-Forwarded-For")
static Optional<String> provideForwardedFor(HttpServletRequest req) {
@@ -89,14 +89,26 @@ public class CertificateChecker {
* Checks the given certificate string for violations and throws an exception if any violations
* exist.
*/
public void validateCertificate(String certificateString) {
ImmutableSet<CertificateViolation> violations = checkCertificate(certificateString);
public void validateCertificate(String certificateString) throws InsecureCertificateException {
handleCertViolations(checkCertificate(certificateString));
}
/**
* Checks the given certificate string for violations and throws an exception if any violations
* exist.
*/
public void validateCertificate(X509Certificate certificate) throws InsecureCertificateException {
handleCertViolations(checkCertificate(certificate));
}
private void handleCertViolations(ImmutableSet<CertificateViolation> violations)
throws InsecureCertificateException {
if (!violations.isEmpty()) {
String displayMessages =
violations.stream()
.map(violation -> getViolationDisplayMessage(violation))
.collect(Collectors.joining("\n"));
throw new IllegalArgumentException(displayMessages);
throw new InsecureCertificateException(violations, displayMessages);
}
}
@@ -258,4 +270,14 @@ public class CertificateChecker {
return certificateChecker.getViolationDisplayMessage(this);
}
}
/** Exception to throw when a certificate has security violations. */
public static class InsecureCertificateException extends Exception {
ImmutableSet<CertificateViolation> violations;
InsecureCertificateException(ImmutableSet<CertificateViolation> violations, String message) {
super(message);
this.violations = violations;
}
}
}
@@ -22,9 +22,9 @@ import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
import static google.registry.flows.contact.ContactFlowUtils.createGainingTransferPollMessage;
import static google.registry.flows.contact.ContactFlowUtils.createTransferResponse;
import static google.registry.model.ResourceTransferUtils.approvePendingTransfer;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.Key;
import google.registry.flows.EppException;
import google.registry.flows.ExtensionManager;
@@ -94,7 +94,8 @@ public final class ContactTransferApproveFlow implements TransactionalFlow {
// Create a poll message for the gaining client.
PollMessage gainingPollMessage =
createGainingTransferPollMessage(targetId, newContact.getTransferData(), historyEntry);
ofy().save().<Object>entities(newContact, historyEntry, gainingPollMessage);
tm().insertAll(ImmutableSet.of(historyEntry.toChildHistoryEntity(), gainingPollMessage));
tm().update(newContact);
// Delete the billing event and poll messages that were written in case the transfer would have
// been implicitly server approved.
tm().delete(existingContact.getTransferData().getServerApproveEntities());
@@ -22,9 +22,9 @@ import static google.registry.flows.ResourceFlowUtils.verifyTransferInitiator;
import static google.registry.flows.contact.ContactFlowUtils.createLosingTransferPollMessage;
import static google.registry.flows.contact.ContactFlowUtils.createTransferResponse;
import static google.registry.model.ResourceTransferUtils.denyPendingTransfer;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.Key;
import google.registry.flows.EppException;
import google.registry.flows.ExtensionManager;
@@ -90,7 +90,8 @@ public final class ContactTransferCancelFlow implements TransactionalFlow {
// Create a poll message for the losing client.
PollMessage losingPollMessage =
createLosingTransferPollMessage(targetId, newContact.getTransferData(), historyEntry);
ofy().save().<Object>entities(newContact, historyEntry, losingPollMessage);
tm().insertAll(ImmutableSet.of(historyEntry.toChildHistoryEntity(), losingPollMessage));
tm().update(newContact);
// Delete the billing event and poll messages that were written in case the transfer would have
// been implicitly server approved.
tm().delete(existingContact.getTransferData().getServerApproveEntities());
@@ -22,9 +22,9 @@ import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
import static google.registry.flows.contact.ContactFlowUtils.createGainingTransferPollMessage;
import static google.registry.flows.contact.ContactFlowUtils.createTransferResponse;
import static google.registry.model.ResourceTransferUtils.denyPendingTransfer;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.Key;
import google.registry.flows.EppException;
import google.registry.flows.ExtensionManager;
@@ -87,7 +87,8 @@ public final class ContactTransferRejectFlow implements TransactionalFlow {
.build();
PollMessage gainingPollMessage =
createGainingTransferPollMessage(targetId, newContact.getTransferData(), historyEntry);
ofy().save().<Object>entities(newContact, historyEntry, gainingPollMessage);
tm().insertAll(ImmutableSet.of(historyEntry.toChildHistoryEntity(), gainingPollMessage));
tm().update(newContact);
// Delete the billing event and poll messages that were written in case the transfer would have
// been implicitly server approved.
tm().delete(existingContact.getTransferData().getServerApproveEntities());
@@ -23,7 +23,6 @@ import static google.registry.flows.contact.ContactFlowUtils.createGainingTransf
import static google.registry.flows.contact.ContactFlowUtils.createLosingTransferPollMessage;
import static google.registry.flows.contact.ContactFlowUtils.createTransferResponse;
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACTION_PENDING;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet;
@@ -145,12 +144,13 @@ public final class ContactTransferRequestFlow implements TransactionalFlow {
.setTransferData(pendingTransferData)
.addStatusValue(StatusValue.PENDING_TRANSFER)
.build();
ofy().save().<Object>entities(
newContact,
historyEntry,
requestPollMessage,
serverApproveGainingPollMessage,
serverApproveLosingPollMessage);
tm().update(newContact);
tm().insertAll(
ImmutableSet.of(
historyEntry.toChildHistoryEntity(),
requestPollMessage,
serverApproveGainingPollMessage,
serverApproveLosingPollMessage));
return responseBuilder
.setResultFromCode(SUCCESS_WITH_ACTION_PENDING)
.setResData(createTransferResponse(targetId, newContact.getTransferData()))
@@ -154,7 +154,7 @@ public class AllocationTokenFlowUtils {
}
Optional<AllocationToken> maybeTokenEntity =
tm().loadByKeyIfPresent(VKey.create(AllocationToken.class, token));
if (maybeTokenEntity.isEmpty()) {
if (!maybeTokenEntity.isPresent()) {
throw new InvalidAllocationTokenException();
}
if (maybeTokenEntity.get().isRedeemed()) {
@@ -148,7 +148,7 @@ public abstract class EppResource extends BackupGroupRoot implements Buildable {
*
* @see google.registry.model.translators.CommitLogRevisionsTranslatorFactory
*/
@Transient
@Transient @DoNotCompare
ImmutableSortedMap<DateTime, Key<CommitLogManifest>> revisions = ImmutableSortedMap.of();
public String getRepoId() {
@@ -54,9 +54,37 @@ public abstract class ImmutableObject implements Cloneable {
@Target(FIELD)
public @interface DoNotHydrate {}
@Ignore
@XmlTransient
Integer hashCode;
/**
* Indicates that the field should be ignored when comparing an object in the datastore to the
* corresponding object in Cloud SQL.
*/
@Documented
@Retention(RUNTIME)
@Target(FIELD)
public @interface DoNotCompare {}
/**
* Indicates that the field stores a null value to indicate an empty set. This is also used in
* object comparison.
*/
@Documented
@Retention(RUNTIME)
@Target(FIELD)
public @interface EmptySetToNull {}
/**
* Indicates that the field does not take part in the immutability contract.
*
* <p>Certain fields currently get modified by hibernate and there is nothing we can do about it.
* As well as violating immutability, this breaks hashing and equality comparisons, so we mark
* these fields with this annotation to exclude them from most operations.
*/
@Documented
@Retention(RUNTIME)
@Target(FIELD)
public @interface Insignificant {}
@Ignore @XmlTransient protected Integer hashCode;
private boolean equalsImmutableObject(ImmutableObject other) {
return getClass().equals(other.getClass())
@@ -71,7 +99,14 @@ public abstract class ImmutableObject implements Cloneable {
* <p>Isolated into a method so that derived classes can override it.
*/
protected Map<Field, Object> getSignificantFields() {
return ModelUtils.getFieldValues(this);
// Can't use streams or ImmutableMap because we can have null values.
Map<Field, Object> result = new LinkedHashMap();
for (Map.Entry<Field, Object> entry : ModelUtils.getFieldValues(this).entrySet()) {
if (!entry.getKey().isAnnotationPresent(Insignificant.class)) {
result.put(entry.getKey(), entry.getValue());
}
}
return result;
}
@Override
@@ -14,6 +14,7 @@
package google.registry.model.domain;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Strings.emptyToNull;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
@@ -65,6 +66,7 @@ import google.registry.model.transfer.DomainTransferData;
import google.registry.model.transfer.TransferStatus;
import google.registry.persistence.VKey;
import google.registry.util.CollectionUtils;
import google.registry.util.DateTimeUtils;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
@@ -132,7 +134,7 @@ public class DomainContent extends EppResource
@Index String tld;
/** References to hosts that are the nameservers for the domain. */
@Index @Transient Set<VKey<HostResource>> nsHosts;
@EmptySetToNull @Index @Transient Set<VKey<HostResource>> nsHosts;
/**
* The union of the contacts visible via {@link #getContacts} and {@link #getRegistrant}.
@@ -283,9 +285,10 @@ public class DomainContent extends EppResource
/**
* When the domain's autorenewal status will expire.
*
* <p>This will be null for the vast majority of domains because all domains autorenew
* indefinitely by default and autorenew can only be countermanded by administrators, typically
* for reasons of the URS process or termination of a registrar for nonpayment.
* <p>This will be {@link DateTimeUtils#END_OF_TIME} for the vast majority of domains because all
* domains autorenew indefinitely by default and autorenew can only be countermanded by
* administrators, typically for reasons of the URS process or termination of a registrar for
* nonpayment.
*
* <p>When a domain is scheduled to not autorenew, this field is set to the current value of its
* {@link #registrationExpirationTime}, after which point the next invocation of a periodic
@@ -294,10 +297,16 @@ public class DomainContent extends EppResource
* difference domains that have reached their life and must be deleted now, and domains that
* happen to be in the autorenew grace period now but should be deleted in roughly a year.
*/
@Nullable @Index DateTime autorenewEndTime;
@Index DateTime autorenewEndTime;
@OnLoad
void load() {
// Back fill with correct END_OF_TIME sentinel value.
// TODO(mcilwain): Remove this once back-filling is complete.
if (autorenewEndTime == null) {
autorenewEndTime = END_OF_TIME;
}
// Reconstitute all of the contacts so that they have VKeys.
allContacts =
allContacts.stream().map(DesignatedContact::reconstitute).collect(toImmutableSet());
@@ -319,6 +328,11 @@ public class DomainContent extends EppResource
autorenewPollMessageHistoryId = getHistoryId(autorenewPollMessage);
autorenewBillingEventHistoryId = getHistoryId(autorenewBillingEvent);
deletePollMessageHistoryId = getHistoryId(deletePollMessage);
// Fix PollMessage VKeys.
autorenewPollMessage = PollMessage.Autorenew.convertVKey(autorenewPollMessage);
deletePollMessage = PollMessage.OneTime.convertVKey(deletePollMessage);
dsData =
nullToEmptyImmutableCopy(dsData).stream()
.map(dsData -> dsData.cloneWithDomainRepoId(getRepoId()))
@@ -397,8 +411,20 @@ public class DomainContent extends EppResource
return smdId;
}
/**
* Returns the autorenew end time if there is one, otherwise empty.
*
* <p>Note that {@link DateTimeUtils#END_OF_TIME} is used as a sentinel value in the database
* representation to signify that autorenew doesn't end, and is mapped to empty here for the
* purposes of more legible business logic.
*/
public Optional<DateTime> getAutorenewEndTime() {
return Optional.ofNullable(autorenewEndTime);
// TODO(mcilwain): Remove null handling for autorenewEndTime once data migration away from null
// is complete.
return Optional.ofNullable(
(autorenewEndTime == null || autorenewEndTime.equals(END_OF_TIME))
? null
: autorenewEndTime);
}
@Override
@@ -773,6 +799,8 @@ public class DomainContent extends EppResource
} else { // There are nameservers, so make sure INACTIVE isn't there.
removeStatusValue(StatusValue.INACTIVE);
}
// If there is no autorenew end time, set it to END_OF_TIME.
instance.autorenewEndTime = firstNonNull(getInstance().autorenewEndTime, END_OF_TIME);
checkArgumentNotNull(emptyToNull(instance.fullyQualifiedDomainName), "Missing domainName");
if (instance.getRegistrant() == null
@@ -952,8 +980,15 @@ public class DomainContent extends EppResource
return thisCastToDerived();
}
/**
* Sets the autorenew end time, or clears it if empty is passed.
*
* <p>Note that {@link DateTimeUtils#END_OF_TIME} is used as a sentinel value in the database
* representation to signify that autorenew doesn't end, and is mapped to empty here for the
* purposes of more legible business logic.
*/
public B setAutorenewEndTime(Optional<DateTime> autorenewEndTime) {
getInstance().autorenewEndTime = autorenewEndTime.orElse(null);
getInstance().autorenewEndTime = autorenewEndTime.orElse(END_OF_TIME);
return thisCastToDerived();
}
@@ -104,6 +104,7 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
"domain_history_history_revision_id,domain_history_domain_repo_id,host_repo_id",
unique = true),
})
@ImmutableObject.EmptySetToNull
@Column(name = "host_repo_id")
Set<VKey<HostResource>> nsHosts;
@@ -180,7 +181,9 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
* #getDomainTransactionRecords()}.
*/
@Access(AccessType.PROPERTY)
@OneToMany(cascade = {CascadeType.ALL})
@OneToMany(
cascade = {CascadeType.ALL},
fetch = FetchType.EAGER)
@JoinColumn(name = "historyRevisionId", referencedColumnName = "historyRevisionId")
@JoinColumn(name = "domainRepoId", referencedColumnName = "domainRepoId")
@SuppressWarnings("unused")
@@ -193,19 +193,6 @@ public class GracePeriod extends GracePeriodBase implements DatastoreAndSqlEntit
return clone;
}
/**
* Returns a clone of this {@link GracePeriod} with prepopulated {@link #gracePeriodId} generated
* by {@link ObjectifyService#allocateId()}.
*
* <p>TODO(shicong): Figure out how to generate the id only when the entity is used for Cloud SQL.
*/
@VisibleForTesting
public GracePeriod cloneWithPrepopulatedId() {
GracePeriod clone = clone(this);
clone.gracePeriodId = ObjectifyService.allocateId();
return clone;
}
/** Entity class to represent a historic {@link GracePeriod}. */
@Entity(name = "GracePeriodHistory")
@Table(indexes = @Index(columnList = "domainRepoId"))
@@ -35,7 +35,7 @@ import javax.xml.bind.annotation.XmlType;
*
* @see <a href="http://tools.ietf.org/html/rfc5910">RFC 5910</a>
* @see <a href="http://tools.ietf.org/html/rfc4034">RFC 4034</a>
* <p>TODO(shicong): Rename this class to DomainDsData.
* <p>TODO(b/177567432): Rename this class to DomainDsData.
*/
@Embed
@XmlType(name = "dsData")
@@ -59,8 +59,8 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
public class Address extends ImmutableObject implements Jsonifiable {
/** The schema validation will enforce that this has 3 lines at most. */
// TODO(shicong): Remove this field after migration. We need to figure out how to generate same
// XML from streetLine[1,2,3].
// TODO(b/177569726): Remove this field after migration. We need to figure out how to generate
// same XML from streetLine[1,2,3].
@XmlJavaTypeAdapter(NormalizedStringAdapter.class)
@Transient
List<String> street;
@@ -175,7 +175,7 @@ public class Address extends ImmutableObject implements Jsonifiable {
* entity from Datastore.
*
* <p>This callback method is used by Objectify to set streetLine[1,2,3] fields as they are not
* persisted in the Datastore. TODO(shicong): Delete this method after database migration.
* persisted in the Datastore.
*/
void onLoad(@AlsoLoad("street") List<String> street) {
mapStreetListToIndividualFields(street);
@@ -30,6 +30,7 @@ import com.google.common.collect.Streams;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Result;
import google.registry.model.contact.ContactHistory;
import google.registry.model.domain.DomainHistory;
import google.registry.model.host.HostHistory;
import google.registry.model.reporting.HistoryEntry;
import google.registry.persistence.VKey;
@@ -292,6 +293,21 @@ public class DatastoreTransactionManager implements TransactionManager {
getOfy().clearSessionCache();
}
/**
* Executes the given {@link Result} instance synchronously if not in a transaction.
*
* <p>The {@link Result} instance contains a task that will be executed by Objectify
* asynchronously. If it is in a transaction, we don't need to execute the task immediately
* because it is guaranteed to be done by the end of the transaction. However, if it is not in a
* transaction, we need to execute it in case the following code expects that happens before
* themselves.
*/
private void syncIfTransactionless(Result<?> result) {
if (!inTransaction()) {
result.now();
}
}
/**
* The following three methods exist due to the migration to Cloud SQL.
*
@@ -309,34 +325,23 @@ public class DatastoreTransactionManager implements TransactionManager {
syncIfTransactionless(getOfy().save().entity(entity));
}
@SuppressWarnings("unchecked")
private <T> T toChildHistoryEntryIfPossible(@Nullable T obj) {
// NB: The Key of the object in question may not necessarily be the resulting class that we
// wish to have. Because all *History classes are @EntitySubclasses, their Keys will have type
// HistoryEntry -- even if you create them based off the *History class.
if (obj != null && HistoryEntry.class.isAssignableFrom(obj.getClass())) {
return (T) ((HistoryEntry) obj).toChildHistoryEntity();
}
return obj;
}
@Nullable
private <T> T loadNullable(VKey<T> key) {
return toChildHistoryEntryIfPossible(getOfy().load().key(key.getOfyKey()).now());
}
/**
* Executes the given {@link Result} instance synchronously if not in a transaction.
*
* <p>The {@link Result} instance contains a task that will be executed by Objectify
* asynchronously. If it is in a transaction, we don't need to execute the task immediately
* because it is guaranteed to be done by the end of the transaction. However, if it is not in a
* transaction, we need to execute it in case the following code expects that happens before
* themselves.
*/
private void syncIfTransactionless(Result<?> result) {
if (!inTransaction()) {
result.now();
/** Converts a nonnull {@link HistoryEntry} to the child format, e.g. {@link DomainHistory} */
@SuppressWarnings("unchecked")
public static <T> T toChildHistoryEntryIfPossible(@Nullable T obj) {
// NB: The Key of the object in question may not necessarily be the resulting class that we
// wish to have. Because all *History classes are @EntitySubclasses, their Keys will have type
// HistoryEntry -- even if you create them based off the *History class.
if (obj instanceof HistoryEntry
&& !(obj instanceof ContactHistory)
&& !(obj instanceof DomainHistory)
&& !(obj instanceof HostHistory)) {
return (T) ((HistoryEntry) obj).toChildHistoryEntity();
}
return obj;
}
}
@@ -14,7 +14,18 @@
package google.registry.model.ofy;
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 com.google.common.collect.ImmutableMap;
import com.googlecode.objectify.Key;
import google.registry.config.RegistryEnvironment;
import google.registry.model.UpdateAutoTimestamp;
import google.registry.persistence.VKey;
import google.registry.schema.replay.DatastoreEntity;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
@@ -24,23 +35,79 @@ import java.util.concurrent.ConcurrentLinkedQueue;
*/
public class ReplayQueue {
static ConcurrentLinkedQueue<TransactionInfo> queue =
new ConcurrentLinkedQueue<TransactionInfo>();
static ConcurrentLinkedQueue<ImmutableMap<Key<?>, Object>> queue =
new ConcurrentLinkedQueue<ImmutableMap<Key<?>, Object>>();
static void addInTests(TransactionInfo info) {
if (RegistryEnvironment.get() == RegistryEnvironment.UNITTEST) {
queue.add(info);
// Transform the entities to be persisted to the set of values as they were actually
// persisted.
ImmutableMap.Builder<Key<?>, Object> builder = new ImmutableMap.Builder<Key<?>, Object>();
for (ImmutableMap.Entry<Key<?>, Object> entry : info.getChanges().entrySet()) {
if (entry.getValue().equals(TransactionInfo.Delete.SENTINEL)) {
builder.put(entry.getKey(), entry.getValue());
} else {
// The value is an entity object that has not yet been persisted, and thus some of the
// special transformations that we do (notably the auto-timestamp transformations) have
// not been applied. Converting the object to an entity and then back again performs
// those transformations so that we persist the same values to SQL that we have in
// Datastore.
builder.put(entry.getKey(), ofy().toPojo(ofy().toEntity(entry.getValue())));
}
}
queue.add(builder.build());
}
}
public static void replay() {
TransactionInfo info;
while ((info = queue.poll()) != null) {
info.saveToJpa();
/** Replay all transactions, return the set of keys that were replayed. */
public static ImmutableMap<Key<?>, Object> replay() {
// We can't use an ImmutableMap.Builder here, we need to be able to overwrite existing values
// and the builder doesn't support that.
Map<Key<?>, Object> result = new HashMap<Key<?>, Object>();
ImmutableMap<Key<?>, Object> changes;
while ((changes = queue.poll()) != null) {
saveToJpa(changes);
result.putAll(changes);
}
return ImmutableMap.copyOf(result);
}
public static void clear() {
queue.clear();
}
/** Returns the priority of the entity type in the map entry. */
private static int getPriority(ImmutableMap.Entry<Key<?>, Object> entry) {
return getEntityPriority(
entry.getKey().getKind(), entry.getValue().equals(TransactionInfo.Delete.SENTINEL));
}
private static int compareByPriority(
ImmutableMap.Entry<Key<?>, Object> a, ImmutableMap.Entry<Key<?>, Object> b) {
return getPriority(a) - getPriority(b);
}
private static void saveToJpa(ImmutableMap<Key<?>, Object> changes) {
try (UpdateAutoTimestamp.DisableAutoUpdateResource disabler =
UpdateAutoTimestamp.disableAutoUpdate()) {
// Sort the changes into an order that will work for insertion into the database.
jpaTm()
.transact(
() -> {
changes.entrySet().stream()
.sorted(ReplayQueue::compareByPriority)
.forEach(
entry -> {
if (entry.getValue().equals(TransactionInfo.Delete.SENTINEL)) {
jpaTm().delete(VKey.from(entry.getKey()));
} else {
((DatastoreEntity) entry.getValue())
.toSqlEntity()
.ifPresent(jpaTm()::put);
}
});
});
}
}
}
@@ -20,24 +20,20 @@ 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;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.Key;
import google.registry.persistence.VKey;
import google.registry.schema.replay.DatastoreEntity;
import java.util.Map;
import org.joda.time.DateTime;
/** Metadata for an {@link Ofy} transaction that saves commit logs. */
class TransactionInfo {
public class TransactionInfo {
@VisibleForTesting
enum Delete {
public enum Delete {
SENTINEL
}
@@ -87,6 +83,10 @@ class TransactionInfo {
return ImmutableSet.copyOf(changesBuilder.build().keySet());
}
ImmutableMap<Key<?>, Object> getChanges() {
return changesBuilder.build();
}
ImmutableSet<Key<?>> getDeletes() {
return ImmutableSet.copyOf(
filterValues(changesBuilder.build(), Delete.SENTINEL::equals).keySet());
@@ -100,35 +100,4 @@ class TransactionInfo {
.filter(not(Delete.SENTINEL::equals))
.collect(toImmutableSet());
}
/** Returns the weight of the entity type in the map entry. */
@VisibleForTesting
static int getWeight(ImmutableMap.Entry<Key<?>, Object> entry) {
return getEntityPriority(entry.getKey().getKind(), entry.getValue().equals(Delete.SENTINEL));
}
private static int compareByWeight(
ImmutableMap.Entry<Key<?>, Object> a, ImmutableMap.Entry<Key<?>, Object> b) {
return getWeight(a) - getWeight(b);
}
void saveToJpa() {
// Sort the changes into an order that will work for insertion into the database.
jpaTm()
.transact(
() -> {
changesBuilder.build().entrySet().stream()
.sorted(TransactionInfo::compareByWeight)
.forEach(
entry -> {
if (entry.getValue().equals(Delete.SENTINEL)) {
jpaTm().delete(VKey.from(entry.getKey()));
} else {
((DatastoreEntity) entry.getValue())
.toSqlEntity()
.ifPresent(jpaTm()::put);
}
});
});
}
}
@@ -16,6 +16,7 @@ package google.registry.model.poll;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.util.CollectionUtils.forceEmptyToNull;
import static google.registry.util.CollectionUtils.isNullOrEmpty;
import static google.registry.util.CollectionUtils.nullToEmpty;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
@@ -52,6 +53,7 @@ import google.registry.persistence.WithLongVKey;
import google.registry.schema.replay.DatastoreAndSqlEntity;
import java.util.List;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
import javax.persistence.Column;
@@ -185,6 +187,7 @@ public abstract class PollMessage extends ImmutableObject
@Override
public abstract VKey<? extends PollMessage> createVKey();
/** Static VKey factory method for use by VKeyTranslatorFactory. */
public static VKey<PollMessage> createVKey(Key<PollMessage> key) {
return VKey.create(PollMessage.class, key.getId(), key);
}
@@ -289,7 +292,7 @@ public abstract class PollMessage extends ImmutableObject
@Transient List<ContactTransferResponse> contactTransferResponses;
@Transient
@Transient @ImmutableObject.DoNotCompare
List<DomainPendingActionNotificationResponse> domainPendingActionNotificationResponses;
@Transient List<DomainTransferResponse> domainTransferResponses;
@@ -355,6 +358,11 @@ public abstract class PollMessage extends ImmutableObject
return VKey.create(OneTime.class, getId(), Key.create(this));
}
/** Converts an unspecialized VKey&lt;PollMessage&gt; to a VKey of the derived class. */
public static @Nullable VKey<OneTime> convertVKey(@Nullable VKey<OneTime> key) {
return key == null ? null : VKey.create(OneTime.class, key.getSqlKey(), key.getOfyKey());
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
@@ -371,6 +379,47 @@ public abstract class PollMessage extends ImmutableObject
.build();
}
@Override
@OnLoad
void onLoad() {
super.onLoad();
if (!isNullOrEmpty(contactPendingActionNotificationResponses)) {
pendingActionNotificationResponse = contactPendingActionNotificationResponses.get(0);
}
if (!isNullOrEmpty(contactTransferResponses)) {
contactId = contactTransferResponses.get(0).getContactId();
transferResponse = contactTransferResponses.get(0);
}
}
@Override
@PostLoad
void postLoad() {
super.postLoad();
if (pendingActionNotificationResponse != null) {
contactPendingActionNotificationResponses =
ImmutableList.of(
ContactPendingActionNotificationResponse.create(
pendingActionNotificationResponse.nameOrId.value,
pendingActionNotificationResponse.getActionResult(),
pendingActionNotificationResponse.getTrid(),
pendingActionNotificationResponse.processedDate));
}
if (contactId != null && transferResponse != null) {
contactTransferResponses =
ImmutableList.of(
new ContactTransferResponse.Builder()
.setContactId(contactId)
.setGainingClientId(transferResponse.getGainingClientId())
.setLosingClientId(transferResponse.getLosingClientId())
.setTransferStatus(transferResponse.getTransferStatus())
.setTransferRequestTime(transferResponse.getTransferRequestTime())
.setPendingTransferExpirationTime(
transferResponse.getPendingTransferExpirationTime())
.build());
}
}
/** A builder for {@link OneTime} since it is immutable. */
public static class Builder extends PollMessage.Builder<OneTime, Builder> {
@@ -389,6 +438,10 @@ public abstract class PollMessage extends ImmutableObject
.filter(ContactPendingActionNotificationResponse.class::isInstance)
.map(ContactPendingActionNotificationResponse.class::cast)
.collect(toImmutableList()));
if (getInstance().contactPendingActionNotificationResponses != null) {
getInstance().pendingActionNotificationResponse =
getInstance().contactPendingActionNotificationResponses.get(0);
}
getInstance().contactTransferResponses =
forceEmptyToNull(
responseData
@@ -396,6 +449,11 @@ public abstract class PollMessage extends ImmutableObject
.filter(ContactTransferResponse.class::isInstance)
.map(ContactTransferResponse.class::cast)
.collect(toImmutableList()));
if (getInstance().contactTransferResponses != null) {
getInstance().contactId = getInstance().contactTransferResponses.get(0).getContactId();
getInstance().transferResponse = getInstance().contactTransferResponses.get(0);
}
getInstance().domainPendingActionNotificationResponses =
forceEmptyToNull(
responseData
@@ -456,6 +514,11 @@ public abstract class PollMessage extends ImmutableObject
return VKey.create(Autorenew.class, getId(), Key.create(this));
}
/** Converts an unspecialized VKey&lt;PollMessage&gt; to a VKey of the derived class. */
public static @Nullable VKey<Autorenew> convertVKey(VKey<Autorenew> key) {
return key == null ? null : VKey.create(Autorenew.class, key.getSqlKey(), key.getOfyKey());
}
@Override
public ImmutableList<ResponseData> getResponseData() {
// Note that the event time is when the auto-renew occured, so the expiration time in the
@@ -234,7 +234,7 @@ public class Registrar extends ImmutableObject
* Unique registrar client id. Must conform to "clIDType" as defined in RFC5730.
*
* @see <a href="http://tools.ietf.org/html/rfc5730#section-4.2">Shared Structure Schema</a>
* <p>TODO(shicong): Rename this field to clientId
* <p>TODO(b/177568946): Rename this field to registrarId.
*/
@Id
@javax.persistence.Id
@@ -49,7 +49,9 @@ public class DomainTransactionRecord extends ImmutableObject
@Id
@Ignore
@ImmutableObject.DoNotCompare
@GeneratedValue(strategy = GenerationType.IDENTITY)
@ImmutableObject.Insignificant
Long id;
/** The TLD this record operates on. */
@@ -198,6 +198,7 @@ public class HistoryEntry extends ImmutableObject implements Buildable, Datastor
* transaction counts (such as contact or host mutations).
*/
@Transient // domain-specific
@ImmutableObject.EmptySetToNull
protected Set<DomainTransactionRecord> domainTransactionRecords;
public long getId() {
@@ -0,0 +1,144 @@
// 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.reporting;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import com.google.common.collect.Iterables;
import google.registry.model.EppResource;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainHistory;
import google.registry.model.host.HostHistory;
import google.registry.model.host.HostResource;
import google.registry.persistence.VKey;
import java.util.Comparator;
import javax.persistence.EntityManager;
import org.joda.time.DateTime;
/**
* Retrieves {@link HistoryEntry} descendants (e.g. {@link DomainHistory}).
*
* <p>This class is configured to retrieve either from Datastore or SQL, depending on which database
* is currently considered the primary database.
*/
public class HistoryEntryDao {
/** Loads all history objects in the times specified, including all types. */
public static Iterable<? extends HistoryEntry> loadAllHistoryObjects(
DateTime afterTime, DateTime beforeTime) {
if (tm().isOfy()) {
return ofy()
.load()
.type(HistoryEntry.class)
.order("modificationTime")
.filter("modificationTime >=", afterTime)
.filter("modificationTime <=", beforeTime);
} else {
return jpaTm()
.transact(
() ->
Iterables.concat(
loadAllHistoryObjectsFromSql(ContactHistory.class, afterTime, beforeTime),
loadAllHistoryObjectsFromSql(DomainHistory.class, afterTime, beforeTime),
loadAllHistoryObjectsFromSql(HostHistory.class, afterTime, beforeTime)));
}
}
/** Loads all history objects corresponding to the given {@link EppResource}. */
public static Iterable<? extends HistoryEntry> loadHistoryObjectsForResource(
VKey<? extends EppResource> parentKey) {
return loadHistoryObjectsForResource(parentKey, START_OF_TIME, END_OF_TIME);
}
/** Loads all history objects in the time period specified for the given {@link EppResource}. */
public static Iterable<? extends HistoryEntry> loadHistoryObjectsForResource(
VKey<? extends EppResource> parentKey, DateTime afterTime, DateTime beforeTime) {
if (tm().isOfy()) {
return ofy()
.load()
.type(HistoryEntry.class)
.ancestor(parentKey.getOfyKey())
.order("modificationTime")
.filter("modificationTime >=", afterTime)
.filter("modificationTime <=", beforeTime);
} else {
return jpaTm()
.transact(() -> loadHistoryObjectsForResourceFromSql(parentKey, afterTime, beforeTime));
}
}
private static Iterable<? extends HistoryEntry> loadHistoryObjectsForResourceFromSql(
VKey<? extends EppResource> parentKey, DateTime afterTime, DateTime beforeTime) {
Class<? extends HistoryEntry> historyClass = getHistoryClassFromParent(parentKey.getKind());
String repoIdFieldName = getRepoIdFieldNameFromHistoryClass(historyClass);
EntityManager entityManager = jpaTm().getEntityManager();
String tableName = entityManager.getMetamodel().entity(historyClass).getName();
String queryString =
String.format(
"SELECT entry FROM %s entry WHERE entry.modificationTime >= :afterTime AND "
+ "entry.modificationTime <= :beforeTime AND entry.%s = :parentKey",
tableName, repoIdFieldName);
return entityManager
.createQuery(queryString, historyClass)
.setParameter("afterTime", afterTime)
.setParameter("beforeTime", beforeTime)
.setParameter("parentKey", parentKey.getSqlKey().toString())
.getResultStream()
.sorted(Comparator.comparing(HistoryEntry::getModificationTime))
.collect(toImmutableList());
}
private static Class<? extends HistoryEntry> getHistoryClassFromParent(
Class<? extends EppResource> parent) {
if (parent.equals(ContactResource.class)) {
return ContactHistory.class;
} else if (parent.equals(DomainBase.class)) {
return DomainHistory.class;
} else if (parent.equals(HostResource.class)) {
return HostHistory.class;
}
throw new IllegalArgumentException(
String.format("Unknown history type for parent %s", parent.getName()));
}
private static String getRepoIdFieldNameFromHistoryClass(
Class<? extends HistoryEntry> historyClass) {
return historyClass.equals(ContactHistory.class)
? "contactRepoId"
: historyClass.equals(DomainHistory.class) ? "domainRepoId" : "hostRepoId";
}
private static Iterable<? extends HistoryEntry> loadAllHistoryObjectsFromSql(
Class<? extends HistoryEntry> historyClass, DateTime afterTime, DateTime beforeTime) {
EntityManager entityManager = jpaTm().getEntityManager();
return entityManager
.createQuery(
String.format(
"SELECT entry FROM %s entry WHERE entry.modificationTime >= :afterTime AND "
+ "entry.modificationTime <= :beforeTime",
entityManager.getMetamodel().entity(historyClass).getName()),
historyClass)
.setParameter("afterTime", afterTime)
.setParameter("beforeTime", beforeTime)
.getResultList();
}
}
@@ -58,7 +58,7 @@ public class ClaimsListDao {
/**
* Returns the most recent revision of the {@link ClaimsListShard} in Cloud SQL, if it exists.
* TODO(shicong): Change this method to package level access after dual-read phase.
* TODO(b/177569979): Change this method to package level access after dual-read phase.
* ClaimsListShard uses this method to retrieve claims list in Cloud SQL for the comparison, and
* ClaimsListShard is not in this package.
*/
@@ -14,15 +14,25 @@
package google.registry.model.transfer;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.util.CollectionUtils.isNullOrEmpty;
import static google.registry.util.CollectionUtils.nullToEmpty;
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.AlsoLoad;
import com.googlecode.objectify.annotation.Ignore;
import com.googlecode.objectify.annotation.IgnoreSave;
import com.googlecode.objectify.condition.IfNull;
import google.registry.model.Buildable;
import google.registry.model.EppResource;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.DomainBase;
import google.registry.model.eppcommon.Trid;
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry;
import google.registry.persistence.VKey;
import google.registry.util.TypeUtils.TypeInstantiator;
import java.util.Set;
@@ -32,6 +42,7 @@ import javax.persistence.AttributeOverrides;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.MappedSuperclass;
import javax.persistence.PostLoad;
import javax.persistence.Transient;
/**
@@ -68,15 +79,27 @@ public abstract class TransferData<
@IgnoreSave(IfNull.class)
Set<VKey<? extends TransferServerApproveEntity>> serverApproveEntities;
// The following 3 fields are the replacement for serverApproveEntities in Cloud SQL.
// TODO(shicong): Add getter/setter for these 3 fields and use them in the application code.
@Ignore
@Column(name = "transfer_gaining_poll_message_id")
Long gainingTransferPollMessageId;
@Column(name = "transfer_repo_id")
String repoId;
@Ignore
@Column(name = "transfer_losing_poll_message_id")
Long losingTransferPollMessageId;
@Column(name = "transfer_history_entry_id")
Long historyEntryId;
// The pollMessageId1 and pollMessageId2 are used to store the IDs for gaining and losing poll
// messages in Cloud SQL, and they are added to replace the VKeys in serverApproveEntities.
// Although we can distinguish which is which when we construct the TransferData instance from
// the transfer request flow, when the instance is loaded from Datastore, we cannot make this
// distinction because they are just VKeys. Also, the only way we use serverApproveEntities is to
// just delete all the entities referenced by the VKeys, so we don't need to make the distinction.
@Ignore
@Column(name = "transfer_poll_message_id_1")
Long pollMessageId1;
@Ignore
@Column(name = "transfer_poll_message_id_2")
Long pollMessageId2;
public abstract boolean isEmpty();
@@ -116,6 +139,83 @@ public abstract class TransferData<
return newBuilder;
}
void onLoad(
@AlsoLoad("serverApproveEntities")
Set<VKey<? extends TransferServerApproveEntity>> serverApproveEntities) {
mapServerApproveEntitiesToFields(serverApproveEntities, this);
}
@PostLoad
void postLoad() {
mapFieldsToServerApproveEntities();
}
/**
* Reconstructs serverApproveEntities set from the individual fields, e.g. repoId, historyEntryId,
* pollMessageId1.
*/
void mapFieldsToServerApproveEntities() {
if (repoId == null) {
return;
}
Key<? extends EppResource> eppKey;
if (getClass().equals(DomainBase.class)) {
eppKey = Key.create(DomainBase.class, repoId);
} else {
eppKey = Key.create(ContactResource.class, repoId);
}
Key<HistoryEntry> historyEntryKey = Key.create(eppKey, HistoryEntry.class, historyEntryId);
ImmutableSet.Builder<VKey<? extends TransferServerApproveEntity>> entityKeysBuilder =
new ImmutableSet.Builder<>();
if (pollMessageId1 != null) {
Key<PollMessage> ofyKey = Key.create(historyEntryKey, PollMessage.class, pollMessageId1);
entityKeysBuilder.add(PollMessage.createVKey(ofyKey));
}
if (pollMessageId2 != null) {
Key<PollMessage> ofyKey = Key.create(historyEntryKey, PollMessage.class, pollMessageId2);
entityKeysBuilder.add(PollMessage.createVKey(ofyKey));
}
serverApproveEntities = entityKeysBuilder.build();
}
/** Maps serverApproveEntities set to the individual fields. */
static void mapServerApproveEntitiesToFields(
Set<VKey<? extends TransferServerApproveEntity>> serverApproveEntities,
TransferData transferData) {
if (isNullOrEmpty(serverApproveEntities)) {
transferData.historyEntryId = null;
transferData.repoId = null;
transferData.pollMessageId1 = null;
transferData.pollMessageId2 = null;
return;
}
// Each element in serverApproveEntities should have the exact same Key<HistoryEntry> as its
// parent. So, we can use any to set historyEntryId and repoId.
Key<?> key = serverApproveEntities.iterator().next().getOfyKey();
transferData.historyEntryId = key.getParent().getId();
transferData.repoId = key.getParent().getParent().getName();
ImmutableList<Long> sortedPollMessageIds = getSortedPollMessageIds(serverApproveEntities);
if (sortedPollMessageIds.size() >= 1) {
transferData.pollMessageId1 = sortedPollMessageIds.get(0);
}
if (sortedPollMessageIds.size() >= 2) {
transferData.pollMessageId2 = sortedPollMessageIds.get(1);
}
}
/**
* Gets poll message IDs from the given serverApproveEntities and sorted the IDs in natural order.
*/
private static ImmutableList<Long> getSortedPollMessageIds(
Set<VKey<? extends TransferServerApproveEntity>> serverApproveEntities) {
return nullToEmpty(serverApproveEntities).stream()
.filter(vKey -> PollMessage.class.isAssignableFrom(vKey.getKind()))
.map(vKey -> (long) vKey.getSqlKey())
.sorted()
.collect(toImmutableList());
}
/** Builder for {@link TransferData} because it is immutable. */
public abstract static class Builder<T extends TransferData, B extends Builder<T, B>>
extends BaseTransferObject.Builder<T, B> {
@@ -141,6 +241,7 @@ public abstract class TransferData<
@Override
public T build() {
mapServerApproveEntitiesToFields(getInstance().serverApproveEntities, getInstance());
return super.build();
}
}
@@ -351,8 +351,8 @@ public abstract class PersistenceModule {
@interface BeamPipelineCloudSqlConfigs {}
/** Dagger qualifier for the default Hibernate configurations. */
// TODO(shicong): Change annotations in this class to none public or put them in a top level
// package
// TODO(b/177568911): Change annotations in this class to none public or put them in a top level
// package.
@Qualifier
@Documented
public @interface DefaultHibernateConfigs {}
@@ -20,7 +20,6 @@ import javax.persistence.Converter;
/**
* JPA {@link AttributeConverter} for storing/retrieving {@code List<CidrAddressBlock>} objects.
* TODO(shicong): Investigate if we can have one converter for any List type
*/
@Converter(autoApply = true)
public class CidrAddressBlockListConverter extends StringListConverterBase<CidrAddressBlock> {
@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.model.ofy.DatastoreTransactionManager.toChildHistoryEntryIfPossible;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import static java.util.AbstractMap.SimpleEntry;
import static java.util.stream.Collectors.joining;
@@ -34,6 +35,7 @@ import google.registry.model.index.EppResourceIndex;
import google.registry.model.index.ForeignKeyIndex.ForeignKeyContactIndex;
import google.registry.model.index.ForeignKeyIndex.ForeignKeyDomainIndex;
import google.registry.model.index.ForeignKeyIndex.ForeignKeyHostIndex;
import google.registry.model.ofy.DatastoreTransactionManager;
import google.registry.persistence.JpaRetries;
import google.registry.persistence.VKey;
import google.registry.util.Clock;
@@ -76,8 +78,9 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
// EntityManagerFactory is thread safe.
private final EntityManagerFactory emf;
private final Clock clock;
// TODO(shicong): Investigate alternatives for managing transaction information. ThreadLocal adds
// an unnecessary restriction that each request has to be processed by one thread synchronously.
// TODO(b/177588434): Investigate alternatives for managing transaction information. ThreadLocal
// adds an unnecessary restriction that each request has to be processed by one thread
// synchronously.
private final ThreadLocal<TransactionInfo> transactionInfo =
ThreadLocal.withInitial(TransactionInfo::new);
@@ -115,8 +118,6 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
@Override
public <T> T transact(Supplier<T> work) {
// TODO(shicong): Investigate removing transactNew functionality after migration as it may
// be same as this one.
return retrier.callWithRetry(
() -> {
if (inTransaction()) {
@@ -196,6 +197,8 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
});
}
// TODO(b/177674699): Remove all transactNew methods as they are same as transact after the
// database migration.
@Override
public <T> T transactNew(Supplier<T> work) {
return transact(work);
@@ -249,8 +252,10 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
return;
}
assertInTransaction();
getEntityManager().persist(entity);
transactionInfo.get().addUpdate(entity);
// Necessary due to the changes in HistoryEntry representation during the migration to SQL
Object toPersist = toChildHistoryEntryIfPossible(entity);
getEntityManager().persist(toPersist);
transactionInfo.get().addUpdate(toPersist);
}
@Override
@@ -277,8 +282,10 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
return;
}
assertInTransaction();
getEntityManager().merge(entity);
transactionInfo.get().addUpdate(entity);
// Necessary due to the changes in HistoryEntry representation during the migration to SQL
Object toPersist = toChildHistoryEntryIfPossible(entity);
getEntityManager().merge(toPersist);
transactionInfo.get().addUpdate(toPersist);
}
@Override
@@ -306,8 +313,10 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
}
assertInTransaction();
checkArgument(exists(entity), "Given entity does not exist");
getEntityManager().merge(entity);
transactionInfo.get().addUpdate(entity);
// Necessary due to the changes in HistoryEntry representation during the migration to SQL
Object toPersist = toChildHistoryEntryIfPossible(entity);
getEntityManager().merge(toPersist);
transactionInfo.get().addUpdate(toPersist);
}
@Override
@@ -338,6 +347,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
@Override
public boolean exists(Object entity) {
checkArgumentNotNull(entity, "entity must be specified");
entity = toChildHistoryEntryIfPossible(entity);
EntityType<?> entityType = getEntityType(entity.getClass());
ImmutableSet<EntityId> entityIds = getEntityIdsFromEntity(entityType, entity);
return exists(entityType.getName(), entityIds);
@@ -381,6 +391,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
@Override
public <T> ImmutableList<T> loadByEntitiesIfPresent(Iterable<T> entities) {
return Streams.stream(entities)
.map(DatastoreTransactionManager::toChildHistoryEntryIfPossible)
.filter(this::exists)
.map(this::loadByEntity)
.collect(toImmutableList());
@@ -415,9 +426,14 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
public <T> T loadByEntity(T entity) {
checkArgumentNotNull(entity, "entity must be specified");
assertInTransaction();
entity = toChildHistoryEntryIfPossible(entity);
// If the caller requested a HistoryEntry, load the corresponding *History class
T possibleChild = toChildHistoryEntryIfPossible(entity);
return (T)
loadByKey(
VKey.createSql(entity.getClass(), emf.getPersistenceUnitUtil().getIdentifier(entity)));
VKey.createSql(
possibleChild.getClass(),
emf.getPersistenceUnitUtil().getIdentifier(possibleChild)));
}
@Override
@@ -468,6 +484,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
return;
}
assertInTransaction();
entity = toChildHistoryEntryIfPossible(entity);
Object managedEntity = entity;
if (!getEntityManager().contains(entity)) {
managedEntity = getEntityManager().merge(entity);
@@ -52,6 +52,7 @@ import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarAddress;
import google.registry.model.registrar.RegistrarContact;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.reporting.HistoryEntryDao;
import google.registry.persistence.VKey;
import google.registry.rdap.RdapDataStructures.Event;
import google.registry.rdap.RdapDataStructures.EventAction;
@@ -880,8 +881,9 @@ public class RdapJsonFormatter {
// 2.3.2.3 An event of *eventAction* type *transfer*, with the last date and time that the
// domain was transferred. The event of *eventAction* type *transfer* MUST be omitted if the
// domain name has not been transferred since it was created.
for (HistoryEntry historyEntry :
ofy().load().type(HistoryEntry.class).ancestor(resource).order("modificationTime")) {
Iterable<? extends HistoryEntry> historyEntries =
HistoryEntryDao.loadHistoryObjectsForResource(resource.createVKey());
for (HistoryEntry historyEntry : historyEntries) {
EventAction rdapEventAction =
HISTORY_ENTRY_TYPE_TO_RDAP_EVENT_ACTION_MAP.get(historyEntry.getType());
// Only save the historyEntries if this is a type we care about.
@@ -930,13 +932,9 @@ public class RdapJsonFormatter {
return eventsBuilder.build();
}
/**
* Creates an RDAP event object as defined by RFC 7483.
*/
/** Creates an RDAP event object as defined by RFC 7483. */
private static Event makeEvent(
EventAction eventAction,
@Nullable String eventActor,
DateTime eventDate) {
EventAction eventAction, @Nullable String eventActor, DateTime eventDate) {
Event.Builder builder = Event.builder()
.setEventAction(eventAction)
.setEventDate(eventDate);
@@ -76,7 +76,7 @@ final class DeleteAllocationTokensCommand extends UpdateOrDeleteAllocationTokens
// since the query ran. This also filters out per-domain tokens if they're not to be deleted.
ImmutableSet<VKey<AllocationToken>> tokensToDelete =
tm().loadByKeys(batch).values().stream()
.filter(t -> withDomains || t.getDomainName().isEmpty())
.filter(t -> withDomains || !t.getDomainName().isPresent())
.filter(t -> SINGLE_USE.equals(t.getTokenType()))
.filter(t -> !t.isRedeemed())
.map(AllocationToken::createVKey)
@@ -59,7 +59,7 @@ final class GetAllocationTokenCommand implements CommandWithRemoteApi {
if (loadedTokens.containsKey(token)) {
AllocationToken loadedToken = loadedTokens.get(token);
System.out.println(loadedToken.toString());
if (loadedToken.getRedemptionHistoryEntry().isEmpty()) {
if (!loadedToken.getRedemptionHistoryEntry().isPresent()) {
System.out.printf("Token %s was not redeemed.\n", token);
} else {
Key<DomainBase> domainOfyKey =
@@ -15,7 +15,6 @@
package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
@@ -25,14 +24,16 @@ import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import google.registry.model.EppResource;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.reporting.HistoryEntryDao;
import google.registry.persistence.VKey;
import google.registry.tools.CommandUtilities.ResourceType;
import google.registry.xml.XmlTransformer;
import org.joda.time.DateTime;
/** Command to show history entries. */
@Parameters(separators = " =",
commandDescription = "Show history entries that occurred in a given time range")
@Parameters(
separators = " =",
commandDescription = "Show history entries that occurred in a given time range")
final class GetHistoryEntriesCommand implements CommandWithRemoteApi {
@Parameter(
@@ -45,33 +46,26 @@ final class GetHistoryEntriesCommand implements CommandWithRemoteApi {
description = "Only show history entries that occurred at or before this time")
private DateTime before = END_OF_TIME;
@Parameter(
names = "--type",
description = "Resource type.")
@Parameter(names = "--type", description = "Resource type.")
private ResourceType type;
@Parameter(
names = "--id",
description = "Foreign key of the resource.")
@Parameter(names = "--id", description = "Foreign key of the resource.")
private String uniqueId;
@Override
public void run() {
VKey<? extends EppResource> parentKey = null;
Iterable<? extends HistoryEntry> historyEntries;
if (type != null || uniqueId != null) {
checkArgument(
type != null && uniqueId != null,
"If either of 'type' or 'id' are set then both must be");
parentKey = type.getKey(uniqueId, DateTime.now(UTC));
VKey<? extends EppResource> parentKey = type.getKey(uniqueId, DateTime.now(UTC));
checkArgumentNotNull(parentKey, "Invalid resource ID");
historyEntries = HistoryEntryDao.loadHistoryObjectsForResource(parentKey, after, before);
} else {
historyEntries = HistoryEntryDao.loadAllHistoryObjects(after, before);
}
for (HistoryEntry entry :
(parentKey == null
? ofy().load().type(HistoryEntry.class)
: ofy().load().type(HistoryEntry.class).ancestor(parentKey.getOfyKey()))
.order("modificationTime")
.filter("modificationTime >=", after)
.filter("modificationTime <=", before)) {
for (HistoryEntry entry : historyEntries) {
System.out.printf(
"Client: %s\nTime: %s\nClient TRID: %s\nServer TRID: %s\n%s\n",
entry.getClientId(),
@@ -168,6 +168,8 @@ interface RegistryToolComponent {
void inject(ValidateEscrowDepositCommand command);
void inject(ValidateLoginCredentialsCommand command);
void inject(WhoisQueryCommand command);
AppEngineConnection appEngineConnection();
@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.domain.rgp.GracePeriodStatus.AUTO_RENEW;
import static google.registry.model.eppcommon.StatusValue.PENDING_DELETE;
import static google.registry.model.eppcommon.StatusValue.SERVER_UPDATE_PROHIBITED;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
@@ -139,6 +140,11 @@ final class UpdateDomainCommand extends CreateOrUpdateDomainCommand {
+ " deleted at the end of its current registration period.")
Boolean autorenews;
@Parameter(
names = {"--force_in_pending_delete"},
description = "Force a superuser update even on domains that are in pending delete")
boolean forceInPendingDelete;
@Override
protected void initMutatingEppToolCommand() {
if (!nameservers.isEmpty()) {
@@ -186,6 +192,12 @@ final class UpdateDomainCommand extends CreateOrUpdateDomainCommand {
"The domain '%s' has status SERVER_UPDATE_PROHIBITED. Verify that you are allowed "
+ "to make updates, and if so, use the domain_unlock command to enable updates.",
domain);
checkArgument(
!domainBase.getStatusValues().contains(PENDING_DELETE) || forceInPendingDelete,
"The domain '%s' has status PENDING_DELETE. Verify that you really are intending to "
+ "update a domain in pending delete (this is uncommon), and if so, pass the "
+ "--force_in_pending_delete parameter to allow this update.",
domain);
// Use TreeSets so that the results are always in the same order (this makes testing easier).
Set<String> addAdminsThisDomain = new TreeSet<>(addAdmins);
@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Strings.isNullOrEmpty;
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
import static google.registry.util.X509Utils.encodeX509CertificateFromPemString;
import static google.registry.util.X509Utils.getCertificateHash;
import static google.registry.util.X509Utils.loadCertificate;
import static java.nio.charset.StandardCharsets.US_ASCII;
@@ -25,12 +26,14 @@ import static java.nio.charset.StandardCharsets.US_ASCII;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import google.registry.flows.TlsCredentials;
import google.registry.flows.certs.CertificateChecker;
import google.registry.model.registrar.Registrar;
import google.registry.tools.params.PathParameter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.inject.Inject;
/** A command to test registrar login credentials. */
@Parameters(separators = " =", commandDescription = "Test registrar login credentials")
@@ -55,6 +58,7 @@ final class ValidateLoginCredentialsCommand implements CommandWithRemoteApi {
validateWith = PathParameter.InputFile.class)
private Path clientCertificatePath;
// TODO(sarahbot@): Remove this after hash fallback is removed
@Nullable
@Parameter(
names = {"-h", "--cert_hash"},
@@ -67,20 +71,28 @@ final class ValidateLoginCredentialsCommand implements CommandWithRemoteApi {
description = "Client ip address to pretend to use")
private String clientIpAddress = "10.0.0.1";
@Inject CertificateChecker certificateChecker;
@Override
public void run() throws Exception {
checkArgument(
clientCertificatePath == null || isNullOrEmpty(clientCertificateHash),
"Can't specify both --cert_hash and --cert_file");
String encodedCertificate = "";
if (clientCertificatePath != null) {
clientCertificateHash = getCertificateHash(
loadCertificate(new String(Files.readAllBytes(clientCertificatePath), US_ASCII)));
String certificateString = new String(Files.readAllBytes(clientCertificatePath), US_ASCII);
encodedCertificate = encodeX509CertificateFromPemString(certificateString);
clientCertificateHash = getCertificateHash(loadCertificate(clientCertificatePath));
}
Registrar registrar =
checkArgumentPresent(
Registrar.loadByClientId(clientId), "Registrar %s not found", clientId);
new TlsCredentials(
true, Optional.ofNullable(clientCertificateHash), Optional.ofNullable(clientIpAddress))
true,
Optional.ofNullable(clientCertificateHash),
Optional.ofNullable(encodedCertificate),
Optional.ofNullable(clientIpAddress),
certificateChecker)
.validate(registrar, password);
checkState(
registrar.isLive(), "Registrar %s has non-live state: %s", clientId, registrar.getState());
@@ -16,20 +16,24 @@ package google.registry.tools.javascrap;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STATUSES;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import com.googlecode.objectify.Key;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.domain.DomainBase;
import google.registry.model.registry.RegistryLockDao;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.reporting.HistoryEntryDao;
import google.registry.persistence.VKey;
import google.registry.schema.domain.RegistryLock;
import google.registry.tools.CommandWithRemoteApi;
import google.registry.tools.ConfirmingCommand;
@@ -130,7 +134,7 @@ public class BackfillRegistryLocksCommand extends ConfirmingCommand
private DateTime getLockCompletionTimestamp(DomainBase domainBase, DateTime now) {
// Best-effort, if a domain was URS-locked we should use that time
// If we can't find that, return now.
return ofy().load().type(HistoryEntry.class).ancestor(domainBase).list().stream()
return Streams.stream(HistoryEntryDao.loadHistoryObjectsForResource(domainBase.createVKey()))
// sort by modification time descending so we get the most recent one if it was locked twice
.sorted(Comparator.comparing(HistoryEntry::getModificationTime).reversed())
.filter(entry -> entry.getReason().equals("Uniform Rapid Suspension"))
@@ -140,18 +144,14 @@ public class BackfillRegistryLocksCommand extends ConfirmingCommand
}
private ImmutableList<DomainBase> getLockedDomainsWithoutLocks(DateTime now) {
return ImmutableList.copyOf(
ofy()
.load()
.keys(
roids.stream()
.map(roid -> Key.create(DomainBase.class, roid))
.collect(toImmutableList()))
.values()
.stream()
.filter(d -> d.getDeletionTime().isAfter(now))
.filter(d -> d.getStatusValues().containsAll(REGISTRY_LOCK_STATUSES))
.filter(d -> !RegistryLockDao.getMostRecentByRepoId(d.getRepoId()).isPresent())
.collect(toImmutableList()));
ImmutableList<VKey<DomainBase>> domainKeys =
roids.stream().map(roid -> VKey.create(DomainBase.class, roid)).collect(toImmutableList());
ImmutableCollection<DomainBase> domains =
transactIfJpaTm(() -> tm().loadByKeys(domainKeys)).values();
return domains.stream()
.filter(d -> d.getDeletionTime().isAfter(now))
.filter(d -> d.getStatusValues().containsAll(REGISTRY_LOCK_STATUSES))
.filter(d -> RegistryLockDao.getMostRecentByRepoId(d.getRepoId()).isEmpty())
.collect(toImmutableList());
}
}
@@ -39,6 +39,7 @@ import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryEnvironment;
import google.registry.flows.certs.CertificateChecker;
import google.registry.flows.certs.CertificateChecker.InsecureCertificateException;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarContact;
import google.registry.model.registrar.RegistrarContact.Type;
@@ -337,7 +338,11 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA
private boolean validateCertificate(
Optional<String> existingCertificate, String certificateString) {
if (!existingCertificate.isPresent() || !existingCertificate.get().equals(certificateString)) {
try {
certificateChecker.validateCertificate(certificateString);
} catch (InsecureCertificateException e) {
throw new IllegalArgumentException(e.getMessage());
}
return true;
}
return false;
@@ -0,0 +1,183 @@
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.batch;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.eppcommon.StatusValue.PENDING_DELETE;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_CREATE;
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.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import com.google.common.collect.ImmutableSet;
import google.registry.flows.DaggerEppTestComponent;
import google.registry.flows.EppController;
import google.registry.flows.EppTestComponent.FakesAndMocksModule;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Flag;
import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.domain.DomainBase;
import google.registry.model.ofy.Ofy;
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry;
import google.registry.monitoring.whitebox.EppMetric;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeLockHandler;
import google.registry.testing.FakeResponse;
import google.registry.testing.InjectExtension;
import java.util.Optional;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link DeleteExpiredDomainsAction}. */
class DeleteExpiredDomainsActionTest {
@RegisterExtension
public final AppEngineExtension appEngine =
AppEngineExtension.builder().withDatastoreAndCloudSql().withTaskQueue().build();
@RegisterExtension public final InjectExtension inject = new InjectExtension();
private final FakeClock clock = new FakeClock(DateTime.parse("2016-06-13T20:21:22Z"));
private final FakeResponse response = new FakeResponse();
private DeleteExpiredDomainsAction action;
@BeforeEach
void beforeEach() {
inject.setStaticField(Ofy.class, "clock", clock);
createTld("tld");
EppController eppController =
DaggerEppTestComponent.builder()
.fakesAndMocksModule(
FakesAndMocksModule.create(clock, EppMetric.builderForRequest(clock)))
.build()
.startRequest()
.eppController();
action =
new DeleteExpiredDomainsAction(
eppController, "NewRegistrar", clock, new FakeLockHandler(true), response);
}
@Test
void test_deletesOnlyExpiredDomain() {
// A normal, active autorenewing domain that shouldn't be touched.
DomainBase activeDomain = persistActiveDomain("foo.tld");
clock.advanceOneMilli();
// A non-autorenewing domain that is already pending delete and shouldn't be touched.
DomainBase alreadyDeletedDomain =
persistResource(
newDomainBase("bar.tld")
.asBuilder()
.setAutorenewEndTime(Optional.of(clock.nowUtc().minusDays(10)))
.setDeletionTime(clock.nowUtc().plusDays(17))
.build());
clock.advanceOneMilli();
// A non-autorenewing domain that hasn't reached its expiration time and shouldn't be touched.
DomainBase notYetExpiredDomain =
persistResource(
newDomainBase("baz.tld")
.asBuilder()
.setAutorenewEndTime(Optional.of(clock.nowUtc().plusDays(15)))
.build());
clock.advanceOneMilli();
// A non-autorenewing domain that is past its expiration time and should be deleted.
// (This is the only one that needs a full set of subsidiary resources, for the delete flow to
// to operate on.)
DomainBase pendingExpirationDomain = persistNonAutorenewingDomain("fizz.tld");
assertThat(tm().loadByEntity(pendingExpirationDomain).getStatusValues())
.doesNotContain(PENDING_DELETE);
action.run();
DomainBase reloadedActiveDomain = tm().loadByEntity(activeDomain);
assertThat(reloadedActiveDomain).isEqualTo(activeDomain);
assertThat(reloadedActiveDomain.getStatusValues()).doesNotContain(PENDING_DELETE);
assertThat(tm().loadByEntity(alreadyDeletedDomain)).isEqualTo(alreadyDeletedDomain);
assertThat(tm().loadByEntity(notYetExpiredDomain)).isEqualTo(notYetExpiredDomain);
DomainBase reloadedExpiredDomain = tm().loadByEntity(pendingExpirationDomain);
assertThat(reloadedExpiredDomain.getStatusValues()).contains(PENDING_DELETE);
assertThat(reloadedExpiredDomain.getDeletionTime()).isEqualTo(clock.nowUtc().plusDays(35));
}
@Test
void test_deletesThreeDomainsInOneRun() {
DomainBase domain1 = persistNonAutorenewingDomain("ecck1.tld");
DomainBase domain2 = persistNonAutorenewingDomain("veee2.tld");
DomainBase domain3 = persistNonAutorenewingDomain("tarm3.tld");
action.run();
assertThat(tm().loadByEntity(domain1).getStatusValues()).contains(PENDING_DELETE);
assertThat(tm().loadByEntity(domain2).getStatusValues()).contains(PENDING_DELETE);
assertThat(tm().loadByEntity(domain3).getStatusValues()).contains(PENDING_DELETE);
}
private DomainBase persistNonAutorenewingDomain(String domainName) {
DomainBase pendingExpirationDomain = persistActiveDomain(domainName);
HistoryEntry createHistoryEntry =
persistResource(
new HistoryEntry.Builder()
.setType(DOMAIN_CREATE)
.setParent(pendingExpirationDomain)
.setModificationTime(clock.nowUtc().minusMonths(9))
.build());
BillingEvent.Recurring autorenewBillingEvent =
persistResource(createAutorenewBillingEvent(createHistoryEntry).build());
PollMessage.Autorenew autorenewPollMessage =
persistResource(createAutorenewPollMessage(createHistoryEntry).build());
pendingExpirationDomain =
persistResource(
pendingExpirationDomain
.asBuilder()
.setAutorenewEndTime(Optional.of(clock.nowUtc().minusDays(10)))
.setAutorenewBillingEvent(autorenewBillingEvent.createVKey())
.setAutorenewPollMessage(autorenewPollMessage.createVKey())
.build());
clock.advanceOneMilli();
return pendingExpirationDomain;
}
private BillingEvent.Recurring.Builder createAutorenewBillingEvent(
HistoryEntry createHistoryEntry) {
return new BillingEvent.Recurring.Builder()
.setReason(Reason.RENEW)
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
.setTargetId("fizz.tld")
.setClientId("TheRegistrar")
.setEventTime(clock.nowUtc().plusYears(1))
.setRecurrenceEndTime(END_OF_TIME)
.setParent(createHistoryEntry);
}
private PollMessage.Autorenew.Builder createAutorenewPollMessage(
HistoryEntry createHistoryEntry) {
return new PollMessage.Autorenew.Builder()
.setTargetId("fizz.tld")
.setClientId("TheRegistrar")
.setEventTime(clock.nowUtc().plusYears(1))
.setAutorenewEndTime(END_OF_TIME)
.setParent(createHistoryEntry);
}
}
@@ -0,0 +1,152 @@
// 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.beam.datastore;
import static google.registry.beam.datastore.BulkDeletePipeline.discoverEntityKinds;
import static google.registry.beam.datastore.BulkDeletePipeline.getDeletionTags;
import static google.registry.beam.datastore.BulkDeletePipeline.getOneDeletionTag;
import com.google.common.base.Verify;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.datastore.v1.Entity;
import com.google.datastore.v1.Key;
import com.google.datastore.v1.Key.PathElement;
import google.registry.beam.TestPipelineExtension;
import google.registry.beam.datastore.BulkDeletePipeline.GenerateQueries;
import google.registry.beam.datastore.BulkDeletePipeline.SplitEntities;
import java.io.Serializable;
import java.util.Map;
import org.apache.beam.sdk.testing.PAssert;
import org.apache.beam.sdk.transforms.Count;
import org.apache.beam.sdk.transforms.Create;
import org.apache.beam.sdk.transforms.ParDo;
import org.apache.beam.sdk.transforms.View;
import org.apache.beam.sdk.values.KV;
import org.apache.beam.sdk.values.PCollection;
import org.apache.beam.sdk.values.PCollectionTuple;
import org.apache.beam.sdk.values.PCollectionView;
import org.apache.beam.sdk.values.TupleTag;
import org.apache.beam.sdk.values.TupleTagList;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link BulkDeletePipeline}. */
class BulkDeletePipelineTest implements Serializable {
@RegisterExtension
final transient TestPipelineExtension testPipeline =
TestPipelineExtension.create().enableAbandonedNodeEnforcement(true);
@Test
void generateQueries() {
PCollection<String> queries =
testPipeline
.apply("InjectKinds", Create.of("A", "B"))
.apply("GenerateQueries", ParDo.of(new GenerateQueries()));
PAssert.that(queries).containsInAnyOrder("select __key__ from `A`", "select __key__ from `B`");
testPipeline.run();
}
@Test
void mapKindsToTags() {
TupleTagList tags = getDeletionTags(2);
PCollection<String> kinds = testPipeline.apply("InjectKinds", Create.of("A", "B"));
PCollection<KV<String, TupleTag<Entity>>> kindToTagMapping =
BulkDeletePipeline.mapKindsToDeletionTags(kinds, tags);
PAssert.thatMap(kindToTagMapping)
.isEqualTo(
ImmutableMap.of(
"A", new TupleTag<Entity>("0"),
"B", new TupleTag<Entity>("1")));
testPipeline.run();
}
@Test
void mapKindsToTags_fewerKindsThanTags() {
TupleTagList tags = getDeletionTags(3);
PCollection<String> kinds = testPipeline.apply("InjectKinds", Create.of("A", "B"));
PCollection<KV<String, TupleTag<Entity>>> kindToTagMapping =
BulkDeletePipeline.mapKindsToDeletionTags(kinds, tags);
PAssert.thatMap(kindToTagMapping)
.isEqualTo(
ImmutableMap.of(
"A", new TupleTag<Entity>("0"),
"B", new TupleTag<Entity>("1")));
testPipeline.run();
}
@Test
void mapKindsToTags_moreKindsThanTags() {
TupleTagList tags = getDeletionTags(2);
PCollection<String> kinds = testPipeline.apply("InjectKinds", Create.of("A", "B", "C"));
PCollection<KV<String, TupleTag<Entity>>> kindToTagMapping =
BulkDeletePipeline.mapKindsToDeletionTags(kinds, tags);
PAssert.thatMap(kindToTagMapping)
.isEqualTo(
ImmutableMap.of(
"A", new TupleTag<Entity>("0"),
"B", new TupleTag<Entity>("1"),
"C", new TupleTag<Entity>("0")));
testPipeline.run();
}
@Test
void splitEntitiesByKind() {
TupleTagList tags = getDeletionTags(2);
PCollection<String> kinds = testPipeline.apply("InjectKinds", Create.of("A", "B"));
PCollectionView<Map<String, TupleTag<Entity>>> kindToTagMapping =
BulkDeletePipeline.mapKindsToDeletionTags(kinds, tags).apply(View.asMap());
Entity entityA = createTestEntity("A", 1);
Entity entityB = createTestEntity("B", 2);
PCollection<Entity> entities =
testPipeline.apply("InjectEntities", Create.of(entityA, entityB));
PCollectionTuple allCollections =
entities.apply(
"SplitByKind",
ParDo.of(new SplitEntities(kindToTagMapping))
.withSideInputs(kindToTagMapping)
.withOutputTags(getOneDeletionTag("placeholder"), tags));
PAssert.that(allCollections.get((TupleTag<Entity>) tags.get(0))).containsInAnyOrder(entityA);
PAssert.that(allCollections.get((TupleTag<Entity>) tags.get(1))).containsInAnyOrder(entityB);
testPipeline.run();
}
private static Entity createTestEntity(String kind, long id) {
return Entity.newBuilder()
.setKey(Key.newBuilder().addPath(PathElement.newBuilder().setId(id).setKind(kind)))
.build();
}
@Test
@EnabledIfSystemProperty(named = "test.gcp_integration.env", matches = "\\S+")
void discoverKindsFromDatastore() {
String environmentName = System.getProperty("test.gcp_integration.env");
String project = "domain-registry-" + environmentName;
PCollection<String> kinds =
testPipeline.apply("DiscoverEntityKinds", discoverEntityKinds(project));
PAssert.that(kinds.apply(Count.globally()))
.satisfies(
longs -> {
Verify.verify(Iterables.size(longs) == 1 && Iterables.getFirst(longs, -1L) > 0);
return null;
});
testPipeline.run();
}
}
@@ -92,16 +92,10 @@ public final class InitSqlTestUtils {
* VersionedEntities} contains exactly the same elements in the {@code expected} array.
*
* <p>This method makes assertions in the pipeline and only use {@link PAssert} on the result.
* This has two advantages over {@code PAssert}:
*
* <ul>
* <li>It supports assertions on 'containsExactlyElementsIn', which is not available in {@code
* PAssert}.
* <li>It supports assertions on Objectify entities, which {@code PAssert} cannot not do.
* Compared with PAssert-compatible options like {@code google.registry.tools.EntityWrapper}
* or {@link EntityProto}, Objectify entities in Java give better-formatted error messages
* when assertions fail.
* </ul>
* This way it supports assertions on Objectify entities, which {@code PAssert} cannot do ( since
* we have not implemented Coders for them). Compared with PAssert-compatible options like {@code
* google.registry.tools.EntityWrapper} or {@link EntityProto}, Objectify entities in Java give
* better-formatted error messages when assertions fail.
*
* <p>Each {@code expected} {@link KV key-value pair} refers to a versioned state of an Ofy
* entity. The {@link KV#getKey key} is the timestamp, while the {@link KV#getValue value} is
@@ -16,16 +16,36 @@ package google.registry.flows;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.LogsSubject.assertAboutLogs;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static google.registry.util.X509Utils.encodeX509Certificate;
import static google.registry.util.X509Utils.encodeX509CertificateFromPemString;
import static org.joda.time.DateTimeZone.UTC;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.testing.TestLogHandler;
import google.registry.config.RegistryEnvironment;
import google.registry.flows.certs.CertificateChecker;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.CertificateSamples;
import google.registry.testing.SystemPropertyExtension;
import google.registry.util.SelfSignedCaCertificate;
import java.io.StringWriter;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
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;
import org.testcontainers.shaded.org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
import org.testcontainers.shaded.org.bouncycastle.util.io.pem.PemObjectGenerator;
import org.testcontainers.shaded.org.bouncycastle.util.io.pem.PemWriter;
/** Test logging in with TLS credentials. */
class EppLoginTlsTest extends EppTestCase {
@@ -34,18 +54,40 @@ class EppLoginTlsTest extends EppTestCase {
final AppEngineExtension appEngine =
AppEngineExtension.builder().withDatastoreAndCloudSql().build();
void setClientCertificateHash(String clientCertificateHash) {
@RegisterExtension
@Order(value = Integer.MAX_VALUE)
final SystemPropertyExtension systemPropertyExtension = new SystemPropertyExtension();
private final CertificateChecker certificateChecker =
new CertificateChecker(
ImmutableSortedMap.of(START_OF_TIME, 825, DateTime.parse("2020-09-01T00:00:00Z"), 398),
30,
2048,
ImmutableSet.of("secp256r1", "secp384r1"),
clock);
private final Logger loggerToIntercept =
Logger.getLogger(TlsCredentials.class.getCanonicalName());
private final TestLogHandler handler = new TestLogHandler();
private String encodedCertString;
void setCredentials(String clientCertificateHash, String clientCertificate) {
setTransportCredentials(
new TlsCredentials(
true, Optional.ofNullable(clientCertificateHash), Optional.of("192.168.1.100:54321")));
true,
Optional.ofNullable(clientCertificateHash),
Optional.ofNullable(clientCertificate),
Optional.of("192.168.1.100:54321"),
certificateChecker));
}
@BeforeEach
void beforeEach() {
void beforeEach() throws CertificateException {
persistResource(
loadRegistrar("NewRegistrar")
.asBuilder()
.setClientCertificate(CertificateSamples.SAMPLE_CERT, DateTime.now(UTC))
.setClientCertificate(CertificateSamples.SAMPLE_CERT3, DateTime.now(UTC))
.build());
// Set a cert for the second registrar, or else any cert will be allowed for login.
persistResource(
@@ -53,18 +95,20 @@ class EppLoginTlsTest extends EppTestCase {
.asBuilder()
.setClientCertificate(CertificateSamples.SAMPLE_CERT2, DateTime.now(UTC))
.build());
loggerToIntercept.addHandler(handler);
encodedCertString = encodeX509CertificateFromPemString(CertificateSamples.SAMPLE_CERT3);
}
@Test
void testLoginLogout() throws Exception {
setClientCertificateHash(CertificateSamples.SAMPLE_CERT_HASH);
setCredentials(null, encodedCertString);
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
assertThatLogoutSucceeds();
}
@Test
void testLogin_wrongPasswordFails() throws Exception {
setClientCertificateHash(CertificateSamples.SAMPLE_CERT_HASH);
setCredentials(CertificateSamples.SAMPLE_CERT3_HASH, encodedCertString);
// For TLS login, we also check the epp xml password.
assertThatLogin("NewRegistrar", "incorrect")
.hasResponse(
@@ -74,7 +118,7 @@ class EppLoginTlsTest extends EppTestCase {
@Test
void testMultiLogin() throws Exception {
setClientCertificateHash(CertificateSamples.SAMPLE_CERT_HASH);
setCredentials(CertificateSamples.SAMPLE_CERT3_HASH, encodedCertString);
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
assertThatLogoutSucceeds();
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
@@ -88,7 +132,7 @@ class EppLoginTlsTest extends EppTestCase {
@Test
void testNonAuthedLogin_fails() throws Exception {
setClientCertificateHash(CertificateSamples.SAMPLE_CERT_HASH);
setCredentials(CertificateSamples.SAMPLE_CERT3_HASH, encodedCertString);
assertThatLogin("TheRegistrar", "password2")
.hasResponse(
"response_error.xml",
@@ -98,7 +142,7 @@ class EppLoginTlsTest extends EppTestCase {
@Test
void testBadCertificate_failsBadCertificate2200() throws Exception {
setClientCertificateHash("laffo");
setCredentials("laffo", "cert");
assertThatLogin("NewRegistrar", "foo-BAR2")
.hasResponse(
"response_error.xml",
@@ -108,7 +152,7 @@ class EppLoginTlsTest extends EppTestCase {
@Test
void testGfeDidntProvideClientCertificate_failsMissingCertificate2200() throws Exception {
setClientCertificateHash(null);
setCredentials(null, null);
assertThatLogin("NewRegistrar", "foo-BAR2")
.hasResponse(
"response_error.xml",
@@ -117,12 +161,12 @@ class EppLoginTlsTest extends EppTestCase {
@Test
void testGoodPrimaryCertificate() throws Exception {
setClientCertificateHash(CertificateSamples.SAMPLE_CERT_HASH);
setCredentials(null, encodedCertString);
DateTime now = DateTime.now(UTC);
persistResource(
loadRegistrar("NewRegistrar")
.asBuilder()
.setClientCertificate(CertificateSamples.SAMPLE_CERT, now)
.setClientCertificate(CertificateSamples.SAMPLE_CERT3, now)
.setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT2, now)
.build());
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
@@ -130,33 +174,33 @@ class EppLoginTlsTest extends EppTestCase {
@Test
void testGoodFailoverCertificate() throws Exception {
setClientCertificateHash(CertificateSamples.SAMPLE_CERT2_HASH);
setCredentials(null, encodedCertString);
DateTime now = DateTime.now(UTC);
persistResource(
loadRegistrar("NewRegistrar")
.asBuilder()
.setClientCertificate(CertificateSamples.SAMPLE_CERT, now)
.setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT2, now)
.setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT3, now)
.build());
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
}
@Test
void testMissingPrimaryCertificateButHasFailover_usesFailover() throws Exception {
setClientCertificateHash(CertificateSamples.SAMPLE_CERT2_HASH);
setCredentials(null, encodedCertString);
DateTime now = DateTime.now(UTC);
persistResource(
loadRegistrar("NewRegistrar")
.asBuilder()
.setClientCertificate(null, now)
.setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT2, now)
.setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT3, now)
.build());
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
}
@Test
void testRegistrarHasNoCertificatesOnFile_fails() throws Exception {
setClientCertificateHash("laffo");
setCredentials("laffo", "cert");
DateTime now = DateTime.now(UTC);
persistResource(
loadRegistrar("NewRegistrar")
@@ -169,4 +213,145 @@ class EppLoginTlsTest extends EppTestCase {
"response_error.xml",
ImmutableMap.of("CODE", "2200", "MSG", "Registrar certificate is not configured"));
}
@Test
void testCertificateDoesNotMeetRequirements_fails() throws Exception {
String proxyEncoded = encodeX509CertificateFromPemString(CertificateSamples.SAMPLE_CERT);
// SAMPLE_CERT has a validity period that is too long
setCredentials(CertificateSamples.SAMPLE_CERT_HASH, proxyEncoded);
persistResource(
loadRegistrar("NewRegistrar")
.asBuilder()
.setClientCertificate(CertificateSamples.SAMPLE_CERT, clock.nowUtc())
.setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT2, clock.nowUtc())
.build());
assertThatLogin("NewRegistrar", "foo-BAR2")
.hasResponse(
"response_error.xml",
ImmutableMap.of(
"CODE",
"2200",
"MSG",
"Registrar certificate contains the following security violations:\n"
+ "Certificate validity period is too long; it must be less than or equal to"
+ " 398 days."));
}
@Test
void testCertificateDoesNotMeetMultipleRequirements_fails() throws Exception {
X509Certificate certificate =
SelfSignedCaCertificate.create(
"test", clock.nowUtc().plusDays(100), clock.nowUtc().plusDays(5000))
.cert();
StringWriter sw = new StringWriter();
try (PemWriter pw = new PemWriter(sw)) {
PemObjectGenerator generator = new JcaMiscPEMGenerator(certificate);
pw.writeObject(generator);
}
String proxyEncoded = encodeX509Certificate(certificate);
// SAMPLE_CERT has a validity period that is too long
setCredentials(null, proxyEncoded);
persistResource(
loadRegistrar("NewRegistrar")
.asBuilder()
.setClientCertificate(sw.toString(), clock.nowUtc())
.setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT2, clock.nowUtc())
.build());
assertThatLogin("NewRegistrar", "foo-BAR2")
.hasResponse(
"response_error.xml",
ImmutableMap.of(
"CODE",
"2200",
"MSG",
"Registrar certificate contains the following security violations:\n"
+ "Certificate is expired.\n"
+ "Certificate validity period is too long; it must be less than or equal to"
+ " 398 days."));
}
@Test
// TODO(sarahbot@): Remove this test once requirements are enforced in production
void testCertificateDoesNotMeetRequirementsInProduction_succeeds() throws Exception {
RegistryEnvironment.PRODUCTION.setup(systemPropertyExtension);
// SAMPLE_CERT has a validity period that is too long
String proxyEncoded = encodeX509CertificateFromPemString(CertificateSamples.SAMPLE_CERT);
setCredentials(null, proxyEncoded);
persistResource(
loadRegistrar("NewRegistrar")
.asBuilder()
.setClientCertificate(CertificateSamples.SAMPLE_CERT, clock.nowUtc())
.setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT2, clock.nowUtc())
.build());
// Even though the certificate contains security violations, the login will succeed in
// production
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
assertAboutLogs()
.that(handler)
.hasLogAtLevelWithMessage(
Level.WARNING,
"Registrar certificate used for NewRegistrar does not meet certificate requirements:"
+ " Certificate validity period is too long; it must be less than or equal to 398"
+ " days.");
}
@Test
void testRegistrarCertificateContainsExtraMetadata_succeeds() throws Exception {
String certPem =
String.format(
"Bag Attributes\n"
+ " localKeyID: 1F 1C 3A 3A 4C 03 EC C4 BC 7A C3 21 A9 F2 13 66 21 B8 7B 26 \n"
+ "subject=/C=US/ST=New York/L=New"
+ " York/O=Test/OU=ABC/CN=tester.test/emailAddress=test-certificate@test.test\n"
+ "issuer=/C=US/ST=NY/L=NYC/O=ABC/OU=TEST CA/CN=TEST"
+ " CA/emailAddress=testing@test.test\n"
+ "%s",
CertificateSamples.SAMPLE_CERT3);
setCredentials(null, encodeX509CertificateFromPemString(certPem));
DateTime now = DateTime.now(UTC);
persistResource(
loadRegistrar("NewRegistrar")
.asBuilder()
.setClientCertificate(certPem, now)
.setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT2, now)
.build());
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
}
@Test
void testRegistrarCertificateContainsExtraMetadataAndViolations_fails() throws Exception {
String certPem =
String.format(
"Bag Attributes\n"
+ " localKeyID: 1F 1C 3A 3A 4C 03 EC C4 BC 7A C3 21 A9 F2 13 66 21 B8 7B 26 \n"
+ "subject=/C=US/ST=New York/L=New"
+ " York/O=Test/OU=ABC/CN=tester.test/emailAddress=test-certificate@test.test\n"
+ "issuer=/C=US/ST=NY/L=NYC/O=ABC/OU=TEST CA/CN=TEST"
+ " CA/emailAddress=testing@test.test\n"
+ "%s",
CertificateSamples.SAMPLE_CERT);
setCredentials(null, encodeX509CertificateFromPemString(certPem));
DateTime now = DateTime.now(UTC);
persistResource(
loadRegistrar("NewRegistrar")
.asBuilder()
.setClientCertificate(certPem, now)
.setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT2, now)
.build());
assertThatLogin("NewRegistrar", "foo-BAR2")
.hasResponse(
"response_error.xml",
ImmutableMap.of(
"CODE",
"2200",
"MSG",
"Registrar certificate contains the following security violations:\n"
+ "Certificate validity period is too long; it must be less than or equal to"
+ " 398 days."));
}
}
@@ -45,12 +45,8 @@ import javax.inject.Singleton;
/** Dagger component for running EPP tests. */
@Singleton
@Component(
modules = {
ConfigModule.class,
EppTestComponent.FakesAndMocksModule.class
})
interface EppTestComponent {
@Component(modules = {ConfigModule.class, EppTestComponent.FakesAndMocksModule.class})
public interface EppTestComponent {
RequestComponent startRequest();
@@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.common.truth.Truth8.assertThat;
import static google.registry.testing.TestDataHelper.loadFile;
import static google.registry.testing.TestLogHandlerUtils.findFirstLogMessageByPrefix;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -27,8 +28,10 @@ import static org.mockito.Mockito.verify;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.flogger.LoggerConfig;
import com.google.common.testing.TestLogHandler;
import google.registry.flows.certs.CertificateChecker;
import google.registry.model.eppcommon.Trid;
import google.registry.model.eppoutput.EppOutput.ResponseOrGreeting;
import google.registry.model.eppoutput.EppResponse;
@@ -38,6 +41,7 @@ import google.registry.testing.FakeClock;
import google.registry.testing.FakeHttpSession;
import java.util.List;
import java.util.Optional;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
@@ -54,6 +58,16 @@ class FlowRunnerTest {
private final TestLogHandler handler = new TestLogHandler();
protected final FakeClock clock = new FakeClock();
private final CertificateChecker certificateChecker =
new CertificateChecker(
ImmutableSortedMap.of(START_OF_TIME, 825, DateTime.parse("2020-09-01T00:00:00Z"), 398),
30,
2048,
ImmutableSet.of("secp256r1", "secp384r1"),
clock);
static class TestCommandFlow implements Flow {
@Override
public ResponseOrGreeting run() {
@@ -138,10 +152,17 @@ class FlowRunnerTest {
@Test
void testRun_loggingStatement_tlsCredentials() throws Exception {
flowRunner.credentials =
new TlsCredentials(true, Optional.of("abc123def"), Optional.of("127.0.0.1"));
new TlsCredentials(
true,
Optional.of("abc123def"),
Optional.of("cert046F5A3"),
Optional.of("127.0.0.1"),
certificateChecker);
flowRunner.run(eppMetricBuilder);
assertThat(Splitter.on("\n\t").split(findFirstLogMessageByPrefix(handler, "EPP Command\n\t")))
.contains("TlsCredentials{clientCertificateHash=abc123def, clientAddress=/127.0.0.1}");
.contains(
"TlsCredentials{clientCertificate=cert046F5A3, clientCertificateHash=abc123def,"
+ " clientAddress=/127.0.0.1}");
}
@Test
@@ -18,16 +18,20 @@ import static com.google.common.truth.Truth8.assertThat;
import static google.registry.testing.CertificateSamples.SAMPLE_CERT;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.persistResource;
import static org.joda.time.DateTimeZone.UTC;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.flows.TlsCredentials.BadRegistrarIpAddressException;
import google.registry.flows.TlsCredentials.MissingRegistrarCertificateException;
import google.registry.flows.TlsCredentials.RegistrarCertificateNotConfiguredException;
import google.registry.flows.certs.CertificateChecker;
import google.registry.model.registrar.Registrar;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.FakeClock;
import google.registry.util.CidrAddressBlock;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
@@ -42,6 +46,16 @@ final class TlsCredentialsTest {
final AppEngineExtension appEngine =
AppEngineExtension.builder().withDatastoreAndCloudSql().build();
protected final FakeClock clock = new FakeClock();
private final CertificateChecker certificateChecker =
new CertificateChecker(
ImmutableSortedMap.of(START_OF_TIME, 825, DateTime.parse("2020-09-01T00:00:00Z"), 398),
30,
2048,
ImmutableSet.of("secp256r1", "secp384r1"),
clock);
@Test
void testProvideClientCertificateHash() {
HttpServletRequest req = mock(HttpServletRequest.class);
@@ -50,12 +64,18 @@ final class TlsCredentialsTest {
}
@Test
void testClientCertificateHash_missing() {
TlsCredentials tls = new TlsCredentials(true, Optional.empty(), Optional.of("192.168.1.1"));
void testClientCertificateAndHash_missing() {
TlsCredentials tls =
new TlsCredentials(
true,
Optional.empty(),
Optional.empty(),
Optional.of("192.168.1.1"),
certificateChecker);
persistResource(
loadRegistrar("TheRegistrar")
.asBuilder()
.setClientCertificate(SAMPLE_CERT, DateTime.now(UTC))
.setClientCertificate(SAMPLE_CERT, clock.nowUtc())
.build());
assertThrows(
MissingRegistrarCertificateException.class,
@@ -64,11 +84,13 @@ final class TlsCredentialsTest {
@Test
void test_missingIpAddress_doesntAllowAccess() {
TlsCredentials tls = new TlsCredentials(false, Optional.of("certHash"), Optional.empty());
TlsCredentials tls =
new TlsCredentials(
false, Optional.of("certHash"), Optional.empty(), Optional.empty(), certificateChecker);
persistResource(
loadRegistrar("TheRegistrar")
.asBuilder()
.setClientCertificate(SAMPLE_CERT, DateTime.now(UTC))
.setClientCertificate(SAMPLE_CERT, clock.nowUtc())
.setIpAddressAllowList(ImmutableSet.of(CidrAddressBlock.create("3.5.8.13")))
.build());
assertThrows(
@@ -79,12 +101,59 @@ final class TlsCredentialsTest {
@Test
void test_validateCertificate_canBeConfiguredToBypassCertHashes() throws Exception {
TlsCredentials tls =
new TlsCredentials(false, Optional.of("certHash"), Optional.of("192.168.1.1"));
new TlsCredentials(
false,
Optional.of("certHash"),
Optional.of("cert"),
Optional.of("192.168.1.1"),
certificateChecker);
persistResource(
loadRegistrar("TheRegistrar")
.asBuilder()
.setClientCertificate(null, DateTime.now(UTC))
.setFailoverClientCertificate(null, DateTime.now(UTC))
.setClientCertificate(null, clock.nowUtc())
.setFailoverClientCertificate(null, clock.nowUtc())
.build());
// This would throw a RegistrarCertificateNotConfiguredException if cert hashes were not
// bypassed
tls.validateCertificate(Registrar.loadByClientId("TheRegistrar").get());
}
@Test
void testProvideClientCertificate() {
HttpServletRequest req = mock(HttpServletRequest.class);
when(req.getHeader("X-SSL-Full-Certificate")).thenReturn("data");
assertThat(TlsCredentials.EppTlsModule.provideClientCertificate(req)).hasValue("data");
}
@Test
void testClientCertificate_notConfigured() {
TlsCredentials tls =
new TlsCredentials(
true,
Optional.of("hash"),
Optional.of(SAMPLE_CERT),
Optional.of("192.168.1.1"),
certificateChecker);
persistResource(loadRegistrar("TheRegistrar").asBuilder().build());
assertThrows(
RegistrarCertificateNotConfiguredException.class,
() -> tls.validateCertificate(Registrar.loadByClientId("TheRegistrar").get()));
}
@Test
void test_validateCertificate_canBeConfiguredToBypassCerts() throws Exception {
TlsCredentials tls =
new TlsCredentials(
false,
Optional.of("certHash"),
Optional.of("cert"),
Optional.of("192.168.1.1"),
certificateChecker);
persistResource(
loadRegistrar("TheRegistrar")
.asBuilder()
.setClientCertificate(null, clock.nowUtc())
.setFailoverClientCertificate(null, clock.nowUtc())
.build());
// This would throw a RegistrarCertificateNotConfiguredException if cert hashes wren't bypassed.
tls.validateCertificate(Registrar.loadByClientId("TheRegistrar").get());
@@ -19,7 +19,6 @@ import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.ContactResourceSubject.assertAboutContacts;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.deleteResource;
import static google.registry.testing.DatabaseHelper.getOnlyPollMessage;
import static google.registry.testing.DatabaseHelper.getPollMessages;
import static google.registry.testing.DatabaseHelper.persistResource;
@@ -41,10 +40,12 @@ import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferResponse;
import google.registry.model.transfer.TransferStatus;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link ContactTransferApproveFlow}. */
@DualDatabaseTest
class ContactTransferApproveFlowTest
extends ContactTransferFlowTestCase<ContactTransferApproveFlow, ContactResource> {
@@ -121,24 +122,24 @@ class ContactTransferApproveFlowTest
runFlow();
}
@Test
@TestOfyAndSql
void testDryRun() throws Exception {
setEppInput("contact_transfer_approve.xml");
dryRunFlowAssertResponse(loadFile("contact_transfer_approve_response.xml"));
}
@Test
@TestOfyAndSql
void testSuccess() throws Exception {
doSuccessfulTest("contact_transfer_approve.xml", "contact_transfer_approve_response.xml");
}
@Test
@TestOfyAndSql
void testSuccess_withAuthinfo() throws Exception {
doSuccessfulTest("contact_transfer_approve_with_authinfo.xml",
"contact_transfer_approve_response.xml");
}
@Test
@TestOfyAndSql
void testFailure_badContactPassword() {
// Change the contact's password so it does not match the password in the file.
contact = persistResource(
@@ -152,7 +153,7 @@ class ContactTransferApproveFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_neverBeenTransferred() {
changeTransferStatus(null);
EppException thrown =
@@ -161,7 +162,7 @@ class ContactTransferApproveFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_clientApproved() {
changeTransferStatus(TransferStatus.CLIENT_APPROVED);
EppException thrown =
@@ -170,7 +171,7 @@ class ContactTransferApproveFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_clientRejected() {
changeTransferStatus(TransferStatus.CLIENT_REJECTED);
EppException thrown =
@@ -179,7 +180,7 @@ class ContactTransferApproveFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_clientCancelled() {
changeTransferStatus(TransferStatus.CLIENT_CANCELLED);
EppException thrown =
@@ -188,7 +189,7 @@ class ContactTransferApproveFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_serverApproved() {
changeTransferStatus(TransferStatus.SERVER_APPROVED);
EppException thrown =
@@ -197,7 +198,7 @@ class ContactTransferApproveFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_serverCancelled() {
changeTransferStatus(TransferStatus.SERVER_CANCELLED);
EppException thrown =
@@ -206,7 +207,7 @@ class ContactTransferApproveFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_gainingClient() {
setClientIdForFlow("NewRegistrar");
EppException thrown =
@@ -215,7 +216,7 @@ class ContactTransferApproveFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_unrelatedClient() {
setClientIdForFlow("ClientZ");
EppException thrown =
@@ -224,7 +225,7 @@ class ContactTransferApproveFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_deletedContact() throws Exception {
contact = persistResource(
contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
@@ -236,9 +237,9 @@ class ContactTransferApproveFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_nonexistentContact() throws Exception {
deleteResource(contact);
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
contact = persistResource(
contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
ResourceDoesNotExistException thrown =
@@ -249,7 +250,7 @@ class ContactTransferApproveFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testIcannActivityReportField_getsLogged() throws Exception {
runFlow();
assertIcannReportingActivityFieldLogged("srs-cont-transfer-approve");
@@ -18,7 +18,6 @@ import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.ContactResourceSubject.assertAboutContacts;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.deleteResource;
import static google.registry.testing.DatabaseHelper.getOnlyPollMessage;
import static google.registry.testing.DatabaseHelper.getPollMessages;
import static google.registry.testing.DatabaseHelper.persistResource;
@@ -38,10 +37,12 @@ import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferResponse;
import google.registry.model.transfer.TransferStatus;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link ContactTransferCancelFlow}. */
@DualDatabaseTest
class ContactTransferCancelFlowTest
extends ContactTransferFlowTestCase<ContactTransferCancelFlow, ContactResource> {
@@ -105,24 +106,24 @@ class ContactTransferCancelFlowTest
runFlow();
}
@Test
@TestOfyAndSql
void testDryRun() throws Exception {
setEppInput("contact_transfer_cancel.xml");
dryRunFlowAssertResponse(loadFile("contact_transfer_cancel_response.xml"));
}
@Test
@TestOfyAndSql
void testSuccess() throws Exception {
doSuccessfulTest("contact_transfer_cancel.xml", "contact_transfer_cancel_response.xml");
}
@Test
@TestOfyAndSql
void testSuccess_withAuthinfo() throws Exception {
doSuccessfulTest("contact_transfer_cancel_with_authinfo.xml",
"contact_transfer_cancel_response.xml");
}
@Test
@TestOfyAndSql
void testFailure_badContactPassword() {
// Change the contact's password so it does not match the password in the file.
contact =
@@ -138,7 +139,7 @@ class ContactTransferCancelFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_neverBeenTransferred() {
changeTransferStatus(null);
EppException thrown =
@@ -147,7 +148,7 @@ class ContactTransferCancelFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_clientApproved() {
changeTransferStatus(TransferStatus.CLIENT_APPROVED);
EppException thrown =
@@ -156,7 +157,7 @@ class ContactTransferCancelFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_clientRejected() {
changeTransferStatus(TransferStatus.CLIENT_REJECTED);
EppException thrown =
@@ -165,7 +166,7 @@ class ContactTransferCancelFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_clientCancelled() {
changeTransferStatus(TransferStatus.CLIENT_CANCELLED);
EppException thrown =
@@ -174,7 +175,7 @@ class ContactTransferCancelFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_serverApproved() {
changeTransferStatus(TransferStatus.SERVER_APPROVED);
EppException thrown =
@@ -183,7 +184,7 @@ class ContactTransferCancelFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_serverCancelled() {
changeTransferStatus(TransferStatus.SERVER_CANCELLED);
EppException thrown =
@@ -192,7 +193,7 @@ class ContactTransferCancelFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_sponsoringClient() {
setClientIdForFlow("TheRegistrar");
EppException thrown =
@@ -202,7 +203,7 @@ class ContactTransferCancelFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_unrelatedClient() {
setClientIdForFlow("ClientZ");
EppException thrown =
@@ -212,7 +213,7 @@ class ContactTransferCancelFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_deletedContact() throws Exception {
contact =
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
@@ -224,9 +225,9 @@ class ContactTransferCancelFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_nonexistentContact() throws Exception {
deleteResource(contact);
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
ResourceDoesNotExistException thrown =
assertThrows(
ResourceDoesNotExistException.class,
@@ -235,7 +236,7 @@ class ContactTransferCancelFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testIcannActivityReportField_getsLogged() throws Exception {
runFlow();
assertIcannReportingActivityFieldLogged("srs-cont-transfer-cancel");
@@ -17,7 +17,6 @@ package google.registry.flows.contact;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.ContactResourceSubject.assertAboutContacts;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.deleteResource;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -32,11 +31,13 @@ import google.registry.model.contact.ContactResource;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.TransferStatus;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link ContactTransferQueryFlow}. */
@DualDatabaseTest
class ContactTransferQueryFlowTest
extends ContactTransferFlowTestCase<ContactTransferQueryFlow, ContactResource> {
@@ -68,65 +69,65 @@ class ContactTransferQueryFlowTest
runFlow();
}
@Test
@TestOfyAndSql
void testSuccess() throws Exception {
doSuccessfulTest("contact_transfer_query.xml", "contact_transfer_query_response.xml");
}
@Test
@TestOfyAndSql
void testSuccess_withContactRoid() throws Exception {
doSuccessfulTest("contact_transfer_query_with_roid.xml", "contact_transfer_query_response.xml");
}
@Test
@TestOfyAndSql
void testSuccess_sponsoringClient() throws Exception {
setClientIdForFlow("TheRegistrar");
doSuccessfulTest("contact_transfer_query.xml", "contact_transfer_query_response.xml");
}
@Test
@TestOfyAndSql
void testSuccess_withAuthinfo() throws Exception {
setClientIdForFlow("ClientZ");
doSuccessfulTest("contact_transfer_query_with_authinfo.xml",
"contact_transfer_query_response.xml");
}
@Test
@TestOfyAndSql
void testSuccess_clientApproved() throws Exception {
changeTransferStatus(TransferStatus.CLIENT_APPROVED);
doSuccessfulTest("contact_transfer_query.xml",
"contact_transfer_query_response_client_approved.xml");
}
@Test
@TestOfyAndSql
void testSuccess_clientRejected() throws Exception {
changeTransferStatus(TransferStatus.CLIENT_REJECTED);
doSuccessfulTest("contact_transfer_query.xml",
"contact_transfer_query_response_client_rejected.xml");
}
@Test
@TestOfyAndSql
void testSuccess_clientCancelled() throws Exception {
changeTransferStatus(TransferStatus.CLIENT_CANCELLED);
doSuccessfulTest("contact_transfer_query.xml",
"contact_transfer_query_response_client_cancelled.xml");
}
@Test
@TestOfyAndSql
void testSuccess_serverApproved() throws Exception {
changeTransferStatus(TransferStatus.SERVER_APPROVED);
doSuccessfulTest("contact_transfer_query.xml",
"contact_transfer_query_response_server_approved.xml");
}
@Test
@TestOfyAndSql
void testSuccess_serverCancelled() throws Exception {
changeTransferStatus(TransferStatus.SERVER_CANCELLED);
doSuccessfulTest("contact_transfer_query.xml",
"contact_transfer_query_response_server_cancelled.xml");
}
@Test
@TestOfyAndSql
void testFailure_pendingDeleteContact() throws Exception {
changeTransferStatus(TransferStatus.SERVER_CANCELLED);
contact = persistResource(
@@ -135,7 +136,7 @@ class ContactTransferQueryFlowTest
"contact_transfer_query_response_server_cancelled.xml");
}
@Test
@TestOfyAndSql
void testFailure_badContactPassword() {
// Change the contact's password so it does not match the password in the file.
contact =
@@ -151,7 +152,7 @@ class ContactTransferQueryFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_badContactRoid() {
// Set the contact to a different ROID, but don't persist it; this is just so the substitution
// code above will write the wrong ROID into the file.
@@ -163,7 +164,7 @@ class ContactTransferQueryFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_neverBeenTransferred() {
changeTransferStatus(null);
EppException thrown =
@@ -173,7 +174,7 @@ class ContactTransferQueryFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_unrelatedClient() {
setClientIdForFlow("ClientZ");
EppException thrown =
@@ -183,7 +184,7 @@ class ContactTransferQueryFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_deletedContact() throws Exception {
contact =
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
@@ -194,9 +195,9 @@ class ContactTransferQueryFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_nonexistentContact() throws Exception {
deleteResource(contact);
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
ResourceDoesNotExistException thrown =
assertThrows(
ResourceDoesNotExistException.class, () -> doFailingTest("contact_transfer_query.xml"));
@@ -204,7 +205,7 @@ class ContactTransferQueryFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testIcannActivityReportField_getsLogged() throws Exception {
runFlow();
assertIcannReportingActivityFieldLogged("srs-cont-transfer-query");
@@ -18,7 +18,6 @@ import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.ContactResourceSubject.assertAboutContacts;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.deleteResource;
import static google.registry.testing.DatabaseHelper.getOnlyPollMessage;
import static google.registry.testing.DatabaseHelper.getPollMessages;
import static google.registry.testing.DatabaseHelper.persistResource;
@@ -40,10 +39,12 @@ import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferResponse;
import google.registry.model.transfer.TransferStatus;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link ContactTransferRejectFlow}. */
@DualDatabaseTest
class ContactTransferRejectFlowTest
extends ContactTransferFlowTestCase<ContactTransferRejectFlow, ContactResource> {
@@ -120,24 +121,24 @@ class ContactTransferRejectFlowTest
runFlow();
}
@Test
@TestOfyAndSql
void testDryRun() throws Exception {
setEppInput("contact_transfer_reject.xml");
dryRunFlowAssertResponse(loadFile("contact_transfer_reject_response.xml"));
}
@Test
@TestOfyAndSql
void testSuccess() throws Exception {
doSuccessfulTest("contact_transfer_reject.xml", "contact_transfer_reject_response.xml");
}
@Test
@TestOfyAndSql
void testSuccess_domainAuthInfo() throws Exception {
doSuccessfulTest("contact_transfer_reject_with_authinfo.xml",
"contact_transfer_reject_response.xml");
}
@Test
@TestOfyAndSql
void testFailure_badPassword() {
// Change the contact's password so it does not match the password in the file.
contact =
@@ -153,7 +154,7 @@ class ContactTransferRejectFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_neverBeenTransferred() {
changeTransferStatus(null);
EppException thrown =
@@ -162,7 +163,7 @@ class ContactTransferRejectFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_clientApproved() {
changeTransferStatus(TransferStatus.CLIENT_APPROVED);
EppException thrown =
@@ -171,7 +172,7 @@ class ContactTransferRejectFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_clientRejected() {
changeTransferStatus(TransferStatus.CLIENT_REJECTED);
EppException thrown =
@@ -180,7 +181,7 @@ class ContactTransferRejectFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_clientCancelled() {
changeTransferStatus(TransferStatus.CLIENT_CANCELLED);
EppException thrown =
@@ -189,7 +190,7 @@ class ContactTransferRejectFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_serverApproved() {
changeTransferStatus(TransferStatus.SERVER_APPROVED);
EppException thrown =
@@ -198,7 +199,7 @@ class ContactTransferRejectFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_serverCancelled() {
changeTransferStatus(TransferStatus.SERVER_CANCELLED);
EppException thrown =
@@ -207,7 +208,7 @@ class ContactTransferRejectFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_gainingClient() {
setClientIdForFlow("NewRegistrar");
EppException thrown =
@@ -216,7 +217,7 @@ class ContactTransferRejectFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_unrelatedClient() {
setClientIdForFlow("ClientZ");
EppException thrown =
@@ -225,7 +226,7 @@ class ContactTransferRejectFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_deletedContact() throws Exception {
contact =
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
@@ -237,9 +238,9 @@ class ContactTransferRejectFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_nonexistentContact() throws Exception {
deleteResource(contact);
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
ResourceDoesNotExistException thrown =
assertThrows(
ResourceDoesNotExistException.class,
@@ -248,7 +249,7 @@ class ContactTransferRejectFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testIcannActivityReportField_getsLogged() throws Exception {
runFlow();
assertIcannReportingActivityFieldLogged("srs-cont-transfer-reject");
@@ -20,7 +20,8 @@ import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.config.RegistryConfig.getContactAutomaticTransferLength;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import static google.registry.testing.ContactResourceSubject.assertAboutContacts;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.assertPollMessagesEqual;
@@ -29,11 +30,11 @@ import static google.registry.testing.DatabaseHelper.getPollMessages;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
import static google.registry.util.CollectionUtils.forceEmptyToNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.googlecode.objectify.Key;
import google.registry.flows.EppException;
import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
@@ -50,12 +51,13 @@ import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.ContactTransferData;
import google.registry.model.transfer.TransferStatus;
import google.registry.persistence.VKey;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link ContactTransferRequestFlow}. */
@DualDatabaseTest
class ContactTransferRequestFlowTest
extends ContactTransferFlowTestCase<ContactTransferRequestFlow, ContactResource> {
@@ -102,7 +104,8 @@ class ContactTransferRequestFlowTest
.setPendingTransferExpirationTime(afterTransfer)
// Make the server-approve entities field a no-op comparison; it's easier to
// do this comparison separately below.
.setServerApproveEntities(contact.getTransferData().getServerApproveEntities())
.setServerApproveEntities(
forceEmptyToNull(contact.getTransferData().getServerApproveEntities()))
.build());
assertNoBillingEvents();
assertThat(getPollMessages("TheRegistrar", clock.nowUtc())).hasSize(1);
@@ -126,13 +129,8 @@ class ContactTransferRequestFlowTest
// poll messages, the approval notice ones for gaining and losing registrars.
assertPollMessagesEqual(
Iterables.filter(
ofy()
.load()
// Use toArray() to coerce the type to something keys() will accept.
.keys(
contact.getTransferData().getServerApproveEntities().stream()
.map(VKey::getOfyKey)
.toArray(Key[]::new))
transactIfJpaTm(
() -> tm().loadByKeys(contact.getTransferData().getServerApproveEntities()))
.values(),
PollMessage.class),
ImmutableList.of(gainingApproveMessage, losingApproveMessage));
@@ -145,18 +143,18 @@ class ContactTransferRequestFlowTest
runFlow();
}
@Test
@TestOfyAndSql
void testDryRun() throws Exception {
setEppInput("contact_transfer_request.xml");
dryRunFlowAssertResponse(loadFile("contact_transfer_request_response.xml"));
}
@Test
@TestOfyAndSql
void testSuccess() throws Exception {
doSuccessfulTest("contact_transfer_request.xml", "contact_transfer_request_response.xml");
}
@Test
@TestOfyAndSql
void testFailure_noAuthInfo() {
EppException thrown =
assertThrows(
@@ -165,7 +163,7 @@ class ContactTransferRequestFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_badPassword() {
// Change the contact's password so it does not match the password in the file.
contact =
@@ -181,37 +179,37 @@ class ContactTransferRequestFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testSuccess_clientApproved() throws Exception {
changeTransferStatus(TransferStatus.CLIENT_APPROVED);
doSuccessfulTest("contact_transfer_request.xml", "contact_transfer_request_response.xml");
}
@Test
@TestOfyAndSql
void testSuccess_clientRejected() throws Exception {
changeTransferStatus(TransferStatus.CLIENT_REJECTED);
doSuccessfulTest("contact_transfer_request.xml", "contact_transfer_request_response.xml");
}
@Test
@TestOfyAndSql
void testSuccess_clientCancelled() throws Exception {
changeTransferStatus(TransferStatus.CLIENT_CANCELLED);
doSuccessfulTest("contact_transfer_request.xml", "contact_transfer_request_response.xml");
}
@Test
@TestOfyAndSql
void testSuccess_serverApproved() throws Exception {
changeTransferStatus(TransferStatus.SERVER_APPROVED);
doSuccessfulTest("contact_transfer_request.xml", "contact_transfer_request_response.xml");
}
@Test
@TestOfyAndSql
void testSuccess_serverCancelled() throws Exception {
changeTransferStatus(TransferStatus.SERVER_CANCELLED);
doSuccessfulTest("contact_transfer_request.xml", "contact_transfer_request_response.xml");
}
@Test
@TestOfyAndSql
void testFailure_pending() {
contact =
persistResource(
@@ -232,7 +230,7 @@ class ContactTransferRequestFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_sponsoringClient() {
setClientIdForFlow("TheRegistrar");
EppException thrown =
@@ -242,7 +240,7 @@ class ContactTransferRequestFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_deletedContact() throws Exception {
contact =
persistResource(contact.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
@@ -254,7 +252,7 @@ class ContactTransferRequestFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_nonexistentContact() throws Exception {
deleteResource(contact);
ResourceDoesNotExistException thrown =
@@ -265,7 +263,7 @@ class ContactTransferRequestFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_clientTransferProhibited() {
contact =
persistResource(
@@ -278,7 +276,7 @@ class ContactTransferRequestFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_serverTransferProhibited() {
contact =
persistResource(
@@ -291,7 +289,7 @@ class ContactTransferRequestFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testFailure_pendingDelete() {
contact =
persistResource(contact.asBuilder().addStatusValue(StatusValue.PENDING_DELETE).build());
@@ -303,7 +301,7 @@ class ContactTransferRequestFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
@TestOfyAndSql
void testIcannActivityReportField_getsLogged() throws Exception {
runFlow();
assertIcannReportingActivityFieldLogged("srs-cont-transfer-request");
@@ -184,7 +184,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
@Order(value = Order.DEFAULT - 2)
@RegisterExtension
final ReplayExtension replayExtension = new ReplayExtension(clock);
final ReplayExtension replayExtension = ReplayExtension.createWithCompare(clock);
DomainCreateFlowTest() {
setEppInput("domain_create.xml", ImmutableMap.of("DOMAIN", "example.tld"));
@@ -525,8 +525,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
clock.advanceOneMilli();
runFlow();
assertSuccessfulCreate("tld", ImmutableSet.of(), token);
HistoryEntry historyEntry =
ofy().load().type(HistoryEntry.class).ancestor(reloadResourceByForeignKey()).first().now();
HistoryEntry historyEntry = getHistoryEntries(reloadResourceByForeignKey()).get(0);
assertThat(ofy().load().entity(token).now().getRedemptionHistoryEntry())
.hasValue(HistoryEntry.createVKey(Key.create(historyEntry)));
}
@@ -112,7 +112,7 @@ class DomainDeleteFlowTest extends ResourceFlowTestCase<DomainDeleteFlow, Domain
@Order(value = Order.DEFAULT - 2)
@RegisterExtension
final ReplayExtension replayExtension = new ReplayExtension(clock);
final ReplayExtension replayExtension = ReplayExtension.createWithoutCompare(clock);
private DomainBase domain;
private HistoryEntry earlierHistoryEntry;
@@ -108,7 +108,7 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, DomainBa
@Order(value = Order.DEFAULT - 2)
@RegisterExtension
final ReplayExtension replayExtension = new ReplayExtension(clock);
final ReplayExtension replayExtension = ReplayExtension.createWithoutCompare(clock);
@BeforeEach
void initDomainTest() {
@@ -117,7 +117,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
@Order(value = Order.DEFAULT - 2)
@RegisterExtension
final ReplayExtension replayExtension = new ReplayExtension(clock);
final ReplayExtension replayExtension = ReplayExtension.createWithoutCompare(clock);
@BeforeEach
void initDomainTest() {
@@ -919,7 +919,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
.addStatusValue(SERVER_UPDATE_PROHIBITED)
.build());
Exception e = assertThrows(ResourceStatusProhibitsOperationException.class, this::runFlow);
assertThat(e).hasMessageThat().containsMatch("serverUpdateProhibited");
assertThat(e).hasMessageThat().contains("serverUpdateProhibited");
}
@Test
@@ -937,7 +937,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
persistReferencedEntities();
persistDomain();
Exception e = assertThrows(StatusNotClientSettableException.class, this::runFlow);
assertThat(e).hasMessageThat().containsMatch("serverUpdateProhibited");
assertThat(e).hasMessageThat().contains("serverUpdateProhibited");
}
@Test
@@ -174,9 +174,7 @@ class PollAckFlowTest extends FlowTestCase<PollAckFlow> {
void testFailure_noSuchMessage() throws Exception {
assertTransactionalFlow(true);
Exception e = assertThrows(MessageDoesNotExistException.class, this::runFlow);
assertThat(e)
.hasMessageThat()
.containsMatch(String.format("(1-3-EXAMPLE-4-%d-2011)", MESSAGE_ID));
assertThat(e).hasMessageThat().contains(String.format("(1-3-EXAMPLE-4-%d-2011)", MESSAGE_ID));
}
@Test
@@ -255,8 +253,6 @@ class PollAckFlowTest extends FlowTestCase<PollAckFlow> {
.build());
assertTransactionalFlow(true);
Exception e = assertThrows(MessageDoesNotExistException.class, this::runFlow);
assertThat(e)
.hasMessageThat()
.containsMatch(String.format("(1-3-EXAMPLE-4-%d-2011)", MESSAGE_ID));
assertThat(e).hasMessageThat().contains(String.format("(1-3-EXAMPLE-4-%d-2011)", MESSAGE_ID));
}
}
@@ -15,37 +15,66 @@
package google.registry.flows.session;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static google.registry.util.X509Utils.encodeX509CertificateFromPemString;
import static org.joda.time.DateTimeZone.UTC;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.net.InetAddresses;
import google.registry.flows.TlsCredentials;
import google.registry.flows.TlsCredentials.BadRegistrarCertificateException;
import google.registry.flows.TlsCredentials.BadRegistrarIpAddressException;
import google.registry.flows.TlsCredentials.MissingRegistrarCertificateException;
import google.registry.flows.certs.CertificateChecker;
import google.registry.model.registrar.Registrar;
import google.registry.testing.CertificateSamples;
import google.registry.util.CidrAddressBlock;
import google.registry.util.SelfSignedCaCertificate;
import java.io.StringWriter;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.Optional;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.testcontainers.shaded.org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
import org.testcontainers.shaded.org.bouncycastle.util.io.pem.PemObjectGenerator;
import org.testcontainers.shaded.org.bouncycastle.util.io.pem.PemWriter;
/** Unit tests for {@link LoginFlow} when accessed via a TLS transport. */
public class LoginFlowViaTlsTest extends LoginFlowTestCase {
private static final Optional<String> GOOD_CERT =
Optional.of(CertificateSamples.SAMPLE_CERT_HASH);
private static final Optional<String> BAD_CERT =
private static final Optional<String> GOOD_CERT = Optional.of(CertificateSamples.SAMPLE_CERT3);
private static final Optional<String> GOOD_CERT_HASH =
Optional.of(CertificateSamples.SAMPLE_CERT3_HASH);
private static final Optional<String> BAD_CERT = Optional.of(CertificateSamples.SAMPLE_CERT2);
private static final Optional<String> BAD_CERT_HASH =
Optional.of(CertificateSamples.SAMPLE_CERT2_HASH);
private static final Optional<String> GOOD_IP = Optional.of("192.168.1.1");
private static final Optional<String> BAD_IP = Optional.of("1.1.1.1");
private static final Optional<String> GOOD_IPV6 = Optional.of("2001:db8::1");
private static final Optional<String> BAD_IPV6 = Optional.of("2001:db8::2");
private final CertificateChecker certificateChecker =
new CertificateChecker(
ImmutableSortedMap.of(START_OF_TIME, 825, DateTime.parse("2020-09-01T00:00:00Z"), 398),
30,
2048,
ImmutableSet.of("secp256r1", "secp384r1"),
clock);
private Optional<String> encodedCertString;
@BeforeEach
void beforeEach() throws CertificateException {
encodedCertString = Optional.of(encodeX509CertificateFromPemString(GOOD_CERT.get()));
}
@Override
protected Registrar.Builder getRegistrarBuilder() {
return super.getRegistrarBuilder()
.setClientCertificate(CertificateSamples.SAMPLE_CERT, DateTime.now(UTC))
.setClientCertificate(GOOD_CERT.get(), DateTime.now(UTC))
.setIpAddressAllowList(
ImmutableList.of(CidrAddressBlock.create(InetAddresses.forString(GOOD_IP.get()), 32)));
}
@@ -53,7 +82,36 @@ public class LoginFlowViaTlsTest extends LoginFlowTestCase {
@Test
void testSuccess_withGoodCredentials() throws Exception {
persistResource(getRegistrarBuilder().build());
credentials = new TlsCredentials(true, GOOD_CERT, GOOD_IP);
credentials =
new TlsCredentials(true, GOOD_CERT_HASH, encodedCertString, GOOD_IP, certificateChecker);
doSuccessfulTest("login_valid.xml");
}
@Test
void testSuccess_withNewlyConstructedCertificate() throws Exception {
X509Certificate certificate =
SelfSignedCaCertificate.create(
"test", clock.nowUtc().minusDays(100), clock.nowUtc().plusDays(150))
.cert();
StringWriter sw = new StringWriter();
try (PemWriter pw = new PemWriter(sw)) {
PemObjectGenerator generator = new JcaMiscPEMGenerator(certificate);
pw.writeObject(generator);
}
persistResource(
super.getRegistrarBuilder()
.setClientCertificate(sw.toString(), DateTime.now(UTC))
.setIpAddressAllowList(
ImmutableList.of(
CidrAddressBlock.create(InetAddresses.forString(GOOD_IP.get()), 32)))
.build());
String encodedCertificate = Base64.getEncoder().encodeToString(certificate.getEncoded());
credentials =
new TlsCredentials(
true, Optional.empty(), Optional.of(encodedCertificate), GOOD_IP, certificateChecker);
doSuccessfulTest("login_valid.xml");
}
@@ -64,7 +122,8 @@ public class LoginFlowViaTlsTest extends LoginFlowTestCase {
.setIpAddressAllowList(
ImmutableList.of(CidrAddressBlock.create("2001:db8:0:0:0:0:1:1/32")))
.build());
credentials = new TlsCredentials(true, GOOD_CERT, GOOD_IPV6);
credentials =
new TlsCredentials(true, GOOD_CERT_HASH, encodedCertString, GOOD_IPV6, certificateChecker);
doSuccessfulTest("login_valid.xml");
}
@@ -75,7 +134,8 @@ public class LoginFlowViaTlsTest extends LoginFlowTestCase {
.setIpAddressAllowList(
ImmutableList.of(CidrAddressBlock.create("2001:db8:0:0:0:0:1:1/32")))
.build());
credentials = new TlsCredentials(true, GOOD_CERT, GOOD_IPV6);
credentials =
new TlsCredentials(true, GOOD_CERT_HASH, encodedCertString, GOOD_IPV6, certificateChecker);
doSuccessfulTest("login_valid.xml");
}
@@ -85,21 +145,35 @@ public class LoginFlowViaTlsTest extends LoginFlowTestCase {
getRegistrarBuilder()
.setIpAddressAllowList(ImmutableList.of(CidrAddressBlock.create("192.168.1.255/24")))
.build());
credentials = new TlsCredentials(true, GOOD_CERT, GOOD_IP);
credentials =
new TlsCredentials(true, GOOD_CERT_HASH, encodedCertString, GOOD_IP, certificateChecker);
doSuccessfulTest("login_valid.xml");
}
@Test
void testFailure_incorrectClientCertificateHash() {
void testFailure_incorrectClientCertificateHash() throws Exception {
persistResource(getRegistrarBuilder().build());
credentials = new TlsCredentials(true, BAD_CERT, GOOD_IP);
String proxyEncoded = encodeX509CertificateFromPemString(BAD_CERT.get());
credentials =
new TlsCredentials(
true, BAD_CERT_HASH, Optional.of(proxyEncoded), GOOD_IP, certificateChecker);
doFailingTest("login_valid.xml", BadRegistrarCertificateException.class);
}
@Test
void testFailure_missingClientCertificateHash() {
// TODO(Sarahbot): This should fail once hash fallback is removed
void testSuccess_missingClientCertificate() throws Exception {
persistResource(getRegistrarBuilder().build());
credentials = new TlsCredentials(true, Optional.empty(), GOOD_IP);
credentials =
new TlsCredentials(true, GOOD_CERT_HASH, Optional.empty(), GOOD_IP, certificateChecker);
doSuccessfulTest("login_valid.xml");
}
@Test
void testFailure_missingClientCertificateAndHash() {
persistResource(getRegistrarBuilder().build());
credentials =
new TlsCredentials(true, Optional.empty(), Optional.empty(), GOOD_IP, certificateChecker);
doFailingTest("login_valid.xml", MissingRegistrarCertificateException.class);
}
@@ -112,7 +186,8 @@ public class LoginFlowViaTlsTest extends LoginFlowTestCase {
CidrAddressBlock.create(InetAddresses.forString("192.168.1.1"), 32),
CidrAddressBlock.create(InetAddresses.forString("2001:db8::1"), 128)))
.build());
credentials = new TlsCredentials(true, GOOD_CERT, Optional.empty());
credentials =
new TlsCredentials(true, GOOD_CERT_HASH, GOOD_CERT, Optional.empty(), certificateChecker);
doFailingTest("login_valid.xml", BadRegistrarIpAddressException.class);
}
@@ -125,7 +200,7 @@ public class LoginFlowViaTlsTest extends LoginFlowTestCase {
CidrAddressBlock.create(InetAddresses.forString("192.168.1.1"), 32),
CidrAddressBlock.create(InetAddresses.forString("2001:db8::1"), 128)))
.build());
credentials = new TlsCredentials(true, GOOD_CERT, BAD_IP);
credentials = new TlsCredentials(true, GOOD_CERT_HASH, GOOD_CERT, BAD_IP, certificateChecker);
doFailingTest("login_valid.xml", BadRegistrarIpAddressException.class);
}
@@ -138,7 +213,7 @@ public class LoginFlowViaTlsTest extends LoginFlowTestCase {
CidrAddressBlock.create(InetAddresses.forString("192.168.1.1"), 32),
CidrAddressBlock.create(InetAddresses.forString("2001:db8::1"), 128)))
.build());
credentials = new TlsCredentials(true, GOOD_CERT, BAD_IPV6);
credentials = new TlsCredentials(true, GOOD_CERT_HASH, GOOD_CERT, BAD_IPV6, certificateChecker);
doFailingTest("login_valid.xml", BadRegistrarIpAddressException.class);
}
}
@@ -14,19 +14,30 @@
package google.registry.model;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.truth.Truth.assertAbout;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.truth.Correspondence;
import com.google.common.truth.Correspondence.BinaryPredicate;
import com.google.common.truth.FailureMetadata;
import com.google.common.truth.SimpleSubjectBuilder;
import com.google.common.truth.Subject;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collector;
import javax.annotation.Nullable;
/** Truth subject for asserting things about ImmutableObjects that are not built in. */
@@ -53,6 +64,304 @@ public final class ImmutableObjectSubject extends Subject {
}
}
/**
* Checks that {@code expected} has the same contents as {@code actual} except for fields that are
* marked with {@link ImmutableObject.DoNotCompare}.
*
* <p>This is used to verify that entities stored in both cloud SQL and Datastore are identical.
*/
public void isEqualAcrossDatabases(@Nullable ImmutableObject expected) {
ComparisonResult result =
checkObjectAcrossDatabases(
actual, expected, actual != null ? actual.getClass().getName() : "null");
if (result.isFailure()) {
throw new AssertionError(result.getMessage());
}
}
// The following "check" methods implement a recursive check of immutable object equality across
// databases. All of them function in both assertive and predicate modes: if "path" is
// provided (not null) then they throw AssertionError's with detailed error messages. If
// it is null, they return true for equal objects and false for inequal ones.
//
// The reason for this dual-mode behavior is that all of these methods can either be used in the
// context of a test assertion (in which case we want a detailed error message describing exactly
// the location in a complex object where a difference was discovered) or in the context of a
// membership check in a set (in which case we don't care about the specific location of the first
// difference, we just want to be able to determine if the object "is equal to" another object as
// efficiently as possible -- see checkSetAcrossDatabase()).
@VisibleForTesting
static ComparisonResult checkObjectAcrossDatabases(
@Nullable Object actual, @Nullable Object expected, @Nullable String path) {
if (Objects.equals(actual, expected)) {
return ComparisonResult.createSuccess();
}
// They're different, do a more detailed comparison.
// Check for null first (we can assume both variables are not null at this point).
if (actual == null) {
return ComparisonResult.createFailure(path, "expected ", expected, "got null.");
} else if (expected == null) {
return ComparisonResult.createFailure(path, "expected null, got ", actual);
// For immutable objects, we have to recurse since the contained
// object could have DoNotCompare fields, too.
} else if (expected instanceof ImmutableObject) {
// We only verify that actual is an ImmutableObject so we get a good error message instead
// of a context-less ClassCastException.
if (!(actual instanceof ImmutableObject)) {
return ComparisonResult.createFailure(path, actual, " is not an immutable object.");
}
return checkImmutableAcrossDatabases(
(ImmutableObject) actual, (ImmutableObject) expected, path);
} else if (expected instanceof Map) {
if (!(actual instanceof Map)) {
return ComparisonResult.createFailure(path, actual, " is not a Map.");
}
// This would likely be more efficient if we could assume that keys can be compared across
// databases using .equals(), however we cannot guarantee key equality so the simplest and
// most correct way to accomplish this is by reusing the set comparison.
return checkSetAcrossDatabases(
((Map<?, ?>) actual).entrySet(), ((Map<?, ?>) expected).entrySet(), path, "Map");
} else if (expected instanceof Set) {
if (!(actual instanceof Set)) {
return ComparisonResult.createFailure(path, actual, " is not a Set.");
}
return checkSetAcrossDatabases((Set<?>) actual, (Set<?>) expected, path, "Set");
} else if (expected instanceof Collection) {
if (!(actual instanceof Collection)) {
return ComparisonResult.createFailure(path, actual, " is not a Collection.");
}
return checkListAcrossDatabases((Collection<?>) actual, (Collection<?>) expected, path);
// Give Map.Entry special treatment to facilitate the use of Set comparison for verification
// of Map.
} else if (expected instanceof Map.Entry) {
if (!(actual instanceof Map.Entry)) {
return ComparisonResult.createFailure(path, actual, " is not a Map.Entry.");
}
// Check both the key and value. We can always ignore the path here, this should only be
// called from within a set comparison.
ComparisonResult result;
if ((result =
checkObjectAcrossDatabases(
((Map.Entry<?, ?>) actual).getKey(), ((Map.Entry<?, ?>) expected).getKey(), null))
.isFailure()) {
return result;
}
if ((result =
checkObjectAcrossDatabases(
((Map.Entry<?, ?>) actual).getValue(),
((Map.Entry<?, ?>) expected).getValue(),
null))
.isFailure()) {
return result;
}
} else {
// Since we know that the objects are not equal and since any other types can not be expected
// to contain DoNotCompare elements, this condition is always a failure.
return ComparisonResult.createFailure(path, actual, " is not equal to ", expected);
}
return ComparisonResult.createSuccess();
}
private static ComparisonResult checkSetAcrossDatabases(
Set<?> actual, Set<?> expected, String path, String type) {
// Unfortunately, we can't just check to see whether one set "contains" all of the elements of
// the other, as the cross database checks don't require strict equality. Instead we have to do
// an N^2 comparison to search for an equivalent element.
// Objects in expected that aren't in actual. We use "identity sets" here and below because we
// want to keep track of the _objects themselves_ rather than rely upon any overridable notion
// of equality.
Set<Object> missing = path != null ? Sets.newIdentityHashSet() : null;
// Objects from actual that have matching elements in expected.
Set<Object> found = Sets.newIdentityHashSet();
// Build missing and found.
for (Object expectedElem : expected) {
boolean gotMatch = false;
for (Object actualElem : actual) {
if (!checkObjectAcrossDatabases(actualElem, expectedElem, null).isFailure()) {
gotMatch = true;
// Add the element to the set of expected elements that were "found" in actual. If the
// element matches multiple elements in "expected," we have a basic problem with this
// kind of set that we'll want to know about.
if (!found.add(actualElem)) {
return ComparisonResult.createFailure(
path, "element ", actualElem, " matches multiple elements in ", expected);
}
break;
}
}
if (!gotMatch) {
if (path == null) {
return ComparisonResult.createFailure();
}
missing.add(expectedElem);
}
}
if (path != null) {
// Provide a detailed message consisting of any missing or unexpected items.
// Build a set of all objects in actual that don't have counterparts in expected.
Set<Object> unexpected =
actual.stream()
.filter(actualElem -> !found.contains(actualElem))
.collect(
Collector.of(
Sets::newIdentityHashSet,
Set::add,
(result, values) -> {
result.addAll(values);
return result;
}));
if (!missing.isEmpty() || !unexpected.isEmpty()) {
String message = type + " does not contain the expected contents.";
if (!missing.isEmpty()) {
message += " It is missing: " + formatItems(missing.iterator());
}
if (!unexpected.isEmpty()) {
message += " It contains additional elements: " + formatItems(unexpected.iterator());
}
return ComparisonResult.createFailure(path, message);
}
// We just need to check if there were any objects in "actual" that were not in "expected"
// (where "found" is a proxy for "expected").
} else if (actual.stream().anyMatch(Predicate.not(found::contains))) {
return ComparisonResult.createFailure();
}
return ComparisonResult.createSuccess();
}
private static ComparisonResult checkListAcrossDatabases(
Collection<?> actual, Collection<?> expected, @Nullable String path) {
Iterator<?> actualIter = actual.iterator();
Iterator<?> expectedIter = expected.iterator();
int index = 0;
while (actualIter.hasNext() && expectedIter.hasNext()) {
Object actualItem = actualIter.next();
Object expectedItem = expectedIter.next();
ComparisonResult result =
checkObjectAcrossDatabases(
actualItem, expectedItem, path != null ? path + "[" + index + "]" : null);
if (result.isFailure()) {
return result;
}
++index;
}
if (actualIter.hasNext()) {
return ComparisonResult.createFailure(
path, "has additional items: ", formatItems(actualIter));
}
if (expectedIter.hasNext()) {
return ComparisonResult.createFailure(path, "missing items: ", formatItems(expectedIter));
}
return ComparisonResult.createSuccess();
}
/** Recursive helper for isEqualAcrossDatabases. */
private static ComparisonResult checkImmutableAcrossDatabases(
ImmutableObject actual, ImmutableObject expected, String path) {
Map<Field, Object> actualFields = filterFields(actual, ImmutableObject.DoNotCompare.class);
Map<Field, Object> expectedFields = filterFields(expected, ImmutableObject.DoNotCompare.class);
for (Map.Entry<Field, Object> entry : expectedFields.entrySet()) {
if (!actualFields.containsKey(entry.getKey())) {
return ComparisonResult.createFailure(path, "is missing field ", entry.getKey().getName());
}
// Verify that the field values are the same.
Object expectedFieldValue = entry.getValue();
Object actualFieldValue = actualFields.get(entry.getKey());
ComparisonResult result =
checkObjectAcrossDatabases(
actualFieldValue,
expectedFieldValue,
path != null ? path + "." + entry.getKey().getName() : null);
if (result.isFailure()) {
return result;
}
}
// Check for fields in actual that are not in expected.
for (Map.Entry<Field, Object> entry : actualFields.entrySet()) {
if (!expectedFields.containsKey(entry.getKey())) {
return ComparisonResult.createFailure(
path, "has additional field ", entry.getKey().getName());
}
}
return ComparisonResult.createSuccess();
}
private static String formatItems(Iterator<?> iter) {
return Joiner.on(", ").join(iter);
}
/** Encapsulates success/failure result in recursive comparison with optional error string. */
static class ComparisonResult {
boolean succeeded;
String message;
private ComparisonResult(boolean succeeded, @Nullable String message) {
this.succeeded = succeeded;
this.message = message;
}
static ComparisonResult createFailure() {
return new ComparisonResult(false, null);
}
static ComparisonResult createFailure(@Nullable String path, Object... message) {
return new ComparisonResult(false, "At " + path + ": " + Joiner.on("").join(message));
}
static ComparisonResult createSuccess() {
return new ComparisonResult(true, null);
}
String getMessage() {
checkNotNull(message);
return message;
}
boolean isFailure() {
return !succeeded;
}
}
/**
* Checks that the hash value reported by {@code actual} is correct.
*
* <p>This is used in the replay tests to ensure that hibernate hasn't modified any fields that
* are not marked as @Insignificant while loading.
*/
public void hasCorrectHashValue() {
assertThat(Arrays.hashCode(actual.getSignificantFields().values().toArray()))
.isEqualTo(actual.hashCode());
}
public static Correspondence<ImmutableObject, ImmutableObject> immutableObjectCorrespondence(
String... ignoredFields) {
return Correspondence.from(
@@ -87,7 +396,8 @@ public final class ImmutableObjectSubject extends Subject {
}
}
public static Map<Field, Object> filterFields(ImmutableObject original, String... ignoredFields) {
private static Map<Field, Object> filterFields(
ImmutableObject original, String... ignoredFields) {
ImmutableSet<String> ignoredFieldSet = ImmutableSet.copyOf(ignoredFields);
Map<Field, Object> originalFields = ModelUtils.getFieldValues(original);
// don't use ImmutableMap or a stream->collect model since we can have nulls
@@ -99,4 +409,26 @@ public final class ImmutableObjectSubject extends Subject {
}
return result;
}
/** Filter out fields with the given annotation. */
private static Map<Field, Object> filterFields(
ImmutableObject original, Class<? extends Annotation> annotation) {
Map<Field, Object> originalFields = ModelUtils.getFieldValues(original);
// don't use ImmutableMap or a stream->collect model since we can have nulls
Map<Field, Object> result = new LinkedHashMap<>();
for (Map.Entry<Field, Object> entry : originalFields.entrySet()) {
if (!entry.getKey().isAnnotationPresent(annotation)) {
// Perform any necessary substitutions.
if (entry.getKey().isAnnotationPresent(ImmutableObject.EmptySetToNull.class)
&& entry.getValue() != null
&& ((Set<?>) entry.getValue()).isEmpty()) {
result.put(entry.getKey(), null);
} else {
result.put(entry.getKey(), entry.getValue());
}
}
}
return result;
}
}
@@ -0,0 +1,481 @@
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ImmutableObjectSubject.ComparisonResult;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
import static google.registry.model.ImmutableObjectSubject.checkObjectAcrossDatabases;
import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import org.junit.jupiter.api.Test;
public class ImmutableObjectSubjectTest {
// Unique id to assign to the "ignored" field so that it always gets a unique value.
private static int uniqueId = 0;
@Test
void testCrossDatabase_nulls() {
assertAboutImmutableObjects().that(null).isEqualAcrossDatabases(null);
assertAboutImmutableObjects()
.that(makeTestAtom(null))
.isEqualAcrossDatabases(makeTestAtom(null));
assertThat(checkObjectAcrossDatabases(null, makeTestAtom("foo"), null).isFailure()).isTrue();
assertThat(checkObjectAcrossDatabases(null, makeTestAtom("foo"), null).isFailure()).isTrue();
}
@Test
void testCrossDatabase_equalObjects() {
TestImmutableObject actual = makeTestObj();
assertAboutImmutableObjects().that(actual).isEqualAcrossDatabases(actual);
assertAboutImmutableObjects().that(actual).isEqualAcrossDatabases(makeTestObj());
assertThat(checkObjectAcrossDatabases(makeTestObj(), makeTestObj(), null).isFailure())
.isFalse();
}
@Test
void testCrossDatabase_simpleFieldFailure() {
AssertionError e =
assertThrows(
AssertionError.class,
() ->
assertAboutImmutableObjects()
.that(makeTestObj())
.isEqualAcrossDatabases(makeTestObj().withStringField("bar")));
assertThat(e)
.hasMessageThat()
.contains(
"At google.registry.model.ImmutableObjectSubjectTest$TestImmutableObject.stringField:");
assertThat(
checkObjectAcrossDatabases(makeTestObj(), makeTestObj().withStringField(null), null)
.isFailure())
.isTrue();
}
@Test
void testCrossDatabase_nestedImmutableFailure() {
// Repeat the null checks to verify that the attribute path is preserved.
AssertionError e =
assertThrows(
AssertionError.class,
() ->
assertAboutImmutableObjects()
.that(makeTestObj())
.isEqualAcrossDatabases(makeTestObj().withNested(null)));
assertThat(e)
.hasMessageThat()
.contains(
"At google.registry.model.ImmutableObjectSubjectTest$TestImmutableObject.nested:"
+ " expected null, got TestImmutableObject");
e =
assertThrows(
AssertionError.class,
() ->
assertAboutImmutableObjects()
.that(makeTestObj().withNested(null))
.isEqualAcrossDatabases(makeTestObj()));
assertThat(e)
.hasMessageThat()
.contains(
"At google.registry.model.ImmutableObjectSubjectTest$TestImmutableObject.nested:"
+ " expected TestImmutableObject");
assertThat(e).hasMessageThat().contains("got null.");
// Test with a field difference.
e =
assertThrows(
AssertionError.class,
() ->
assertAboutImmutableObjects()
.that(makeTestObj())
.isEqualAcrossDatabases(
makeTestObj().withNested(makeTestObj().withNested(null))));
assertThat(e)
.hasMessageThat()
.contains(
"At google.registry.model.ImmutableObjectSubjectTest$"
+ "TestImmutableObject.nested.stringField:");
assertThat(
checkObjectAcrossDatabases(makeTestObj(), makeTestObj().withNested(null), null)
.isFailure())
.isTrue();
}
@Test
void testCrossDatabase_listFailure() {
AssertionError e =
assertThrows(
AssertionError.class,
() ->
assertAboutImmutableObjects()
.that(makeTestObj())
.isEqualAcrossDatabases(makeTestObj().withList(null)));
assertThat(e)
.hasMessageThat()
.contains(
"At google.registry.model.ImmutableObjectSubjectTest$" + "TestImmutableObject.list:");
e =
assertThrows(
AssertionError.class,
() ->
assertAboutImmutableObjects()
.that(makeTestObj())
.isEqualAcrossDatabases(
makeTestObj().withList(ImmutableList.of(makeTestAtom("wack")))));
assertThat(e)
.hasMessageThat()
.contains(
"At google.registry.model.ImmutableObjectSubjectTest$"
+ "TestImmutableObject.list[0].stringField:");
e =
assertThrows(
AssertionError.class,
() ->
assertAboutImmutableObjects()
.that(makeTestObj())
.isEqualAcrossDatabases(
makeTestObj()
.withList(
ImmutableList.of(
makeTestAtom("baz"),
makeTestAtom("bot"),
makeTestAtom("boq")))));
assertThat(e)
.hasMessageThat()
.contains(
"At google.registry.model.ImmutableObjectSubjectTest$"
+ "TestImmutableObject.list: missing items");
// Make sure multiple additional items get formatted nicely.
assertThat(e).hasMessageThat().contains("}, TestImmutableObject");
e =
assertThrows(
AssertionError.class,
() ->
assertAboutImmutableObjects()
.that(makeTestObj())
.isEqualAcrossDatabases(makeTestObj().withList(ImmutableList.of())));
assertThat(e)
.hasMessageThat()
.contains(
"At google.registry.model.ImmutableObjectSubjectTest$"
+ "TestImmutableObject.list: has additional items");
assertThat(
checkObjectAcrossDatabases(
makeTestObj(),
makeTestObj()
.withList(ImmutableList.of(makeTestAtom("baz"), makeTestAtom("gauze"))),
null)
.isFailure())
.isTrue();
assertThat(
checkObjectAcrossDatabases(
makeTestObj(), makeTestObj().withList(ImmutableList.of()), null)
.isFailure())
.isTrue();
assertThat(
checkObjectAcrossDatabases(
makeTestObj(),
makeTestObj().withList(ImmutableList.of(makeTestAtom("gauze"))),
null)
.isFailure())
.isTrue();
}
@Test
void testCrossDatabase_setFailure() {
AssertionError e =
assertThrows(
AssertionError.class,
() ->
assertAboutImmutableObjects()
.that(makeTestObj())
.isEqualAcrossDatabases(makeTestObj().withSet(null)));
assertThat(e)
.hasMessageThat()
.contains(
"At google.registry.model.ImmutableObjectSubjectTest$"
+ "TestImmutableObject.set: expected null, got ");
e =
assertThrows(
AssertionError.class,
() ->
assertAboutImmutableObjects()
.that(makeTestObj())
.isEqualAcrossDatabases(
makeTestObj().withSet(ImmutableSet.of(makeTestAtom("jim")))));
assertThat(e)
.hasMessageThat()
.containsMatch(
Pattern.compile(
"Set does not contain the expected contents. "
+ "It is missing: .*jim.* It contains additional elements: .*bob",
Pattern.DOTALL));
// Trickery here to verify that multiple items that both match existing items in the set trigger
// an error: we can add two of the same items because equality for purposes of the set includes
// the DoNotCompare field.
e =
assertThrows(
AssertionError.class,
() ->
assertAboutImmutableObjects()
.that(makeTestObj())
.isEqualAcrossDatabases(
makeTestObj()
.withSet(ImmutableSet.of(makeTestAtom("bob"), makeTestAtom("bob")))));
assertThat(e)
.hasMessageThat()
.containsMatch(
Pattern.compile(
"At google.registry.model.ImmutableObjectSubjectTest\\$TestImmutableObject.set: "
+ "element .*bob.* matches multiple elements in .*bob.*bob",
Pattern.DOTALL));
e =
assertThrows(
AssertionError.class,
() ->
assertAboutImmutableObjects()
.that(
makeTestObj()
.withSet(ImmutableSet.of(makeTestAtom("bob"), makeTestAtom("bob"))))
.isEqualAcrossDatabases(makeTestObj()));
assertThat(e)
.hasMessageThat()
.containsMatch(
Pattern.compile(
"At google.registry.model.ImmutableObjectSubjectTest\\$TestImmutableObject.set: "
+ "Set does not contain the expected contents. It contains additional "
+ "elements: .*bob",
Pattern.DOTALL));
assertThat(
checkObjectAcrossDatabases(
makeTestObj(),
makeTestObj()
.withSet(ImmutableSet.of(makeTestAtom("bob"), makeTestAtom("robert"))),
null)
.isFailure())
.isTrue();
assertThat(
checkObjectAcrossDatabases(
makeTestObj(), makeTestObj().withSet(ImmutableSet.of()), null)
.isFailure())
.isTrue();
assertThat(
checkObjectAcrossDatabases(
makeTestObj(),
makeTestObj()
.withSet(ImmutableSet.of(makeTestAtom("bob"), makeTestAtom("bob"))),
null)
.isFailure())
.isTrue();
// We don't test the case where actual's set contains multiple items matching the single item in
// the expected set: that path is the same as the "additional contents" path.
}
@Test
void testCrossDatabase_mapFailure() {
AssertionError e =
assertThrows(
AssertionError.class,
() ->
assertAboutImmutableObjects()
.that(makeTestObj())
.isEqualAcrossDatabases(makeTestObj().withMap(null)));
assertThat(e)
.hasMessageThat()
.contains(
"At google.registry.model.ImmutableObjectSubjectTest$"
+ "TestImmutableObject.map: expected null, got ");
e =
assertThrows(
AssertionError.class,
() ->
assertAboutImmutableObjects()
.that(makeTestObj())
.isEqualAcrossDatabases(
makeTestObj()
.withMap(ImmutableMap.of(makeTestAtom("difk"), makeTestAtom("difv")))));
assertThat(e)
.hasMessageThat()
.containsMatch(
Pattern.compile(
"Map does not contain the expected contents. "
+ "It is missing: .*difk.*difv.* It contains additional elements: .*key.*val",
Pattern.DOTALL));
assertThat(
checkObjectAcrossDatabases(
makeTestObj(),
makeTestObj()
.withMap(
ImmutableMap.of(
makeTestAtom("key"), makeTestAtom("val"),
makeTestAtom("otherk"), makeTestAtom("otherv"))),
null)
.isFailure())
.isTrue();
assertThat(
checkObjectAcrossDatabases(
makeTestObj(), makeTestObj().withMap(ImmutableMap.of()), null)
.isFailure())
.isTrue();
}
@Test
void testCrossDatabase_typeChecks() {
ComparisonResult result = checkObjectAcrossDatabases("blech", makeTestObj(), "xxx");
assertThat(result.getMessage()).isEqualTo("At xxx: blech is not an immutable object.");
assertThat(result.isFailure()).isTrue();
assertThat(checkObjectAcrossDatabases("blech", makeTestObj(), null).isFailure()).isTrue();
result = checkObjectAcrossDatabases("blech", ImmutableMap.of(), "xxx");
assertThat(result.getMessage()).isEqualTo("At xxx: blech is not a Map.");
assertThat(result.isFailure()).isTrue();
assertThat(checkObjectAcrossDatabases("blech", ImmutableMap.of(), null).isFailure()).isTrue();
result = checkObjectAcrossDatabases("blech", ImmutableList.of(), "xxx");
assertThat(result.getMessage()).isEqualTo("At xxx: blech is not a Collection.");
assertThat(result.isFailure()).isTrue();
assertThat(checkObjectAcrossDatabases("blech", ImmutableList.of(), null).isFailure()).isTrue();
for (ImmutableMap.Entry<String, String> entry : ImmutableMap.of("foo", "bar").entrySet()) {
result = checkObjectAcrossDatabases("blech", entry, "xxx");
assertThat(result.getMessage()).isEqualTo("At xxx: blech is not a Map.Entry.");
assertThat(result.isFailure()).isTrue();
assertThat(checkObjectAcrossDatabases("blech", entry, "xxx").isFailure()).isTrue();
}
}
@Test
void testCrossDatabase_checkAdditionalFields() {
AssertionError e =
assertThrows(
AssertionError.class,
() ->
assertAboutImmutableObjects()
.that(DerivedImmutableObject.create())
.isEqualAcrossDatabases(makeTestAtom(null)));
assertThat(e)
.hasMessageThat()
.contains(
"At google.registry.model.ImmutableObjectSubjectTest$DerivedImmutableObject: "
+ "has additional field extraField");
assertThat(
checkObjectAcrossDatabases(DerivedImmutableObject.create(), makeTestAtom(null), null)
.isFailure())
.isTrue();
}
@Test
void testHasCorrectHashValue() {
TestImmutableObject object = makeTestObj();
assertAboutImmutableObjects().that(object).hasCorrectHashValue();
object.stringField = "changed value!";
assertThrows(
AssertionError.class,
() -> assertAboutImmutableObjects().that(object).hasCorrectHashValue());
}
/** Make a test object with all fields set up. */
TestImmutableObject makeTestObj() {
return TestImmutableObject.create(
"foo",
makeTestAtom("bar"),
ImmutableList.of(makeTestAtom("baz")),
ImmutableSet.of(makeTestAtom("bob")),
ImmutableMap.of(makeTestAtom("key"), makeTestAtom("val")));
}
/** Make a test object without the collection fields. */
TestImmutableObject makeTestAtom(String stringField) {
return TestImmutableObject.create(stringField, null, null, null, null);
}
static class TestImmutableObject extends ImmutableObject {
String stringField;
TestImmutableObject nested;
ImmutableList<TestImmutableObject> list;
ImmutableSet<TestImmutableObject> set;
ImmutableMap<TestImmutableObject, TestImmutableObject> map;
@ImmutableObject.DoNotCompare int ignored;
static TestImmutableObject create(
String stringField,
TestImmutableObject nested,
ImmutableList<TestImmutableObject> list,
ImmutableSet<TestImmutableObject> set,
ImmutableMap<TestImmutableObject, TestImmutableObject> map) {
TestImmutableObject instance = new TestImmutableObject();
instance.stringField = stringField;
instance.nested = nested;
instance.list = list;
instance.set = set;
instance.map = map;
instance.ignored = ++uniqueId;
return instance;
}
TestImmutableObject withStringField(@Nullable String stringField) {
TestImmutableObject result = ImmutableObject.clone(this);
result.stringField = stringField;
return result;
}
TestImmutableObject withNested(@Nullable TestImmutableObject nested) {
TestImmutableObject result = ImmutableObject.clone(this);
result.nested = nested;
return result;
}
TestImmutableObject withList(@Nullable ImmutableList<TestImmutableObject> list) {
TestImmutableObject result = ImmutableObject.clone(this);
result.list = list;
return result;
}
TestImmutableObject withSet(@Nullable ImmutableSet<TestImmutableObject> set) {
TestImmutableObject result = ImmutableObject.clone(this);
result.set = set;
return result;
}
TestImmutableObject withMap(
@Nullable ImmutableMap<TestImmutableObject, TestImmutableObject> map) {
TestImmutableObject result = ImmutableObject.clone(this);
result.map = map;
return result;
}
}
static class DerivedImmutableObject extends TestImmutableObject {
String extraField;
static DerivedImmutableObject create() {
return new DerivedImmutableObject();
}
}
}
@@ -71,7 +71,7 @@ public class ContactCommandTest {
EppLoader eppLoader = new EppLoader(this, "contact_update.xml");
Update command =
(Update)
((ResourceCommandWrapper) (eppLoader.getEpp().getCommandWrapper().getCommand()))
((ResourceCommandWrapper) eppLoader.getEpp().getCommandWrapper().getCommand())
.getResourceCommand();
Change change = command.getInnerChange();
assertThat(change.getInternationalizedPostalInfo().getAddress())
@@ -221,8 +221,7 @@ public class DomainHistoryTest extends EntityTestCase {
.setGracePeriods(
ImmutableSet.of(
GracePeriod.create(
GracePeriodStatus.ADD, "domainRepoId", END_OF_TIME, "clientId", null)
.cloneWithPrepopulatedId()))
GracePeriodStatus.ADD, "domainRepoId", END_OF_TIME, "clientId", null)))
.build();
}
@@ -0,0 +1,127 @@
// 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.reporting;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ImmutableObjectSubject.immutableObjectCorrespondence;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.newDomainBase;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.ImmutableSet;
import google.registry.model.EntityTestCase;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.Period;
import google.registry.model.eppcommon.Trid;
import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
@DualDatabaseTest
public class HistoryEntryDaoTest extends EntityTestCase {
private DomainBase domain;
private HistoryEntry historyEntry;
@BeforeEach
void beforeEach() {
fakeClock.setTo(DateTime.parse("2020-10-01T00:00:00Z"));
createTld("foobar");
domain = persistActiveDomain("foo.foobar");
DomainTransactionRecord transactionRecord =
new DomainTransactionRecord.Builder()
.setTld("foobar")
.setReportingTime(fakeClock.nowUtc())
.setReportField(TransactionReportField.NET_ADDS_1_YR)
.setReportAmount(1)
.build();
// Set up a new persisted HistoryEntry entity.
historyEntry =
new DomainHistory.Builder()
.setParent(domain)
.setType(HistoryEntry.Type.DOMAIN_CREATE)
.setPeriod(Period.create(1, Period.Unit.YEARS))
.setXmlBytes("<xml></xml>".getBytes(UTF_8))
.setModificationTime(fakeClock.nowUtc())
.setClientId("TheRegistrar")
.setOtherClientId("otherClient")
.setTrid(Trid.create("ABC-123", "server-trid"))
.setBySuperuser(false)
.setReason("reason")
.setRequestedByRegistrar(false)
.setDomainTransactionRecords(ImmutableSet.of(transactionRecord))
.build();
persistResource(historyEntry);
}
@TestOfyAndSql
void testSimpleLoadAll() {
assertThat(HistoryEntryDao.loadAllHistoryObjects(START_OF_TIME, END_OF_TIME))
.comparingElementsUsing(immutableObjectCorrespondence("nsHosts"))
.containsExactly(historyEntry);
}
@TestOfyAndSql
void testSkips_tooEarly() {
assertThat(HistoryEntryDao.loadAllHistoryObjects(fakeClock.nowUtc().plusMillis(1), END_OF_TIME))
.isEmpty();
}
@TestOfyAndSql
void testSkips_tooLate() {
assertThat(
HistoryEntryDao.loadAllHistoryObjects(START_OF_TIME, fakeClock.nowUtc().minusMillis(1)))
.isEmpty();
}
@TestOfyAndSql
void testLoadByResource() {
transactIfJpaTm(
() ->
assertThat(HistoryEntryDao.loadHistoryObjectsForResource(domain.createVKey()))
.comparingElementsUsing(immutableObjectCorrespondence("nsHosts"))
.containsExactly(historyEntry));
}
@TestOfyAndSql
void testLoadByResource_skips_tooEarly() {
assertThat(
HistoryEntryDao.loadHistoryObjectsForResource(
domain.createVKey(), fakeClock.nowUtc().plusMillis(1), END_OF_TIME))
.isEmpty();
}
@TestOfyAndSql
void testLoadByResource_skips_tooLate() {
assertThat(
HistoryEntryDao.loadHistoryObjectsForResource(
domain.createVKey(), START_OF_TIME, fakeClock.nowUtc().minusMillis(1)))
.isEmpty();
}
@TestOfyAndSql
void testLoadByResource_noEntriesForResource() {
DomainBase newDomain = persistResource(newDomainBase("new.foobar"));
assertThat(HistoryEntryDao.loadHistoryObjectsForResource(newDomain.createVKey())).isEmpty();
}
}
@@ -14,22 +14,29 @@
package google.registry.model.reporting;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.newDomainBase;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistResource;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import google.registry.model.EntityTestCase;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.Period;
import google.registry.model.eppcommon.Trid;
import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
import google.registry.testing.TestOfyOnly;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link HistoryEntry}. */
@DualDatabaseTest
class HistoryEntryTest extends EntityTestCase {
private HistoryEntry historyEntry;
@@ -37,6 +44,7 @@ class HistoryEntryTest extends EntityTestCase {
@BeforeEach
void setUp() {
createTld("foobar");
DomainBase domain = persistActiveDomain("foo.foobar");
DomainTransactionRecord transactionRecord =
new DomainTransactionRecord.Builder()
.setTld("foobar")
@@ -46,13 +54,13 @@ class HistoryEntryTest extends EntityTestCase {
.build();
// Set up a new persisted HistoryEntry entity.
historyEntry =
new HistoryEntry.Builder()
.setParent(newDomainBase("foo.foobar"))
new DomainHistory.Builder()
.setParent(domain)
.setType(HistoryEntry.Type.DOMAIN_CREATE)
.setPeriod(Period.create(1, Period.Unit.YEARS))
.setXmlBytes("<xml></xml>".getBytes(UTF_8))
.setModificationTime(fakeClock.nowUtc())
.setClientId("foo")
.setClientId("TheRegistrar")
.setOtherClientId("otherClient")
.setTrid(Trid.create("ABC-123", "server-trid"))
.setBySuperuser(false)
@@ -63,13 +71,23 @@ class HistoryEntryTest extends EntityTestCase {
persistResource(historyEntry);
}
@Test
@TestOfyAndSql
void testPersistence() {
assertThat(ofy().load().entity(historyEntry).now()).isEqualTo(historyEntry);
transactIfJpaTm(
() -> {
HistoryEntry fromDatabase = tm().loadByEntity(historyEntry);
assertAboutImmutableObjects()
.that(fromDatabase)
.isEqualExceptFields(historyEntry, "nsHosts", "domainTransactionRecords");
assertAboutImmutableObjects()
.that(Iterables.getOnlyElement(fromDatabase.getDomainTransactionRecords()))
.isEqualExceptFields(
Iterables.getOnlyElement(historyEntry.getDomainTransactionRecords()), "id");
});
}
@Test
@TestOfyOnly
void testIndexing() throws Exception {
verifyIndexing(historyEntry, "modificationTime", "clientId");
verifyIndexing(historyEntry.asHistoryEntry(), "modificationTime", "clientId");
}
}
@@ -54,28 +54,28 @@ public class TransferDataTest {
transferBillingEventKey =
VKey.create(
BillingEvent.OneTime.class,
12345,
Key.create(historyEntryKey, BillingEvent.OneTime.class, 12345));
12345L,
Key.create(historyEntryKey, BillingEvent.OneTime.class, 12345L));
otherServerApproveBillingEventKey =
VKey.create(
BillingEvent.Cancellation.class,
2468,
Key.create(historyEntryKey, BillingEvent.Cancellation.class, 2468));
2468L,
Key.create(historyEntryKey, BillingEvent.Cancellation.class, 2468L));
recurringBillingEventKey =
VKey.create(
BillingEvent.Recurring.class,
13579,
Key.create(historyEntryKey, BillingEvent.Recurring.class, 13579));
13579L,
Key.create(historyEntryKey, BillingEvent.Recurring.class, 13579L));
autorenewPollMessageKey =
VKey.create(
PollMessage.Autorenew.class,
67890,
Key.create(historyEntryKey, PollMessage.Autorenew.class, 67890));
67890L,
Key.create(historyEntryKey, PollMessage.Autorenew.class, 67890L));
otherServerApprovePollMessageKey =
VKey.create(
PollMessage.OneTime.class,
314159,
Key.create(historyEntryKey, PollMessage.OneTime.class, 314159));
314159L,
Key.create(historyEntryKey, PollMessage.OneTime.class, 314159L));
}
@Test
@@ -48,8 +48,8 @@ import org.junit.jupiter.api.extension.RegisterExtension;
* Unit tests for SQL only APIs defined in {@link JpaTransactionManagerImpl}. Note that the tests
* for common APIs in {@link TransactionManager} are added in {@link TransactionManagerTest}.
*
* <p>TODO(shicong): Remove duplicate tests that covered by TransactionManagerTest by refactoring
* the test schema.
* <p>TODO(b/177587763): Remove duplicate tests that covered by TransactionManagerTest by
* refactoring the test schema.
*/
class JpaTransactionManagerImplTest {
@@ -95,7 +95,7 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
registrarLol)
.asBuilder()
.setCreationTimeForTest(clock.nowUtc().minusYears(3))
.setCreationClientId("foo")
.setCreationClientId("TheRegistrar")
.build());
// deleted domain in lol
@@ -128,7 +128,7 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
registrarLol)
.asBuilder()
.setCreationTimeForTest(clock.nowUtc().minusYears(3))
.setCreationClientId("foo")
.setCreationClientId("TheRegistrar")
.setDeletionTime(clock.nowUtc().minusDays(1))
.build());
// cat.みんな
@@ -168,7 +168,7 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
registrarIdn)
.asBuilder()
.setCreationTimeForTest(clock.nowUtc().minusYears(3))
.setCreationClientId("foo")
.setCreationClientId("TheRegistrar")
.build());
// 1.tld
@@ -208,7 +208,7 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
registrar1Tld)
.asBuilder()
.setCreationTimeForTest(clock.nowUtc().minusYears(3))
.setCreationClientId("foo")
.setCreationClientId("TheRegistrar")
.build());
// history entries
@@ -176,7 +176,7 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
.asBuilder()
.setSubordinateHosts(ImmutableSet.of("ns1.cat.lol", "ns2.cat.lol"))
.setCreationTimeForTest(clock.nowUtc().minusYears(3))
.setCreationClientId("foo")
.setCreationClientId("TheRegistrar")
.build());
persistResource(
hostNs1CatLol.asBuilder().setSuperordinateDomain(domainCatLol.createVKey()).build());
@@ -216,7 +216,7 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
registrar)
.asBuilder()
.setCreationTimeForTest(clock.nowUtc().minusYears(3))
.setCreationClientId("foo")
.setCreationClientId("TheRegistrar")
.build());
// cat.example
createTld("example");
@@ -255,7 +255,7 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
registrar)
.asBuilder()
.setCreationTimeForTest(clock.nowUtc().minusYears(3))
.setCreationClientId("foo")
.setCreationClientId("TheRegistrar")
.build());
// cat.みんな
createTld("xn--q9jyb4c");
@@ -294,7 +294,7 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
registrar)
.asBuilder()
.setCreationTimeForTest(clock.nowUtc().minusYears(3))
.setCreationClientId("foo")
.setCreationClientId("TheRegistrar")
.build());
// cat.1.test
createTld("1.test");
@@ -334,7 +334,7 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
.asBuilder()
.setSubordinateHosts(ImmutableSet.of("ns1.cat.1.test"))
.setCreationTimeForTest(clock.nowUtc().minusYears(3))
.setCreationClientId("foo")
.setCreationClientId("TheRegistrar")
.build());
persistResource(makeRegistrar("otherregistrar", "other", Registrar.State.ACTIVE));
@@ -430,7 +430,7 @@ class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDomainSear
.asBuilder()
.setNameservers(hostKeys)
.setCreationTimeForTest(clock.nowUtc().minusYears(3))
.setCreationClientId("foo");
.setCreationClientId("TheRegistrar");
if (domainName.equals(mainDomainName)) {
builder.setSubordinateHosts(subordinateHostnamesBuilder.build());
}
@@ -191,7 +191,6 @@ class RdapJsonFormatterTest {
hostResourceIpv6,
registrar)
.asBuilder()
.setCreationClientId("foo")
.setCreationTimeForTest(clock.nowUtc().minusMonths(4))
.setLastEppUpdateTime(clock.nowUtc().minusMonths(3))
.build());
@@ -206,7 +205,6 @@ class RdapJsonFormatterTest {
null,
registrar)
.asBuilder()
.setCreationClientId("foo")
.setCreationTimeForTest(clock.nowUtc())
.setLastEppUpdateTime(null)
.build());
@@ -313,7 +313,7 @@ class RdapTestHelper {
}
builder.append(
String.format(
"Different: %s -> %s\ninstead of %s\n\n",
"Actual: %s -> %s\nExpected: %s\n\n",
name, jsonifyAndIndent(actual), jsonifyAndIndent(expected)));
}
}
@@ -109,6 +109,7 @@ import google.registry.model.transfer.TransferStatus;
import google.registry.persistence.VKey;
import google.registry.tmch.LordnTaskUtils;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -116,7 +117,6 @@ import javax.annotation.Nullable;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.joda.time.DateTimeComparator;
import org.joda.time.DateTimeZone;
/** Static utils for setting up test resources. */
@@ -532,11 +532,14 @@ public class DatabaseHelper {
DateTime requestTime,
DateTime expirationTime,
DateTime now) {
HistoryEntry historyEntryContactTransfer = persistResource(
new HistoryEntry.Builder()
.setType(HistoryEntry.Type.CONTACT_TRANSFER_REQUEST)
.setParent(contact)
.build());
HistoryEntry historyEntryContactTransfer =
persistResource(
new HistoryEntry.Builder()
.setType(HistoryEntry.Type.CONTACT_TRANSFER_REQUEST)
.setParent(persistResource(contact))
.setModificationTime(now)
.build()
.toChildHistoryEntity());
return persistResource(
contact
.asBuilder()
@@ -1115,7 +1118,8 @@ public class DatabaseHelper {
historyEntry ->
historyEntry.getParent().getName().equals(resource.getRepoId()))
.collect(toImmutableList());
return ImmutableList.sortedCopyOf(DateTimeComparator.getInstance(), filtered);
return ImmutableList.sortedCopyOf(
Comparator.comparing(HistoryEntry::getModificationTime), filtered);
});
}
@@ -392,17 +392,18 @@ public final class FullFieldsTestEntityHelper {
Period period,
String reason,
DateTime modificationTime) {
HistoryEntry.Builder builder = new HistoryEntry.Builder()
.setParent(resource)
.setType(type)
.setPeriod(period)
.setXmlBytes("<xml></xml>".getBytes(UTF_8))
.setModificationTime(modificationTime)
.setClientId("foo")
.setTrid(Trid.create("ABC-123", "server-trid"))
.setBySuperuser(false)
.setReason(reason)
.setRequestedByRegistrar(false);
HistoryEntry.Builder builder =
new HistoryEntry.Builder()
.setParent(resource)
.setType(type)
.setPeriod(period)
.setXmlBytes("<xml></xml>".getBytes(UTF_8))
.setModificationTime(modificationTime)
.setClientId(resource.getPersistedCurrentSponsorClientId())
.setTrid(Trid.create("ABC-123", "server-trid"))
.setBySuperuser(false)
.setReason(reason)
.setRequestedByRegistrar(false);
return builder.build();
}
}
@@ -14,7 +14,19 @@
package google.registry.testing;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.Key;
import google.registry.model.ImmutableObject;
import google.registry.model.ofy.ReplayQueue;
import google.registry.model.ofy.TransactionInfo;
import google.registry.persistence.VKey;
import google.registry.schema.replay.DatastoreEntity;
import java.util.Optional;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
@@ -26,13 +38,26 @@ import org.junit.jupiter.api.extension.ExtensionContext;
* that extension are also replayed. If AppEngineExtension is not used,
* JpaTransactionManagerExtension must be, and this extension should be ordered _after_
* JpaTransactionManagerExtension so that writes to SQL work.
*
* <p>If the "compare" flag is set in the constructor, this will also compare all touched objects in
* both databases after performing the replay.
*/
public class ReplayExtension implements BeforeEachCallback, AfterEachCallback {
FakeClock clock;
boolean compare;
public ReplayExtension(FakeClock clock) {
private ReplayExtension(FakeClock clock, boolean compare) {
this.clock = clock;
this.compare = compare;
}
public static ReplayExtension createWithCompare(FakeClock clock) {
return new ReplayExtension(clock, true);
}
public static ReplayExtension createWithoutCompare(FakeClock clock) {
return new ReplayExtension(clock, false);
}
@Override
@@ -51,8 +76,52 @@ public class ReplayExtension implements BeforeEachCallback, AfterEachCallback {
replayToSql();
}
private static ImmutableSet<String> NON_REPLICATED_TYPES =
ImmutableSet.of(
"PremiumList",
"PremiumListRevision",
"PremiumListEntry",
"ReservedList",
"RdeRevision",
"KmsSecretRevision",
"ServerSecret",
"SignedMarkRevocationList",
"ClaimsListShard",
"TmchCrl",
"EppResourceIndex",
"ForeignKeyIndex",
"ForeignKeyHostIndex",
"ForeignKeyContactIndex",
"ForeignKeyDomainIndex");
public void replayToSql() {
DatabaseHelper.setAlwaysSaveWithBackup(false);
ReplayQueue.replay();
ImmutableMap<Key<?>, Object> changes = ReplayQueue.replay();
// Compare JPA to OFY, if requested.
if (compare) {
for (ImmutableMap.Entry<Key<?>, Object> entry : changes.entrySet()) {
// Don't verify non-replicated types.
if (NON_REPLICATED_TYPES.contains(entry.getKey().getKind())) {
continue;
}
// Since the object may have changed in datastore by the time we're doing the replay, we
// have to compare the current value in SQL (which we just mutated) against the value that
// we originally would have persisted (that being the object in the entry).
VKey<?> vkey = VKey.from(entry.getKey());
Optional<?> jpaValue = jpaTm().transact(() -> jpaTm().loadByKeyIfPresent(vkey));
if (entry.getValue().equals(TransactionInfo.Delete.SENTINEL)) {
assertThat(jpaValue.isPresent()).isFalse();
} else {
ImmutableObject immutJpaObject = (ImmutableObject) jpaValue.get();
assertAboutImmutableObjects().that(immutJpaObject).hasCorrectHashValue();
assertAboutImmutableObjects()
.that(immutJpaObject)
.isEqualAcrossDatabases(
(ImmutableObject) ((DatastoreEntity) entry.getValue()).toSqlEntity().get());
}
}
}
}
}
@@ -53,7 +53,7 @@ class TmchCertificateAuthorityTest {
CertificateExpiredException e =
assertThrows(
CertificateExpiredException.class, tmchCertificateAuthority::getAndValidateRoot);
assertThat(e).hasMessageThat().containsMatch("NotAfter: Sun Jul 23 23:59:59 UTC 2023");
assertThat(e).hasMessageThat().contains("NotAfter: Sun Jul 23 23:59:59 UTC 2023");
}
@Test
@@ -64,7 +64,7 @@ class TmchCertificateAuthorityTest {
CertificateNotYetValidException e =
assertThrows(
CertificateNotYetValidException.class, tmchCertificateAuthority::getAndValidateRoot);
assertThat(e).hasMessageThat().containsMatch("NotBefore: Wed Jul 24 00:00:00 UTC 2013");
assertThat(e).hasMessageThat().contains("NotBefore: Wed Jul 24 00:00:00 UTC 2013");
}
@Test
@@ -61,7 +61,7 @@ public class CompareDbBackupsTest {
URL backupRootFolder = Resources.getResource("google/registry/tools/datastore-export");
CompareDbBackups.main(new String[] {backupRootFolder.getPath(), backupRootFolder.getPath()});
String output = new String(stdout.toByteArray(), UTF_8);
assertThat(output).containsMatch("Both sets have the same 41 entities");
assertThat(output).contains("Both sets have the same 41 entities");
}
@Test
@@ -38,6 +38,7 @@ import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Range;
import com.google.common.net.MediaType;
import google.registry.flows.certs.CertificateChecker;
import google.registry.flows.certs.CertificateChecker.InsecureCertificateException;
import google.registry.model.registrar.Registrar;
import java.io.IOException;
import java.util.Optional;
@@ -389,9 +390,9 @@ class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarCommand>
@Test
void testFail_clientCertFileFlagWithViolation() throws Exception {
fakeClock.setTo(DateTime.parse("2020-10-01T00:00:00Z"));
IllegalArgumentException thrown =
InsecureCertificateException thrown =
assertThrows(
IllegalArgumentException.class,
InsecureCertificateException.class,
() ->
runCommandForced(
"--name=blobio",
@@ -419,9 +420,9 @@ class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarCommand>
@Test
void testFail_clientCertFileFlagWithMultipleViolations() throws Exception {
fakeClock.setTo(DateTime.parse("2055-10-01T00:00:00Z"));
IllegalArgumentException thrown =
InsecureCertificateException thrown =
assertThrows(
IllegalArgumentException.class,
InsecureCertificateException.class,
() ->
runCommandForced(
"--name=blobio",
@@ -476,9 +477,9 @@ class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarCommand>
@Test
void testFail_failoverClientCertFileFlagWithViolations() throws Exception {
fakeClock.setTo(DateTime.parse("2020-11-01T00:00:00Z"));
IllegalArgumentException thrown =
InsecureCertificateException thrown =
assertThrows(
IllegalArgumentException.class,
InsecureCertificateException.class,
() ->
runCommandForced(
"--name=blobio",
@@ -506,9 +507,9 @@ class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarCommand>
@Test
void testFail_failoverClientCertFileFlagWithMultipleViolations() throws Exception {
fakeClock.setTo(DateTime.parse("2055-11-01T00:00:00Z"));
IllegalArgumentException thrown =
InsecureCertificateException thrown =
assertThrows(
IllegalArgumentException.class,
InsecureCertificateException.class,
() ->
runCommandForced(
"--name=blobio",
@@ -22,12 +22,14 @@ import static google.registry.testing.FullFieldsTestEntityHelper.makeHistoryEntr
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Period;
import google.registry.model.reporting.HistoryEntry;
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.BeforeEach;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link GetClaimsListCommand}. */
@DualDatabaseTest
class GetHistoryEntriesCommandTest extends CommandTestCase<GetHistoryEntriesCommand> {
private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01T00:00:00Z"));
@@ -40,7 +42,7 @@ class GetHistoryEntriesCommandTest extends CommandTestCase<GetHistoryEntriesComm
domain = persistActiveDomain("example.tld");
}
@Test
@TestOfyAndSql
void testSuccess_works() throws Exception {
persistResource(
makeHistoryEntry(
@@ -51,7 +53,7 @@ class GetHistoryEntriesCommandTest extends CommandTestCase<GetHistoryEntriesComm
clock.nowUtc()));
runCommand("--id=example.tld", "--type=DOMAIN");
assertStdoutIs(
"Client: foo\n"
"Client: TheRegistrar\n"
+ "Time: 2000-01-01T00:00:00.000Z\n"
+ "Client TRID: ABC-123\n"
+ "Server TRID: server-trid\n"
@@ -60,7 +62,57 @@ class GetHistoryEntriesCommandTest extends CommandTestCase<GetHistoryEntriesComm
+ "\n");
}
@Test
@TestOfyAndSql
void testSuccess_nothingBefore() throws Exception {
persistResource(
makeHistoryEntry(
domain,
HistoryEntry.Type.DOMAIN_CREATE,
Period.create(1, Period.Unit.YEARS),
"created",
clock.nowUtc()));
runCommand("--before", clock.nowUtc().minusMinutes(1).toString());
assertStdoutIs("");
}
@TestOfyAndSql
void testSuccess_nothingAfter() throws Exception {
persistResource(
makeHistoryEntry(
domain,
HistoryEntry.Type.DOMAIN_CREATE,
Period.create(1, Period.Unit.YEARS),
"created",
clock.nowUtc()));
runCommand("--after", clock.nowUtc().plusMinutes(1).toString());
assertStdoutIs("");
}
@TestOfyAndSql
void testSuccess_withinRange() throws Exception {
persistResource(
makeHistoryEntry(
domain,
HistoryEntry.Type.DOMAIN_CREATE,
Period.create(1, Period.Unit.YEARS),
"created",
clock.nowUtc()));
runCommand(
"--after",
clock.nowUtc().minusMinutes(1).toString(),
"--before",
clock.nowUtc().plusMinutes(1).toString());
assertStdoutIs(
"Client: TheRegistrar\n"
+ "Time: 2000-01-01T00:00:00.000Z\n"
+ "Client TRID: ABC-123\n"
+ "Server TRID: server-trid\n"
+ "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n"
+ "<xml/>\n"
+ "\n");
}
@TestOfyAndSql
void testSuccess_noTrid() throws Exception {
persistResource(
makeHistoryEntry(
@@ -74,7 +126,7 @@ class GetHistoryEntriesCommandTest extends CommandTestCase<GetHistoryEntriesComm
.build());
runCommand("--id=example.tld", "--type=DOMAIN");
assertStdoutIs(
"Client: foo\n"
"Client: TheRegistrar\n"
+ "Time: 2000-01-01T00:00:00.000Z\n"
+ "Client TRID: null\n"
+ "Server TRID: null\n"
@@ -16,6 +16,7 @@ package google.registry.tools;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.domain.rgp.GracePeriodStatus.AUTO_RENEW;
import static google.registry.model.eppcommon.StatusValue.PENDING_DELETE;
import static google.registry.model.eppcommon.StatusValue.SERVER_UPDATE_PROHIBITED;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_CREATE;
import static google.registry.testing.DatabaseHelper.createTld;
@@ -334,6 +335,39 @@ class UpdateDomainCommandTest extends EppToolCommandTestCase<UpdateDomainCommand
assertThat(stdErr).contains("example.tld");
}
@Test
void testSuccess_canUpdatePendingDeleteDomain_whenSuperuserPassesOverrideFlag() throws Exception {
ContactResource adminContact1 = persistResource(newContactResource("crr-admin1"));
ContactResource adminContact2 = persistResource(newContactResource("crr-admin2"));
ContactResource techContact1 = persistResource(newContactResource("crr-tech1"));
ContactResource techContact2 = persistResource(newContactResource("crr-tech2"));
VKey<ContactResource> adminResourceKey1 = adminContact1.createVKey();
VKey<ContactResource> adminResourceKey2 = adminContact2.createVKey();
VKey<ContactResource> techResourceKey1 = techContact1.createVKey();
VKey<ContactResource> techResourceKey2 = techContact2.createVKey();
persistResource(
newDomainBase("example.tld")
.asBuilder()
.setContacts(
ImmutableSet.of(
DesignatedContact.create(DesignatedContact.Type.ADMIN, adminResourceKey1),
DesignatedContact.create(DesignatedContact.Type.ADMIN, adminResourceKey2),
DesignatedContact.create(DesignatedContact.Type.TECH, techResourceKey1),
DesignatedContact.create(DesignatedContact.Type.TECH, techResourceKey2)))
.setStatusValues(ImmutableSet.of(PENDING_DELETE))
.build());
runCommandForced(
"--client=NewRegistrar",
"--admins=crr-admin2,crr-admin3",
"--techs=crr-tech2,crr-tech3",
"--superuser",
"--force_in_pending_delete",
"example.tld");
eppVerifier.expectSuperuser().verifySent("domain_update_set_contacts.xml");
}
@Test
void testFailure_cantUpdateRegistryLockedDomainEvenAsSuperuser() {
HostResource host = persistActiveHost("ns1.zdns.google");
@@ -356,7 +390,30 @@ class UpdateDomainCommandTest extends EppToolCommandTestCase<UpdateDomainCommand
"example.tld"));
assertThat(e)
.hasMessageThat()
.containsMatch("The domain 'example.tld' has status SERVER_UPDATE_PROHIBITED");
.contains("The domain 'example.tld' has status SERVER_UPDATE_PROHIBITED.");
}
@Test
void testFailure_cantUpdatePendingDeleteDomainEvenAsSuperuser_withoutPassingOverrideFlag() {
HostResource host = persistActiveHost("ns1.zdns.google");
ImmutableSet<VKey<HostResource>> nameservers = ImmutableSet.of(host.createVKey());
persistResource(
newDomainBase("example.tld")
.asBuilder()
.setStatusValues(ImmutableSet.of(PENDING_DELETE))
.setNameservers(nameservers)
.build());
Exception e =
assertThrows(
IllegalArgumentException.class,
() ->
runCommandForced(
"--client=NewRegistrar",
"--statuses=clientRenewProhibited,serverHold",
"--superuser",
"example.tld"));
assertThat(e).hasMessageThat().contains("The domain 'example.tld' has status PENDING_DELETE.");
}
@Test
@@ -33,6 +33,7 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.flows.certs.CertificateChecker;
import google.registry.flows.certs.CertificateChecker.InsecureCertificateException;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.Registrar.State;
import google.registry.model.registrar.Registrar.Type;
@@ -265,9 +266,9 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
Registrar registrar = loadRegistrar("NewRegistrar");
assertThat(registrar.getClientCertificate()).isEmpty();
assertThat(registrar.getClientCertificateHash()).isEmpty();
IllegalArgumentException thrown =
InsecureCertificateException thrown =
assertThrows(
IllegalArgumentException.class,
InsecureCertificateException.class,
() -> runCommand("--cert_file=" + getCertFilename(), "--force", "NewRegistrar"));
assertThat(thrown.getMessage())
.isEqualTo(
@@ -282,9 +283,9 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
Registrar registrar = loadRegistrar("NewRegistrar");
assertThat(registrar.getClientCertificate()).isEmpty();
assertThat(registrar.getClientCertificateHash()).isEmpty();
IllegalArgumentException thrown =
InsecureCertificateException thrown =
assertThrows(
IllegalArgumentException.class,
InsecureCertificateException.class,
() -> runCommand("--cert_file=" + getCertFilename(), "--force", "NewRegistrar"));
assertThat(thrown.getMessage())
.isEqualTo(
@@ -298,9 +299,9 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
fakeClock.setTo(DateTime.parse("2020-11-01T00:00:00Z"));
Registrar registrar = loadRegistrar("NewRegistrar");
assertThat(registrar.getFailoverClientCertificate()).isEmpty();
IllegalArgumentException thrown =
InsecureCertificateException thrown =
assertThrows(
IllegalArgumentException.class,
InsecureCertificateException.class,
() ->
runCommand("--failover_cert_file=" + getCertFilename(), "--force", "NewRegistrar"));
assertThat(thrown.getMessage())
@@ -315,9 +316,9 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
fakeClock.setTo(DateTime.parse("2055-10-01T00:00:00Z"));
Registrar registrar = loadRegistrar("NewRegistrar");
assertThat(registrar.getFailoverClientCertificate()).isEmpty();
IllegalArgumentException thrown =
InsecureCertificateException thrown =
assertThrows(
IllegalArgumentException.class,
InsecureCertificateException.class,
() ->
runCommand("--failover_cert_file=" + getCertFilename(), "--force", "NewRegistrar"));
assertThat(thrown.getMessage())
@@ -20,14 +20,17 @@ 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 google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.joda.time.DateTimeZone.UTC;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.beust.jcommander.ParameterException;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.flows.EppException;
import google.registry.flows.TransportCredentials.BadRegistrarPasswordException;
import google.registry.flows.certs.CertificateChecker;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.Registrar.State;
import google.registry.testing.CertificateSamples;
@@ -42,7 +45,7 @@ import org.junit.jupiter.api.Test;
class ValidateLoginCredentialsCommandTest extends CommandTestCase<ValidateLoginCredentialsCommand> {
private static final String PASSWORD = "foo-BAR2";
private static final String CERT_HASH = CertificateSamples.SAMPLE_CERT_HASH;
private static final String CERT_HASH = CertificateSamples.SAMPLE_CERT3_HASH;
private static final String CLIENT_IP = "1.2.3.4";
@BeforeEach
@@ -52,11 +55,18 @@ class ValidateLoginCredentialsCommandTest extends CommandTestCase<ValidateLoginC
loadRegistrar("NewRegistrar")
.asBuilder()
.setPassword(PASSWORD)
.setClientCertificate(CertificateSamples.SAMPLE_CERT, DateTime.now(UTC))
.setClientCertificate(CertificateSamples.SAMPLE_CERT3, DateTime.now(UTC))
.setIpAddressAllowList(ImmutableList.of(new CidrAddressBlock(CLIENT_IP)))
.setState(ACTIVE)
.setAllowedTlds(ImmutableSet.of("tld"))
.build());
command.certificateChecker =
new CertificateChecker(
ImmutableSortedMap.of(START_OF_TIME, 825, DateTime.parse("2020-09-01T00:00:00Z"), 398),
30,
2048,
ImmutableSet.of("secp256r1", "secp384r1"),
fakeClock);
}
@Test
@@ -64,7 +74,7 @@ class ValidateLoginCredentialsCommandTest extends CommandTestCase<ValidateLoginC
runCommand(
"--client=NewRegistrar",
"--password=" + PASSWORD,
"--cert_hash=" + CERT_HASH,
"--cert_file=" + getCertFilename(CertificateSamples.SAMPLE_CERT3),
"--ip_address=" + CLIENT_IP);
}
@@ -83,7 +93,7 @@ class ValidateLoginCredentialsCommandTest extends CommandTestCase<ValidateLoginC
runCommand(
"--client=NewRegistrar",
"--password=" + PASSWORD,
"--cert_hash=" + CERT_HASH,
"--cert_file=" + getCertFilename(CertificateSamples.SAMPLE_CERT3),
"--ip_address=" + CLIENT_IP));
assertThat(thrown)
.hasMessageThat()
@@ -91,7 +101,7 @@ class ValidateLoginCredentialsCommandTest extends CommandTestCase<ValidateLoginC
}
@Test
void testFailure_loginWithBadPassword() {
void testFailure_loginWithBadPassword() throws Exception {
EppException thrown =
assertThrows(
BadRegistrarPasswordException.class,
@@ -99,7 +109,7 @@ class ValidateLoginCredentialsCommandTest extends CommandTestCase<ValidateLoginC
runCommand(
"--client=NewRegistrar",
"--password=" + new StringBuilder(PASSWORD).reverse(),
"--cert_hash=" + CERT_HASH,
"--cert_file=" + getCertFilename(CertificateSamples.SAMPLE_CERT3),
"--ip_address=" + CLIENT_IP));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@@ -37,15 +37,17 @@ import google.registry.model.registrar.Registrar;
import google.registry.model.reporting.HistoryEntry;
import google.registry.schema.domain.RegistryLock;
import google.registry.testing.DeterministicStringGenerator;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
import google.registry.tools.CommandTestCase;
import google.registry.util.StringGenerator.Alphabets;
import java.util.Optional;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link BackfillRegistryLocksCommand}. */
@DualDatabaseTest
class BackfillRegistryLocksCommandTest extends CommandTestCase<BackfillRegistryLocksCommand> {
@BeforeEach
@@ -57,7 +59,7 @@ class BackfillRegistryLocksCommandTest extends CommandTestCase<BackfillRegistryL
command.stringGenerator = new DeterministicStringGenerator(Alphabets.BASE_58);
}
@Test
@TestOfyAndSql
void testSimpleBackfill() throws Exception {
DomainBase domain = persistLockedDomain("example.tld");
Truth8.assertThat(getMostRecentRegistryLockByRepoId(domain.getRepoId())).isEmpty();
@@ -69,7 +71,7 @@ class BackfillRegistryLocksCommandTest extends CommandTestCase<BackfillRegistryL
Truth8.assertThat(lockOptional.get().getLockCompletionTimestamp()).isPresent();
}
@Test
@TestOfyAndSql
void testBackfill_onlyLockedDomains() throws Exception {
DomainBase neverLockedDomain = persistActiveDomain("neverlocked.tld");
DomainBase previouslyLockedDomain = persistLockedDomain("unlocked.tld");
@@ -89,7 +91,7 @@ class BackfillRegistryLocksCommandTest extends CommandTestCase<BackfillRegistryL
assertThat(Iterables.getOnlyElement(locks).getDomainName()).isEqualTo("locked.tld");
}
@Test
@TestOfyAndSql
void testBackfill_skipsDeletedDomains() throws Exception {
DomainBase domain = persistDeletedDomain("example.tld", fakeClock.nowUtc());
persistResource(domain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build());
@@ -98,7 +100,7 @@ class BackfillRegistryLocksCommandTest extends CommandTestCase<BackfillRegistryL
Truth8.assertThat(getMostRecentRegistryLockByRepoId(domain.getRepoId())).isEmpty();
}
@Test
@TestOfyAndSql
void testBackfill_skipsDomains_ifLockAlreadyExists() throws Exception {
DomainBase domain = persistLockedDomain("example.tld");
@@ -123,7 +125,7 @@ class BackfillRegistryLocksCommandTest extends CommandTestCase<BackfillRegistryL
.isEqualTo(previousLock.getLockCompletionTimestamp());
}
@Test
@TestOfyAndSql
void testBackfill_usesUrsTime_ifExists() throws Exception {
DateTime ursTime = fakeClock.nowUtc();
DomainBase ursDomain = persistLockedDomain("urs.tld");
@@ -151,7 +153,7 @@ class BackfillRegistryLocksCommandTest extends CommandTestCase<BackfillRegistryL
assertThat(nonUrsLock.getLockCompletionTimestamp()).hasValue(fakeClock.nowUtc());
}
@Test
@TestOfyAndSql
void testFailure_mustProvideDomainRoids() {
assertThat(assertThrows(IllegalArgumentException.class, this::runCommandForced))
.hasMessageThat()
@@ -33,7 +33,7 @@
"events": [
{
"eventAction": "registration",
"eventActor": "foo",
"eventActor": "TheRegistrar",
"eventDate": "1997-01-01T00:00:00.000Z"
},
{
@@ -33,7 +33,7 @@
"events": [
{
"eventAction": "registration",
"eventActor": "foo",
"eventActor": "TheRegistrar",
"eventDate": "1997-01-01T00:00:00.000Z"
},
{
@@ -34,7 +34,7 @@
"events": [
{
"eventAction": "registration",
"eventActor": "foo",
"eventActor": "TheRegistrar",
"eventDate": "1997-01-01T00:00:00.000Z"
},
{
@@ -47,7 +47,7 @@
},
{
"eventAction": "deletion",
"eventActor": "foo",
"eventActor": "evilregistrar",
"eventDate": "1999-07-01T00:00:00.000Z"
},
{
@@ -33,7 +33,7 @@
"events": [
{
"eventAction": "registration",
"eventActor": "foo",
"eventActor": "TheRegistrar",
"eventDate": "1997-01-01T00:00:00.000Z"
},
{
@@ -34,7 +34,7 @@
"events": [
{
"eventAction": "registration",
"eventActor": "foo",
"eventActor": "TheRegistrar",
"eventDate": "1997-01-01T00:00:00.000Z"
},
{
@@ -34,7 +34,7 @@
"events": [
{
"eventAction": "registration",
"eventActor": "foo",
"eventActor": "TheRegistrar",
"eventDate": "1997-01-01T00:00:00.000Z"
},
{
@@ -31,7 +31,7 @@
"events": [
{
"eventAction": "registration",
"eventActor": "foo",
"eventActor": "unicoderegistrar",
"eventDate": "1999-09-01T00:00:00.000Z"
},
{
@@ -44,7 +44,7 @@
},
{
"eventAction": "transfer",
"eventActor": "foo",
"eventActor": "unicoderegistrar",
"eventDate": "1999-12-01T00:00:00.000Z"
},
{
@@ -31,7 +31,7 @@
"events": [
{
"eventAction": "registration",
"eventActor": "foo",
"eventActor": "unicoderegistrar",
"eventDate": "1999-09-01T00:00:00.000Z"
},
{
@@ -44,7 +44,7 @@
},
{
"eventAction": "transfer",
"eventActor": "foo",
"eventActor": "unicoderegistrar",
"eventDate": "1999-12-01T00:00:00.000Z"
},
{
@@ -32,7 +32,7 @@
"events": [
{
"eventAction": "registration",
"eventActor": "foo",
"eventActor": "unicoderegistrar",
"eventDate": "2000-01-01T00:00:00.000Z"
},
{
@@ -14,7 +14,7 @@
"events": [
{
"eventAction": "registration",
"eventActor": "foo",
"eventActor": "TheRegistrar",
"eventDate": "1999-01-01T00:00:00.000Z"
},
{
@@ -261,11 +261,11 @@ td.section {
</tr>
<tr>
<td class="property_name">generated on</td>
<td class="property_value">2021-01-14 16:15:22.842637</td>
<td class="property_value">2021-01-21 00:11:27.19594</td>
</tr>
<tr>
<td class="property_name">last flyway file</td>
<td id="lastFlywayFile" class="property_value">V84__add_vkey_columns_in_billing_cancellation.sql</td>
<td id="lastFlywayFile" class="property_value">V85__add_required_columns_in_transfer_data.sql</td>
</tr>
</tbody>
</table>
@@ -274,19 +274,19 @@ td.section {
<svg viewbox="0.00 0.00 4221.44 2624.18" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="erDiagram" style="overflow: hidden; width: 100%; height: 800px"> <g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 2620.18)">
<title>SchemaCrawler_Diagram</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-2620.18 4217.44,-2620.18 4217.44,4 -4,4" />
<text text-anchor="start" x="3944.94" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">
<text text-anchor="start" x="3952.94" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">
generated by
</text>
<text text-anchor="start" x="4027.94" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">
<text text-anchor="start" x="4035.94" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">
SchemaCrawler 16.10.1
</text>
<text text-anchor="start" x="3943.94" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">
<text text-anchor="start" x="3951.94" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">
generated on
</text>
<text text-anchor="start" x="4027.94" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">
2021-01-14 16:15:22.842637
<text text-anchor="start" x="4035.94" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">
2021-01-21 00:11:27.19594
</text>
<polygon fill="none" stroke="#888888" points="3940.44,-4 3940.44,-44 4205.44,-44 4205.44,-4 3940.44,-4" /> <!-- allocationtoken_a08ccbef -->
<polygon fill="none" stroke="#888888" points="3948.44,-4 3948.44,-44 4205.44,-44 4205.44,-4 3948.44,-4" /> <!-- allocationtoken_a08ccbef -->
<g id="node1" class="node">
<title>allocationtoken_a08ccbef</title>
<polygon fill="#ebcef2" stroke="transparent" points="2538.5,-1071.68 2538.5,-1090.68 2691.5,-1090.68 2691.5,-1071.68 2538.5,-1071.68" />
File diff suppressed because it is too large Load Diff
+1
View File
@@ -82,3 +82,4 @@ V81__drop_spec11_fkeys.sql
V82__add_columns_to_restore_symmetric_billing_vkey.sql
V83__add_indexes_on_domainhost.sql
V84__add_vkey_columns_in_billing_cancellation.sql
V85__add_required_columns_in_transfer_data.sql
@@ -0,0 +1,57 @@
-- Copyright 2021 The Nomulus Authors. All Rights Reserved.
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
alter table "Contact"
add column if not exists "transfer_history_entry_id" int8;
alter table "Contact"
add column if not exists "transfer_repo_id" text;
alter table "Contact"
rename column "transfer_gaining_poll_message_id"
to "transfer_poll_message_id_1";
alter table "Contact"
rename column "transfer_losing_poll_message_id"
to "transfer_poll_message_id_2";
alter table "ContactHistory"
add column if not exists "transfer_history_entry_id" int8;
alter table "ContactHistory"
add column if not exists "transfer_repo_id" text;
alter table "ContactHistory"
rename column "transfer_gaining_poll_message_id"
to "transfer_poll_message_id_1";
alter table "ContactHistory"
rename column "transfer_losing_poll_message_id"
to "transfer_poll_message_id_2";
alter table "Domain"
add column if not exists "transfer_history_entry_id" int8;
alter table "Domain"
add column if not exists "transfer_repo_id" text;
alter table "Domain"
rename column "transfer_gaining_poll_message_id"
to "transfer_poll_message_id_1";
alter table "Domain"
rename column "transfer_losing_poll_message_id"
to "transfer_poll_message_id_2";
alter table "DomainHistory"
add column if not exists "transfer_history_entry_id" int8;
alter table "DomainHistory"
add column if not exists "transfer_repo_id" text;
alter table "DomainHistory"
rename column "transfer_gaining_poll_message_id"
to "transfer_poll_message_id_1";
alter table "DomainHistory"
rename column "transfer_losing_poll_message_id"
to "transfer_poll_message_id_2";

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