1
0
mirror of https://github.com/google/nomulus synced 2026-07-06 08:06:33 +00:00

Compare commits

...

16 Commits

Author SHA1 Message Date
Michael Muller 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
sarahcaseybot 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
sarahcaseybot 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 McIlwain 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 McIlwain 4e66fed497 Use nullness parity helper (#944)
* Use nullness parity helper
2021-01-26 13:20:48 -05:00
gbrodman 886cdfa39b Update NPM dependency based on Github security warning (#941) 2021-01-25 23:04:30 -05:00
sarahcaseybot 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
gbrodman 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 McIlwain 08cec96a93 Correct containsMatch() -> contains() for non-regexes (#940)
* Correct containsMatch() -> contains() for non-regexes
2021-01-22 14:31:38 -05:00
Ben McIlwain 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 Muller 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 Huang 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 McIlwain 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 Huang 6794c6fbd7 Resolve remaining TODO(shicong) (#932) 2021-01-20 19:27:48 -05:00
Ben McIlwain 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
sarahcaseybot 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
76 changed files with 5272 additions and 3214 deletions
@@ -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());
}
}
@@ -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()) {
@@ -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;
}
}
@@ -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;
@@ -378,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> {
@@ -396,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
@@ -403,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
@@ -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();
}
}
@@ -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(b/177589157): Make transfer flows work with Cloud SQL.
@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();
}
}
@@ -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;
@@ -116,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()) {
@@ -197,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);
@@ -250,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
@@ -278,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
@@ -307,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
@@ -339,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);
@@ -382,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());
@@ -416,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
@@ -469,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;
@@ -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."));
}
}
@@ -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");
@@ -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)));
}
@@ -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,10 +14,14 @@
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;
@@ -25,10 +29,15 @@ 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. */
@@ -62,17 +71,295 @@ public final class ImmutableObjectSubject extends Subject {
* <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) {
assertThat(expected).isNull();
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 {
assertThat(expected).isNotNull();
// 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);
}
if (actual != null) {
Map<Field, Object> actualFields = filterFields(actual, ImmutableObject.DoNotCompare.class);
Map<Field, Object> expectedFields =
filterFields(expected, ImmutableObject.DoNotCompare.class);
assertThat(actualFields).containsExactlyEntriesIn(expectedFields);
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(
@@ -109,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
@@ -123,7 +411,7 @@ public final class ImmutableObjectSubject extends Subject {
}
/** Filter out fields with the given annotation. */
public static Map<Field, Object> filterFields(
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
@@ -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())
@@ -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
@@ -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);
});
}
@@ -398,7 +398,7 @@ public final class FullFieldsTestEntityHelper {
.setPeriod(period)
.setXmlBytes("<xml></xml>".getBytes(UTF_8))
.setModificationTime(modificationTime)
.setClientId("foo")
.setClientId(resource.getPersistedCurrentSponsorClientId())
.setTrid(Trid.create("ABC-123", "server-trid"))
.setBySuperuser(false)
.setReason(reason)
@@ -17,7 +17,6 @@ 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 static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
@@ -26,6 +25,7 @@ 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;
@@ -106,16 +106,20 @@ public class ReplayExtension implements BeforeEachCallback, AfterEachCallback {
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<?> ofyValue = ofyTm().transact(() -> ofyTm().loadByKeyIfPresent(vkey));
Optional<?> jpaValue = jpaTm().transact(() -> jpaTm().loadByKeyIfPresent(vkey));
if (entry.getValue().equals(TransactionInfo.Delete.SENTINEL)) {
assertThat(jpaValue.isPresent()).isFalse();
assertThat(ofyValue.isPresent()).isFalse();
} else {
ImmutableObject immutJpaObject = (ImmutableObject) jpaValue.get();
assertAboutImmutableObjects().that(immutJpaObject).hasCorrectHashValue();
assertAboutImmutableObjects()
.that((ImmutableObject) jpaValue.get())
.isEqualAcrossDatabases((ImmutableObject) ofyValue.get());
.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";
@@ -140,8 +140,10 @@
addr_local_org text,
addr_local_type text,
search_name text,
transfer_gaining_poll_message_id int8,
transfer_losing_poll_message_id int8,
transfer_history_entry_id int8,
transfer_poll_message_id_1 int8,
transfer_poll_message_id_2 int8,
transfer_repo_id text,
transfer_client_txn_id text,
transfer_server_txn_id text,
transfer_gaining_registrar_id text,
@@ -201,8 +203,10 @@
addr_local_org text,
addr_local_type text,
search_name text,
transfer_gaining_poll_message_id int8,
transfer_losing_poll_message_id int8,
transfer_history_entry_id int8,
transfer_poll_message_id_1 int8,
transfer_poll_message_id_2 int8,
transfer_repo_id text,
transfer_client_txn_id text,
transfer_server_txn_id text,
transfer_gaining_registrar_id text,
@@ -284,8 +288,10 @@
transfer_renew_period_unit text,
transfer_renew_period_value int4,
transfer_registration_expiration_time timestamptz,
transfer_gaining_poll_message_id int8,
transfer_losing_poll_message_id int8,
transfer_history_entry_id int8,
transfer_poll_message_id_1 int8,
transfer_poll_message_id_2 int8,
transfer_repo_id text,
transfer_client_txn_id text,
transfer_server_txn_id text,
transfer_gaining_registrar_id text,
@@ -353,8 +359,10 @@
transfer_renew_period_unit text,
transfer_renew_period_value int4,
transfer_registration_expiration_time timestamptz,
transfer_gaining_poll_message_id int8,
transfer_losing_poll_message_id int8,
transfer_history_entry_id int8,
transfer_poll_message_id_1 int8,
transfer_poll_message_id_2 int8,
transfer_repo_id text,
transfer_client_txn_id text,
transfer_server_txn_id text,
transfer_gaining_registrar_id text,
@@ -210,8 +210,8 @@ CREATE TABLE public."Contact" (
search_name text,
voice_phone_extension text,
voice_phone_number text,
transfer_gaining_poll_message_id bigint,
transfer_losing_poll_message_id bigint,
transfer_poll_message_id_1 bigint,
transfer_poll_message_id_2 bigint,
transfer_client_txn_id text,
transfer_server_txn_id text,
transfer_gaining_registrar_id text,
@@ -219,7 +219,9 @@ CREATE TABLE public."Contact" (
transfer_pending_expiration_time timestamp with time zone,
transfer_request_time timestamp with time zone,
transfer_status text,
update_timestamp timestamp with time zone
update_timestamp timestamp with time zone,
transfer_history_entry_id bigint,
transfer_repo_id text
);
@@ -273,8 +275,8 @@ CREATE TABLE public."ContactHistory" (
addr_local_org text,
addr_local_type text,
search_name text,
transfer_gaining_poll_message_id bigint,
transfer_losing_poll_message_id bigint,
transfer_poll_message_id_1 bigint,
transfer_poll_message_id_2 bigint,
transfer_client_txn_id text,
transfer_server_txn_id text,
transfer_gaining_registrar_id text,
@@ -292,7 +294,9 @@ CREATE TABLE public."ContactHistory" (
last_epp_update_time timestamp with time zone,
statuses text[],
contact_repo_id text NOT NULL,
update_timestamp timestamp with time zone
update_timestamp timestamp with time zone,
transfer_history_entry_id bigint,
transfer_repo_id text
);
@@ -351,8 +355,8 @@ CREATE TABLE public."Domain" (
billing_contact text,
registrant_contact text,
tech_contact text,
transfer_gaining_poll_message_id bigint,
transfer_losing_poll_message_id bigint,
transfer_poll_message_id_1 bigint,
transfer_poll_message_id_2 bigint,
transfer_billing_cancellation_id bigint,
transfer_billing_event_id bigint,
transfer_billing_recurrence_id bigint,
@@ -377,7 +381,9 @@ CREATE TABLE public."Domain" (
deletion_poll_message_history_id bigint,
transfer_billing_recurrence_history_id bigint,
transfer_autorenew_poll_message_history_id bigint,
transfer_billing_event_history_id bigint
transfer_billing_event_history_id bigint,
transfer_history_entry_id bigint,
transfer_repo_id text
);
@@ -438,8 +444,8 @@ CREATE TABLE public."DomainHistory" (
transfer_renew_period_unit text,
transfer_renew_period_value integer,
transfer_registration_expiration_time timestamp with time zone,
transfer_gaining_poll_message_id bigint,
transfer_losing_poll_message_id bigint,
transfer_poll_message_id_1 bigint,
transfer_poll_message_id_2 bigint,
transfer_client_txn_id text,
transfer_server_txn_id text,
transfer_gaining_registrar_id text,
@@ -465,7 +471,9 @@ CREATE TABLE public."DomainHistory" (
deletion_poll_message_history_id bigint,
transfer_billing_recurrence_history_id bigint,
transfer_autorenew_poll_message_history_id bigint,
transfer_billing_event_history_id bigint
transfer_billing_event_history_id bigint,
transfer_history_entry_id bigint,
transfer_repo_id text
);
+254 -293
View File
@@ -4,12 +4,6 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@types/color-name": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
"dev": true
},
"accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
@@ -42,12 +36,11 @@
"dev": true
},
"ansi-styles": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
"integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"requires": {
"@types/color-name": "^1.1.1",
"color-convert": "^2.0.1"
}
},
@@ -67,15 +60,6 @@
"integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==",
"dev": true
},
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"dev": true,
"requires": {
"lodash": "^4.17.14"
}
},
"async-limiter": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
@@ -95,30 +79,21 @@
"dev": true
},
"base64-arraybuffer": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
"integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=",
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
"integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=",
"dev": true
},
"base64id": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz",
"integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
"dev": true
},
"better-assert": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
"integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=",
"dev": true,
"requires": {
"callsite": "1.0.0"
}
},
"binary-extensions": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
"integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"dev": true
},
"blob": {
@@ -182,12 +157,6 @@
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
"dev": true
},
"callsite": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
"integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=",
"dev": true
},
"camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
@@ -195,19 +164,19 @@
"dev": true
},
"chokidar": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.1.tgz",
"integrity": "sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==",
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
"integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==",
"dev": true,
"requires": {
"anymatch": "~3.1.1",
"braces": "~3.0.2",
"fsevents": "~2.1.2",
"fsevents": "~2.3.1",
"glob-parent": "~5.1.0",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.3.0"
"readdirp": "~3.5.0"
}
},
"cliui": {
@@ -249,9 +218,9 @@
"dev": true
},
"component-emitter": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
"integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
"dev": true
},
"component-inherit": {
@@ -297,9 +266,9 @@
"dev": true
},
"cookie": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
"integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=",
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
"dev": true
},
"core-util-is": {
@@ -315,9 +284,9 @@
"dev": true
},
"date-format": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz",
"integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/date-format/-/date-format-3.0.0.tgz",
"integrity": "sha512-eyTcpKOcamdhWJXj56DpQMo1ylSQpcGtGKXcU0Tb97+K56/CF5amAqqqNj0+KvA0iw2ynxtHWFsPDSClCxe48w==",
"dev": true
},
"debug": {
@@ -378,45 +347,51 @@
"dev": true
},
"engine.io": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.2.1.tgz",
"integrity": "sha512-+VlKzHzMhaU+GsCIg4AoXF1UdDFjHHwMmMKqMJNDNLlUlejz58FCy4LBqB2YVJskHGYl06BatYWKP2TVdVXE5w==",
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.5.0.tgz",
"integrity": "sha512-21HlvPUKaitDGE4GXNtQ7PLP0Sz4aWLddMPw2VTyFz1FVZqu/kZsJUO8WNpKuE/OCL7nkfRaOui2ZCJloGznGA==",
"dev": true,
"requires": {
"accepts": "~1.3.4",
"base64id": "1.0.0",
"cookie": "0.3.1",
"debug": "~3.1.0",
"engine.io-parser": "~2.1.0",
"ws": "~3.3.1"
"base64id": "2.0.0",
"cookie": "~0.4.1",
"debug": "~4.1.0",
"engine.io-parser": "~2.2.0",
"ws": "~7.4.2"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"dev": true,
"requires": {
"ms": "2.0.0"
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
}
}
},
"engine.io-client": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz",
"integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==",
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.0.tgz",
"integrity": "sha512-12wPRfMrugVw/DNyJk34GQ5vIVArEcVMXWugQGGuw2XxUSztFNmJggZmv8IZlLyEdnpO1QB9LkcjeWewO2vxtA==",
"dev": true,
"requires": {
"component-emitter": "1.2.1",
"component-emitter": "~1.3.0",
"component-inherit": "0.0.3",
"debug": "~3.1.0",
"engine.io-parser": "~2.1.1",
"engine.io-parser": "~2.2.0",
"has-cors": "1.1.0",
"indexof": "0.0.1",
"parseqs": "0.0.5",
"parseuri": "0.0.5",
"ws": "~3.3.1",
"parseqs": "0.0.6",
"parseuri": "0.0.6",
"ws": "~7.4.2",
"xmlhttprequest-ssl": "~1.5.4",
"yeast": "0.1.2"
},
@@ -433,14 +408,14 @@
}
},
"engine.io-parser": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz",
"integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==",
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz",
"integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==",
"dev": true,
"requires": {
"after": "0.8.2",
"arraybuffer.slice": "~0.0.7",
"base64-arraybuffer": "0.1.5",
"base64-arraybuffer": "0.1.4",
"blob": "0.0.5",
"has-binary2": "~1.0.2"
}
@@ -473,9 +448,9 @@
"dev": true
},
"eventemitter3": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.4.tgz",
"integrity": "sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ==",
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"dev": true
},
"extend": {
@@ -546,47 +521,18 @@
"dev": true
},
"follow-redirects": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.11.0.tgz",
"integrity": "sha512-KZm0V+ll8PfBrKwMzdo5D13b1bur9Iq9Zd/RMmAoQQcl2PxxFml8cxXPaaPYVbV0RjNjq1CU7zIzAOqtUPudmA==",
"dev": true,
"requires": {
"debug": "^3.0.0"
},
"dependencies": {
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
}
}
},
"fs-access": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz",
"integrity": "sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o=",
"dev": true,
"requires": {
"null-check": "^1.0.0"
}
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.2.tgz",
"integrity": "sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA==",
"dev": true
},
"fs-extra": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.2",
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
}
@@ -598,9 +544,9 @@
"dev": true
},
"fsevents": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz",
"integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==",
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.1.tgz",
"integrity": "sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw==",
"dev": true,
"optional": true
},
@@ -639,9 +585,9 @@
"integrity": "sha512-B+Cdh2c3BbvSIONufK3yU/yKwhm7vxaqrAvxIBo3JmUAhA3WQPRSculbJPKC4ca7b/pjlsIR76KDpVqVrJd4dg=="
},
"graceful-fs": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
"integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==",
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz",
"integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==",
"dev": true
},
"has-binary2": {
@@ -811,44 +757,59 @@
}
},
"karma": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/karma/-/karma-5.0.1.tgz",
"integrity": "sha512-xrDGtZ0mykEQjx1BUHOP1ITi39MDsCGocmSvLJWHxUQpxuKwxk3ZUrC6HI2VWh1plLC6+7cA3B19m12yzO/FRw==",
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/karma/-/karma-5.2.3.tgz",
"integrity": "sha512-tHdyFADhVVPBorIKCX8A37iLHxc6RBRphkSoQ+MLKdAtFn1k97tD8WUGi1KlEtDZKL3hui0qhsY9HXUfSNDYPQ==",
"dev": true,
"requires": {
"body-parser": "^1.16.1",
"body-parser": "^1.19.0",
"braces": "^3.0.2",
"chokidar": "^3.0.0",
"colors": "^1.1.0",
"connect": "^3.6.0",
"chokidar": "^3.4.2",
"colors": "^1.4.0",
"connect": "^3.7.0",
"di": "^0.0.1",
"dom-serialize": "^2.2.0",
"flatted": "^2.0.0",
"glob": "^7.1.1",
"graceful-fs": "^4.1.2",
"http-proxy": "^1.13.0",
"isbinaryfile": "^4.0.2",
"lodash": "^4.17.14",
"log4js": "^4.0.0",
"mime": "^2.3.1",
"minimatch": "^3.0.2",
"qjobs": "^1.1.4",
"range-parser": "^1.2.0",
"rimraf": "^2.6.0",
"socket.io": "2.1.1",
"dom-serialize": "^2.2.1",
"glob": "^7.1.6",
"graceful-fs": "^4.2.4",
"http-proxy": "^1.18.1",
"isbinaryfile": "^4.0.6",
"lodash": "^4.17.19",
"log4js": "^6.2.1",
"mime": "^2.4.5",
"minimatch": "^3.0.4",
"qjobs": "^1.2.0",
"range-parser": "^1.2.1",
"rimraf": "^3.0.2",
"socket.io": "^2.3.0",
"source-map": "^0.6.1",
"tmp": "0.0.33",
"ua-parser-js": "0.7.21",
"tmp": "0.2.1",
"ua-parser-js": "0.7.22",
"yargs": "^15.3.1"
},
"dependencies": {
"mime": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.5.0.tgz",
"integrity": "sha512-ft3WayFSFUVBuJj7BMLKAQcSlItKtfjsKDDsii3rqFDAZ7t11zRe8ASw/GlmivGwVUYtwkQrxiGGpL6gFvB0ag==",
"dev": true
},
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"dev": true,
"requires": {
"glob": "^7.1.3"
}
}
}
},
"karma-chrome-launcher": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz",
"integrity": "sha512-uf/ZVpAabDBPvdPdveyk1EPgbnloPvFFGgmRhYLTDH7gEB4nZdSBk8yTU47w1g/drLSx5uMOkjKk7IWKfWg/+w==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz",
"integrity": "sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg==",
"dev": true,
"requires": {
"fs-access": "^1.0.0",
"which": "^1.2.1"
}
},
@@ -877,31 +838,31 @@
}
},
"lodash": {
"version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==",
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
"dev": true
},
"log4js": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/log4js/-/log4js-4.5.1.tgz",
"integrity": "sha512-EEEgFcE9bLgaYUKuozyFfytQM2wDHtXn4tAN41pkaxpNjAykv11GVdeI4tHtmPWW4Xrgh9R/2d7XYghDVjbKKw==",
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/log4js/-/log4js-6.3.0.tgz",
"integrity": "sha512-Mc8jNuSFImQUIateBFwdOQcmC6Q5maU0VVvdC2R6XMb66/VnT+7WS4D/0EeNMZu1YODmJe5NIn2XftCzEocUgw==",
"dev": true,
"requires": {
"date-format": "^2.0.0",
"date-format": "^3.0.0",
"debug": "^4.1.1",
"flatted": "^2.0.0",
"flatted": "^2.0.1",
"rfdc": "^1.1.4",
"streamroller": "^1.0.6"
"streamroller": "^2.2.4"
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"dev": true,
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"ms": {
@@ -925,18 +886,18 @@
"dev": true
},
"mime-db": {
"version": "1.43.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz",
"integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==",
"version": "1.45.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz",
"integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==",
"dev": true
},
"mime-types": {
"version": "2.1.26",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz",
"integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==",
"version": "2.1.28",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz",
"integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==",
"dev": true,
"requires": {
"mime-db": "1.43.0"
"mime-db": "1.45.0"
}
},
"minimatch": {
@@ -983,18 +944,6 @@
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true
},
"null-check": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/null-check/-/null-check-1.0.0.tgz",
"integrity": "sha1-l33/1xdgErnsMNKjnbXPcqBDnt0=",
"dev": true
},
"object-component": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz",
"integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=",
"dev": true
},
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
@@ -1013,12 +962,6 @@
"wrappy": "1"
}
},
"os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
"dev": true
},
"p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
@@ -1044,22 +987,16 @@
"dev": true
},
"parseqs": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
"integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=",
"dev": true,
"requires": {
"better-assert": "~1.0.0"
}
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz",
"integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==",
"dev": true
},
"parseuri": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz",
"integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=",
"dev": true,
"requires": {
"better-assert": "~1.0.0"
}
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz",
"integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==",
"dev": true
},
"parseurl": {
"version": "1.3.3",
@@ -1211,12 +1148,12 @@
}
},
"readdirp": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.3.0.tgz",
"integrity": "sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ==",
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
"integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
"dev": true,
"requires": {
"picomatch": "^2.0.7"
"picomatch": "^2.2.1"
}
},
"require-directory": {
@@ -1238,9 +1175,9 @@
"dev": true
},
"rfdc": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.4.tgz",
"integrity": "sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.2.0.tgz",
"integrity": "sha512-ijLyszTMmUrXvjSooucVQwimGUk84eRcmCuLV8Xghe3UO85mjUtRAHRyoMM6XtyqbECaXuBWx18La3523sXINA==",
"dev": true
},
"rimraf": {
@@ -1252,12 +1189,6 @@
"glob": "^7.1.3"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -1277,27 +1208,33 @@
"dev": true
},
"socket.io": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.1.1.tgz",
"integrity": "sha512-rORqq9c+7W0DAK3cleWNSyfv/qKXV99hV4tZe+gGLfBECw3XEhBy7x85F3wypA9688LKjtwO9pX9L33/xQI8yA==",
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.4.1.tgz",
"integrity": "sha512-Si18v0mMXGAqLqCVpTxBa8MGqriHGQh8ccEOhmsmNS3thNCGBwO8WGrwMibANsWtQQ5NStdZwHqZR3naJVFc3w==",
"dev": true,
"requires": {
"debug": "~3.1.0",
"engine.io": "~3.2.0",
"debug": "~4.1.0",
"engine.io": "~3.5.0",
"has-binary2": "~1.0.2",
"socket.io-adapter": "~1.1.0",
"socket.io-client": "2.1.1",
"socket.io-parser": "~3.2.0"
"socket.io-client": "2.4.0",
"socket.io-parser": "~3.4.0"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"dev": true,
"requires": {
"ms": "2.0.0"
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
}
}
},
@@ -1308,24 +1245,21 @@
"dev": true
},
"socket.io-client": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.1.1.tgz",
"integrity": "sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ==",
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.4.0.tgz",
"integrity": "sha512-M6xhnKQHuuZd4Ba9vltCLT9oa+YvTsP8j9NcEiLElfIg8KeYPyhWOes6x4t+LTAC8enQbE/995AdTem2uNyKKQ==",
"dev": true,
"requires": {
"backo2": "1.0.2",
"base64-arraybuffer": "0.1.5",
"component-bind": "1.0.0",
"component-emitter": "1.2.1",
"component-emitter": "~1.3.0",
"debug": "~3.1.0",
"engine.io-client": "~3.2.0",
"engine.io-client": "~3.5.0",
"has-binary2": "~1.0.2",
"has-cors": "1.1.0",
"indexof": "0.0.1",
"object-component": "0.0.3",
"parseqs": "0.0.5",
"parseuri": "0.0.5",
"socket.io-parser": "~3.2.0",
"parseqs": "0.0.6",
"parseuri": "0.0.6",
"socket.io-parser": "~3.3.0",
"to-array": "0.1.4"
},
"dependencies": {
@@ -1337,28 +1271,51 @@
"requires": {
"ms": "2.0.0"
}
},
"socket.io-parser": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.2.tgz",
"integrity": "sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==",
"dev": true,
"requires": {
"component-emitter": "~1.3.0",
"debug": "~3.1.0",
"isarray": "2.0.1"
}
}
}
},
"socket.io-parser": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz",
"integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==",
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.1.tgz",
"integrity": "sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A==",
"dev": true,
"requires": {
"component-emitter": "1.2.1",
"debug": "~3.1.0",
"debug": "~4.1.0",
"isarray": "2.0.1"
},
"dependencies": {
"component-emitter": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
"integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
"dev": true
},
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"dev": true,
"requires": {
"ms": "2.0.0"
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
}
}
},
@@ -1375,25 +1332,29 @@
"dev": true
},
"streamroller": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/streamroller/-/streamroller-1.0.6.tgz",
"integrity": "sha512-3QC47Mhv3/aZNFpDDVO44qQb9gwB9QggMEE0sQmkTAwBVYdBRWISdsywlkfm5II1Q5y/pmrHflti/IgmIzdDBg==",
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/streamroller/-/streamroller-2.2.4.tgz",
"integrity": "sha512-OG79qm3AujAM9ImoqgWEY1xG4HX+Lw+yY6qZj9R1K2mhF5bEmQ849wvrb+4vt4jLMLzwXttJlQbOdPOQVRv7DQ==",
"dev": true,
"requires": {
"async": "^2.6.2",
"date-format": "^2.0.0",
"debug": "^3.2.6",
"fs-extra": "^7.0.1",
"lodash": "^4.17.14"
"date-format": "^2.1.0",
"debug": "^4.1.1",
"fs-extra": "^8.1.0"
},
"dependencies": {
"date-format": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz",
"integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==",
"dev": true
},
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"dev": true,
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"ms": {
@@ -1442,12 +1403,23 @@
}
},
"tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
"integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
"integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
"dev": true,
"requires": {
"os-tmpdir": "~1.0.2"
"rimraf": "^3.0.0"
},
"dependencies": {
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"dev": true,
"requires": {
"glob": "^7.1.3"
}
}
}
},
"to-array": {
@@ -1488,15 +1460,9 @@
"dev": true
},
"ua-parser-js": {
"version": "0.7.21",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz",
"integrity": "sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ==",
"dev": true
},
"ultron": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
"integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==",
"version": "0.7.22",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.22.tgz",
"integrity": "sha512-YUxzMjJ5T71w6a8WWVcMGM6YWOTX27rCoIQgLXiWaxqXSx9D7DNjiGWn1aJIRSQ5qr0xuhra77bSIh6voR/46Q==",
"dev": true
},
"universalify": {
@@ -1562,15 +1528,10 @@
"dev": true
},
"ws": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
"integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
"dev": true,
"requires": {
"async-limiter": "~1.0.0",
"safe-buffer": "~5.1.0",
"ultron": "~1.1.0"
}
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.2.tgz",
"integrity": "sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==",
"dev": true
},
"xmlhttprequest-ssl": {
"version": "1.5.5",
@@ -1579,15 +1540,15 @@
"dev": true
},
"y18n": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
"integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
"dev": true
},
"yargs": {
"version": "15.3.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz",
"integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==",
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"dev": true,
"requires": {
"cliui": "^6.0.0",
@@ -1600,13 +1561,13 @@
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.1"
"yargs-parser": "^18.1.2"
}
},
"yargs-parser": {
"version": "18.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.2.tgz",
"integrity": "sha512-hlIPNR3IzC1YuL1c2UwwDKpXlNFBqD1Fswwh1khz5+d8Cq/8yc/Mn0i+rQXduu8hcrFKvO7Eryk+09NecTQAAQ==",
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"dev": true,
"requires": {
"camelcase": "^5.0.0",
+2 -2
View File
@@ -21,8 +21,8 @@
},
"devDependencies": {
"jasmine-core": "3.4.0",
"karma": "^5.0.1",
"karma-chrome-launcher": "2.2.0",
"karma": "^5.2.3",
"karma-chrome-launcher": "3.1.0",
"karma-closure": "0.1.3",
"karma-jasmine": "2.0.1",
"puppeteer": "2.0.0"
@@ -21,6 +21,7 @@ import static google.registry.proxy.TestUtils.assertHttpRequestEquivalent;
import static google.registry.proxy.TestUtils.makeEppHttpResponse;
import static google.registry.proxy.handler.ProxyProtocolHandler.REMOTE_ADDRESS_KEY;
import static google.registry.util.X509Utils.getCertificateHash;
import static google.registry.util.X509Utils.loadCertificate;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
@@ -47,6 +48,7 @@ import io.netty.util.concurrent.Promise;
import java.io.ByteArrayInputStream;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Base64;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -239,6 +241,23 @@ class EppServiceHandlerTest {
assertThat(channel.isActive()).isTrue();
}
@Test
void testSuccess_requestContainsEncodedCertificate() throws Exception {
setHandshakeSuccess();
// First inbound message is hello.
channel.readInbound();
String content = "<epp>stuff</epp>";
channel.writeInbound(Unpooled.wrappedBuffer(content.getBytes(UTF_8)));
FullHttpRequest request = channel.readInbound();
assertThat(request).isEqualTo(makeEppHttpRequestWithCertificate(content));
String encodedCert = request.headers().get("X-SSL-Full-Certificate");
assertThat(encodedCert).isNotEqualTo(SAMPLE_CERT);
X509Certificate decodedCert =
loadCertificate(new ByteArrayInputStream(Base64.getDecoder().decode(encodedCert)));
X509Certificate pemCert = loadCertificate(SAMPLE_CERT);
assertThat(decodedCert).isEqualTo(pemCert);
}
@Test
void testSuccess_sendCertificateOnlyBeforeLogin() throws Exception {
setHandshakeSuccess();
@@ -32,6 +32,7 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CRLException;
import java.security.cert.CRLReason;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateParsingException;
@@ -39,6 +40,7 @@ import java.security.cert.CertificateRevokedException;
import java.security.cert.X509CRL;
import java.security.cert.X509CRLEntry;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.Date;
import java.util.NoSuchElementException;
import java.util.Optional;
@@ -177,5 +179,20 @@ public final class X509Utils {
newCrl.verify(rootCert.getPublicKey());
}
/** Constructs an X.509 certificate from a PEM string and encodes it. */
public static String encodeX509CertificateFromPemString(String certificateString)
throws CertificateException {
return encodeX509Certificate(loadCertificate(certificateString));
}
/**
* Encodes an X.509 certificate in the same form that the proxy encodes a certificate before
* passing it via an HTTP header.
*/
public static String encodeX509Certificate(X509Certificate certificate)
throws CertificateEncodingException {
return Base64.getEncoder().encodeToString(certificate.getEncoded());
}
private X509Utils() {}
}