mirror of
https://github.com/google/nomulus
synced 2026-06-09 16:33:02 +00:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 23896b64c7 | |||
| 844b5ab713 | |||
| aac952d6a3 | |||
| ee31f1fd95 | |||
| 4657be21b7 | |||
| 48732c51e8 | |||
| 7893ba746a | |||
| 1c96cd64fe | |||
| bc2a5dbc02 | |||
| 98d259449b | |||
| 1cc8af4acd | |||
| fbef643488 | |||
| 2161e46a4b | |||
| d7f27bdad3 | |||
| 78e139b2c8 | |||
| 87d511d5e3 | |||
| eff79e9c99 | |||
| bb453b1982 | |||
| 8b41b5c76f | |||
| 881f0f5f09 | |||
| abe6a193a8 | |||
| d35460f14c | |||
| 245e2ea5a8 | |||
| 65f35ac8c1 | |||
| 994af085d8 | |||
| ce25cea134 | |||
| 92dcacf78c | |||
| 020273b184 | |||
| 0156a29f93 | |||
| 0b520f3885 | |||
| da6d90755e | |||
| 4d04e4fd15 | |||
| 928b272d89 | |||
| e31f0cb9ba | |||
| 06b0887c51 | |||
| 73dcb4de4e | |||
| 9dd08c48bc | |||
| eabf056f9b | |||
| 7c3ef52026 | |||
| 75e74f013d | |||
| c077aca433 | |||
| 4e7dd7a95a | |||
| 8952687207 | |||
| 0164bceb95 | |||
| dc51019fd2 | |||
| 36762b5e08 |
+1
-1
@@ -24,7 +24,7 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.google.cloud.tools:appengine-gradle-plugin:2.0.1'
|
||||
classpath 'com.google.cloud.tools:appengine-gradle-plugin:2.4.1'
|
||||
classpath 'net.ltgt.gradle:gradle-errorprone-plugin:0.6.1'
|
||||
classpath 'org.sonatype.aether:aether-api:1.13.1'
|
||||
classpath 'org.sonatype.aether:aether-impl:1.13.1'
|
||||
|
||||
@@ -91,7 +91,7 @@ abstract class ProjectData {
|
||||
/** The task was actually run and has finished successfully. */
|
||||
SUCCESS,
|
||||
/** The task was up-to-date and successful, and hence didn't need to run again. */
|
||||
UP_TO_DATE;
|
||||
UP_TO_DATE
|
||||
}
|
||||
|
||||
abstract String uniqueName();
|
||||
|
||||
@@ -35,6 +35,8 @@ public final class FakeClock implements Clock {
|
||||
// threads should see a consistent flow.
|
||||
private final AtomicLong currentTimeMillis = new AtomicLong();
|
||||
|
||||
private volatile long autoIncrementStepMs;
|
||||
|
||||
/** Creates a FakeClock that starts at START_OF_TIME. */
|
||||
public FakeClock() {
|
||||
this(START_OF_TIME);
|
||||
@@ -48,7 +50,21 @@ public final class FakeClock implements Clock {
|
||||
/** Returns the current time. */
|
||||
@Override
|
||||
public DateTime nowUtc() {
|
||||
return new DateTime(currentTimeMillis.get(), UTC);
|
||||
return new DateTime(currentTimeMillis.addAndGet(autoIncrementStepMs), UTC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the increment applied to the clock whenever it is queried. The increment is zero by
|
||||
* default: the clock is left unchanged when queried.
|
||||
*
|
||||
* <p>Passing a duration of zero to this method effectively unsets the auto increment mode.
|
||||
*
|
||||
* @param autoIncrementStep the new auto increment duration
|
||||
* @return this
|
||||
*/
|
||||
public FakeClock setAutoIncrementStep(ReadableDuration autoIncrementStep) {
|
||||
this.autoIncrementStepMs = autoIncrementStep.getMillis();
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Advances clock by one millisecond. */
|
||||
|
||||
+1
-1
@@ -423,7 +423,7 @@ task jaxbToJava {
|
||||
}
|
||||
}
|
||||
execInBash(
|
||||
'find . -name *.java -exec sed -i /\\*\\ \\<p\\>\\$/d {} +',
|
||||
"find . -name *.java -exec sed -i -e '/" + /\* <p>$/ + "/d' {} +",
|
||||
generatedDir)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,10 +253,10 @@ org.postgresql:postgresql:42.2.18
|
||||
org.rnorth.duct-tape:duct-tape:1.0.8
|
||||
org.rnorth.visible-assertions:visible-assertions:2.1.2
|
||||
org.slf4j:slf4j-api:1.7.30
|
||||
org.testcontainers:database-commons:1.15.1
|
||||
org.testcontainers:jdbc:1.15.1
|
||||
org.testcontainers:postgresql:1.15.1
|
||||
org.testcontainers:testcontainers:1.15.1
|
||||
org.testcontainers:database-commons:1.15.2
|
||||
org.testcontainers:jdbc:1.15.2
|
||||
org.testcontainers:postgresql:1.15.2
|
||||
org.testcontainers:testcontainers:1.15.2
|
||||
org.threeten:threetenbp:1.5.0
|
||||
org.tukaani:xz:1.5
|
||||
org.w3c.css:sac:1.3
|
||||
|
||||
@@ -246,10 +246,10 @@ org.postgresql:postgresql:42.2.18
|
||||
org.rnorth.duct-tape:duct-tape:1.0.8
|
||||
org.rnorth.visible-assertions:visible-assertions:2.1.2
|
||||
org.slf4j:slf4j-api:1.7.30
|
||||
org.testcontainers:database-commons:1.15.1
|
||||
org.testcontainers:jdbc:1.15.1
|
||||
org.testcontainers:postgresql:1.15.1
|
||||
org.testcontainers:testcontainers:1.15.1
|
||||
org.testcontainers:database-commons:1.15.2
|
||||
org.testcontainers:jdbc:1.15.2
|
||||
org.testcontainers:postgresql:1.15.2
|
||||
org.testcontainers:testcontainers:1.15.2
|
||||
org.threeten:threetenbp:1.5.0
|
||||
org.tukaani:xz:1.5
|
||||
org.w3c.css:sac:1.3
|
||||
|
||||
@@ -267,10 +267,10 @@ org.slf4j:jcl-over-slf4j:1.7.30
|
||||
org.slf4j:jul-to-slf4j:1.7.30
|
||||
org.slf4j:slf4j-api:1.7.30
|
||||
org.slf4j:slf4j-jdk14:1.7.28
|
||||
org.testcontainers:database-commons:1.15.1
|
||||
org.testcontainers:jdbc:1.15.1
|
||||
org.testcontainers:postgresql:1.15.1
|
||||
org.testcontainers:testcontainers:1.15.1
|
||||
org.testcontainers:database-commons:1.15.2
|
||||
org.testcontainers:jdbc:1.15.2
|
||||
org.testcontainers:postgresql:1.15.2
|
||||
org.testcontainers:testcontainers:1.15.2
|
||||
org.threeten:threetenbp:1.5.0
|
||||
org.tukaani:xz:1.5
|
||||
org.w3c.css:sac:1.3
|
||||
|
||||
@@ -266,10 +266,10 @@ org.slf4j:jcl-over-slf4j:1.7.30
|
||||
org.slf4j:jul-to-slf4j:1.7.30
|
||||
org.slf4j:slf4j-api:1.7.30
|
||||
org.slf4j:slf4j-jdk14:1.7.28
|
||||
org.testcontainers:database-commons:1.15.1
|
||||
org.testcontainers:jdbc:1.15.1
|
||||
org.testcontainers:postgresql:1.15.1
|
||||
org.testcontainers:testcontainers:1.15.1
|
||||
org.testcontainers:database-commons:1.15.2
|
||||
org.testcontainers:jdbc:1.15.2
|
||||
org.testcontainers:postgresql:1.15.2
|
||||
org.testcontainers:testcontainers:1.15.2
|
||||
org.threeten:threetenbp:1.5.0
|
||||
org.tukaani:xz:1.5
|
||||
org.w3c.css:sac:1.3
|
||||
|
||||
@@ -253,10 +253,10 @@ org.postgresql:postgresql:42.2.18
|
||||
org.rnorth.duct-tape:duct-tape:1.0.8
|
||||
org.rnorth.visible-assertions:visible-assertions:2.1.2
|
||||
org.slf4j:slf4j-api:1.7.30
|
||||
org.testcontainers:database-commons:1.15.1
|
||||
org.testcontainers:jdbc:1.15.1
|
||||
org.testcontainers:postgresql:1.15.1
|
||||
org.testcontainers:testcontainers:1.15.1
|
||||
org.testcontainers:database-commons:1.15.2
|
||||
org.testcontainers:jdbc:1.15.2
|
||||
org.testcontainers:postgresql:1.15.2
|
||||
org.testcontainers:testcontainers:1.15.2
|
||||
org.threeten:threetenbp:1.5.0
|
||||
org.tukaani:xz:1.5
|
||||
org.w3c.css:sac:1.3
|
||||
|
||||
@@ -247,10 +247,10 @@ org.postgresql:postgresql:42.2.18
|
||||
org.rnorth.duct-tape:duct-tape:1.0.8
|
||||
org.rnorth.visible-assertions:visible-assertions:2.1.2
|
||||
org.slf4j:slf4j-api:1.7.30
|
||||
org.testcontainers:database-commons:1.15.1
|
||||
org.testcontainers:jdbc:1.15.1
|
||||
org.testcontainers:postgresql:1.15.1
|
||||
org.testcontainers:testcontainers:1.15.1
|
||||
org.testcontainers:database-commons:1.15.2
|
||||
org.testcontainers:jdbc:1.15.2
|
||||
org.testcontainers:postgresql:1.15.2
|
||||
org.testcontainers:testcontainers:1.15.2
|
||||
org.threeten:threetenbp:1.5.0
|
||||
org.tukaani:xz:1.5
|
||||
org.w3c.css:sac:1.3
|
||||
|
||||
@@ -265,10 +265,10 @@ org.rnorth.visible-assertions:visible-assertions:2.1.2
|
||||
org.slf4j:jcl-over-slf4j:1.7.30
|
||||
org.slf4j:jul-to-slf4j:1.7.30
|
||||
org.slf4j:slf4j-api:1.7.30
|
||||
org.testcontainers:database-commons:1.15.1
|
||||
org.testcontainers:jdbc:1.15.1
|
||||
org.testcontainers:postgresql:1.15.1
|
||||
org.testcontainers:testcontainers:1.15.1
|
||||
org.testcontainers:database-commons:1.15.2
|
||||
org.testcontainers:jdbc:1.15.2
|
||||
org.testcontainers:postgresql:1.15.2
|
||||
org.testcontainers:testcontainers:1.15.2
|
||||
org.threeten:threetenbp:1.5.0
|
||||
org.tukaani:xz:1.5
|
||||
org.w3c.css:sac:1.3
|
||||
|
||||
@@ -265,10 +265,10 @@ org.rnorth.visible-assertions:visible-assertions:2.1.2
|
||||
org.slf4j:jcl-over-slf4j:1.7.30
|
||||
org.slf4j:jul-to-slf4j:1.7.30
|
||||
org.slf4j:slf4j-api:1.7.30
|
||||
org.testcontainers:database-commons:1.15.1
|
||||
org.testcontainers:jdbc:1.15.1
|
||||
org.testcontainers:postgresql:1.15.1
|
||||
org.testcontainers:testcontainers:1.15.1
|
||||
org.testcontainers:database-commons:1.15.2
|
||||
org.testcontainers:jdbc:1.15.2
|
||||
org.testcontainers:postgresql:1.15.2
|
||||
org.testcontainers:testcontainers:1.15.2
|
||||
org.threeten:threetenbp:1.5.0
|
||||
org.tukaani:xz:1.5
|
||||
org.w3c.css:sac:1.3
|
||||
|
||||
@@ -265,10 +265,10 @@ org.rnorth.visible-assertions:visible-assertions:2.1.2
|
||||
org.slf4j:jcl-over-slf4j:1.7.30
|
||||
org.slf4j:jul-to-slf4j:1.7.30
|
||||
org.slf4j:slf4j-api:1.7.30
|
||||
org.testcontainers:database-commons:1.15.1
|
||||
org.testcontainers:jdbc:1.15.1
|
||||
org.testcontainers:postgresql:1.15.1
|
||||
org.testcontainers:testcontainers:1.15.1
|
||||
org.testcontainers:database-commons:1.15.2
|
||||
org.testcontainers:jdbc:1.15.2
|
||||
org.testcontainers:postgresql:1.15.2
|
||||
org.testcontainers:testcontainers:1.15.2
|
||||
org.threeten:threetenbp:1.5.0
|
||||
org.tukaani:xz:1.5
|
||||
org.w3c.css:sac:1.3
|
||||
|
||||
@@ -266,10 +266,10 @@ org.slf4j:jcl-over-slf4j:1.7.30
|
||||
org.slf4j:jul-to-slf4j:1.7.30
|
||||
org.slf4j:slf4j-api:1.7.30
|
||||
org.slf4j:slf4j-jdk14:1.7.28
|
||||
org.testcontainers:database-commons:1.15.1
|
||||
org.testcontainers:jdbc:1.15.1
|
||||
org.testcontainers:postgresql:1.15.1
|
||||
org.testcontainers:testcontainers:1.15.1
|
||||
org.testcontainers:database-commons:1.15.2
|
||||
org.testcontainers:jdbc:1.15.2
|
||||
org.testcontainers:postgresql:1.15.2
|
||||
org.testcontainers:testcontainers:1.15.2
|
||||
org.threeten:threetenbp:1.5.0
|
||||
org.tukaani:xz:1.5
|
||||
org.w3c.css:sac:1.3
|
||||
|
||||
@@ -300,12 +300,12 @@ org.seleniumhq.selenium:selenium-remote-driver:3.141.59
|
||||
org.seleniumhq.selenium:selenium-safari-driver:3.141.59
|
||||
org.seleniumhq.selenium:selenium-support:3.141.59
|
||||
org.slf4j:slf4j-api:1.7.30
|
||||
org.testcontainers:database-commons:1.15.1
|
||||
org.testcontainers:jdbc:1.15.1
|
||||
org.testcontainers:junit-jupiter:1.15.1
|
||||
org.testcontainers:postgresql:1.15.1
|
||||
org.testcontainers:selenium:1.15.1
|
||||
org.testcontainers:testcontainers:1.15.1
|
||||
org.testcontainers:database-commons:1.15.2
|
||||
org.testcontainers:jdbc:1.15.2
|
||||
org.testcontainers:junit-jupiter:1.15.2
|
||||
org.testcontainers:postgresql:1.15.2
|
||||
org.testcontainers:selenium:1.15.2
|
||||
org.testcontainers:testcontainers:1.15.2
|
||||
org.threeten:threetenbp:1.5.0
|
||||
org.tukaani:xz:1.5
|
||||
org.w3c.css:sac:1.3
|
||||
|
||||
@@ -294,12 +294,12 @@ org.seleniumhq.selenium:selenium-remote-driver:3.141.59
|
||||
org.seleniumhq.selenium:selenium-safari-driver:3.141.59
|
||||
org.seleniumhq.selenium:selenium-support:3.141.59
|
||||
org.slf4j:slf4j-api:1.7.30
|
||||
org.testcontainers:database-commons:1.15.1
|
||||
org.testcontainers:jdbc:1.15.1
|
||||
org.testcontainers:junit-jupiter:1.15.1
|
||||
org.testcontainers:postgresql:1.15.1
|
||||
org.testcontainers:selenium:1.15.1
|
||||
org.testcontainers:testcontainers:1.15.1
|
||||
org.testcontainers:database-commons:1.15.2
|
||||
org.testcontainers:jdbc:1.15.2
|
||||
org.testcontainers:junit-jupiter:1.15.2
|
||||
org.testcontainers:postgresql:1.15.2
|
||||
org.testcontainers:selenium:1.15.2
|
||||
org.testcontainers:testcontainers:1.15.2
|
||||
org.threeten:threetenbp:1.5.0
|
||||
org.tukaani:xz:1.5
|
||||
org.w3c.css:sac:1.3
|
||||
|
||||
@@ -313,12 +313,12 @@ org.seleniumhq.selenium:selenium-support:3.141.59
|
||||
org.slf4j:jcl-over-slf4j:1.7.30
|
||||
org.slf4j:jul-to-slf4j:1.7.30
|
||||
org.slf4j:slf4j-api:1.7.30
|
||||
org.testcontainers:database-commons:1.15.1
|
||||
org.testcontainers:jdbc:1.15.1
|
||||
org.testcontainers:junit-jupiter:1.15.1
|
||||
org.testcontainers:postgresql:1.15.1
|
||||
org.testcontainers:selenium:1.15.1
|
||||
org.testcontainers:testcontainers:1.15.1
|
||||
org.testcontainers:database-commons:1.15.2
|
||||
org.testcontainers:jdbc:1.15.2
|
||||
org.testcontainers:junit-jupiter:1.15.2
|
||||
org.testcontainers:postgresql:1.15.2
|
||||
org.testcontainers:selenium:1.15.2
|
||||
org.testcontainers:testcontainers:1.15.2
|
||||
org.threeten:threetenbp:1.5.0
|
||||
org.tukaani:xz:1.5
|
||||
org.w3c.css:sac:1.3
|
||||
|
||||
@@ -314,12 +314,12 @@ org.slf4j:jcl-over-slf4j:1.7.30
|
||||
org.slf4j:jul-to-slf4j:1.7.30
|
||||
org.slf4j:slf4j-api:1.7.30
|
||||
org.slf4j:slf4j-jdk14:1.7.28
|
||||
org.testcontainers:database-commons:1.15.1
|
||||
org.testcontainers:jdbc:1.15.1
|
||||
org.testcontainers:junit-jupiter:1.15.1
|
||||
org.testcontainers:postgresql:1.15.1
|
||||
org.testcontainers:selenium:1.15.1
|
||||
org.testcontainers:testcontainers:1.15.1
|
||||
org.testcontainers:database-commons:1.15.2
|
||||
org.testcontainers:jdbc:1.15.2
|
||||
org.testcontainers:junit-jupiter:1.15.2
|
||||
org.testcontainers:postgresql:1.15.2
|
||||
org.testcontainers:selenium:1.15.2
|
||||
org.testcontainers:testcontainers:1.15.2
|
||||
org.threeten:threetenbp:1.5.0
|
||||
org.tukaani:xz:1.5
|
||||
org.w3c.css:sac:1.3
|
||||
|
||||
@@ -154,7 +154,13 @@ public class ReplayCommitLogsToSqlAction implements Runnable {
|
||||
Object ofyPojo = ofy().toPojo(entity);
|
||||
if (ofyPojo instanceof DatastoreEntity) {
|
||||
DatastoreEntity datastoreEntity = (DatastoreEntity) ofyPojo;
|
||||
datastoreEntity.toSqlEntity().ifPresent(jpaTm()::put);
|
||||
datastoreEntity
|
||||
.toSqlEntity()
|
||||
.ifPresent(
|
||||
sqlEntity -> {
|
||||
ReplaySpecializer.beforeSqlSave(sqlEntity);
|
||||
jpaTm().put(sqlEntity);
|
||||
});
|
||||
} else {
|
||||
// this should never happen, but we shouldn't fail on it
|
||||
logger.atSevere().log(
|
||||
|
||||
@@ -57,8 +57,6 @@ public final class AsyncTaskEnqueuer {
|
||||
public static final String QUEUE_ASYNC_DELETE = "async-delete-pull";
|
||||
public static final String QUEUE_ASYNC_HOST_RENAME = "async-host-rename-pull";
|
||||
|
||||
public static final String PATH_RESAVE_ENTITY = "/_dr/task/resaveEntity";
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
private static final Duration MAX_ASYNC_ETA = Duration.standardDays(30);
|
||||
|
||||
@@ -112,7 +110,7 @@ public final class AsyncTaskEnqueuer {
|
||||
logger.atInfo().log("Enqueuing async re-save of %s to run at %s.", entityKey, whenToResave);
|
||||
String backendHostname = appEngineServiceUtils.getServiceHostname("backend");
|
||||
TaskOptions task =
|
||||
TaskOptions.Builder.withUrl(PATH_RESAVE_ENTITY)
|
||||
TaskOptions.Builder.withUrl(ResaveEntityAction.PATH)
|
||||
.method(Method.POST)
|
||||
.header("Host", backendHostname)
|
||||
.countdownMillis(etaDuration.getMillis())
|
||||
|
||||
@@ -25,7 +25,7 @@ import static google.registry.batch.AsyncTaskMetrics.OperationType.DNS_REFRESH;
|
||||
import static google.registry.mapreduce.inputs.EppResourceInputs.createEntityInput;
|
||||
import static google.registry.model.EppResourceUtils.isActive;
|
||||
import static google.registry.model.EppResourceUtils.isDeleted;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.util.DateTimeUtils.latestOf;
|
||||
import static java.util.concurrent.TimeUnit.DAYS;
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
@@ -44,7 +44,6 @@ import com.google.auto.value.AutoValue;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.batch.AsyncTaskMetrics.OperationResult;
|
||||
import google.registry.dns.DnsQueue;
|
||||
import google.registry.mapreduce.MapreduceRunner;
|
||||
@@ -64,6 +63,7 @@ import google.registry.util.SystemClock;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
import javax.annotation.Nullable;
|
||||
@@ -123,7 +123,7 @@ public class RefreshDnsOnHostRenameAction implements Runnable {
|
||||
}
|
||||
|
||||
ImmutableList.Builder<DnsRefreshRequest> requestsBuilder = new ImmutableList.Builder<>();
|
||||
ImmutableList.Builder<Key<HostResource>> hostKeys = new ImmutableList.Builder<>();
|
||||
ImmutableList.Builder<VKey<HostResource>> hostKeys = new ImmutableList.Builder<>();
|
||||
final List<DnsRefreshRequest> requestsToDelete = new ArrayList<>();
|
||||
|
||||
for (TaskHandle task : tasks) {
|
||||
@@ -204,10 +204,10 @@ public class RefreshDnsOnHostRenameAction implements Runnable {
|
||||
emit(true, true);
|
||||
return;
|
||||
}
|
||||
Key<HostResource> referencingHostKey = null;
|
||||
VKey<HostResource> referencingHostKey = null;
|
||||
for (DnsRefreshRequest request : refreshRequests) {
|
||||
if (isActive(domain, request.lastUpdateTime())
|
||||
&& domain.getNameservers().contains(VKey.from(request.hostKey()))) {
|
||||
&& domain.getNameservers().contains(request.hostKey())) {
|
||||
referencingHostKey = request.hostKey();
|
||||
break;
|
||||
}
|
||||
@@ -293,7 +293,8 @@ public class RefreshDnsOnHostRenameAction implements Runnable {
|
||||
|
||||
private static final long serialVersionUID = 1772812852271288622L;
|
||||
|
||||
abstract Key<HostResource> hostKey();
|
||||
abstract VKey<HostResource> hostKey();
|
||||
|
||||
abstract DateTime lastUpdateTime();
|
||||
abstract DateTime requestedTime();
|
||||
abstract boolean isRefreshNeeded();
|
||||
@@ -301,7 +302,8 @@ public class RefreshDnsOnHostRenameAction implements Runnable {
|
||||
|
||||
@AutoValue.Builder
|
||||
abstract static class Builder {
|
||||
abstract Builder setHostKey(Key<HostResource> hostKey);
|
||||
abstract Builder setHostKey(VKey<HostResource> hostKey);
|
||||
|
||||
abstract Builder setLastUpdateTime(DateTime lastUpdateTime);
|
||||
abstract Builder setRequestedTime(DateTime requestedTime);
|
||||
abstract Builder setIsRefreshNeeded(boolean isRefreshNeeded);
|
||||
@@ -314,10 +316,12 @@ public class RefreshDnsOnHostRenameAction implements Runnable {
|
||||
*/
|
||||
static DnsRefreshRequest createFromTask(TaskHandle task, DateTime now) throws Exception {
|
||||
ImmutableMap<String, String> params = ImmutableMap.copyOf(task.extractParams());
|
||||
Key<HostResource> hostKey =
|
||||
Key.create(checkNotNull(params.get(PARAM_HOST_KEY), "Host to refresh not specified"));
|
||||
VKey<HostResource> hostKey =
|
||||
VKey.fromWebsafeKey(
|
||||
checkNotNull(params.get(PARAM_HOST_KEY), "Host to refresh not specified"));
|
||||
HostResource host =
|
||||
checkNotNull(ofy().load().key(hostKey).now(), "Host to refresh doesn't exist");
|
||||
tm().transact(() -> tm().loadByKeyIfPresent(hostKey))
|
||||
.orElseThrow(() -> new NoSuchElementException("Host to refresh doesn't exist"));
|
||||
boolean isHostDeleted =
|
||||
isDeleted(host, latestOf(now, host.getUpdateTimestamp().getTimestamp()));
|
||||
if (isHostDeleted) {
|
||||
|
||||
@@ -16,7 +16,6 @@ package google.registry.batch;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.request.Action.Method.POST;
|
||||
@@ -33,6 +32,7 @@ import google.registry.model.eppcommon.StatusValue;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.model.registrar.RegistrarContact;
|
||||
import google.registry.model.registry.RegistryLockDao;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.Parameter;
|
||||
import google.registry.request.Response;
|
||||
@@ -125,6 +125,7 @@ public class RelockDomainAction implements Runnable {
|
||||
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
|
||||
|
||||
// nb: DomainLockUtils relies on the JPA transaction being the outermost transaction
|
||||
// if we have Datastore as the primary DB (if SQL is the primary DB, it's irrelevant)
|
||||
jpaTm().transact(() -> tm().transact(this::relockDomain));
|
||||
}
|
||||
|
||||
@@ -139,12 +140,8 @@ public class RelockDomainAction implements Runnable {
|
||||
new IllegalArgumentException(
|
||||
String.format("Unknown revision ID %d", oldUnlockRevisionId)));
|
||||
domain =
|
||||
ofy()
|
||||
.load()
|
||||
.type(DomainBase.class)
|
||||
.id(oldLock.getRepoId())
|
||||
.now()
|
||||
.cloneProjectedAtTime(jpaTm().getTransactionTime());
|
||||
tm().loadByKey(VKey.create(DomainBase.class, oldLock.getRepoId()))
|
||||
.cloneProjectedAtTime(tm().getTransactionTime());
|
||||
} catch (Throwable t) {
|
||||
handleTransientFailure(Optional.ofNullable(oldLock), t);
|
||||
return;
|
||||
|
||||
@@ -17,7 +17,6 @@ package google.registry.batch;
|
||||
import static google.registry.batch.AsyncTaskEnqueuer.PARAM_REQUESTED_TIME;
|
||||
import static google.registry.batch.AsyncTaskEnqueuer.PARAM_RESAVE_TIMES;
|
||||
import static google.registry.batch.AsyncTaskEnqueuer.PARAM_RESOURCE_KEY;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
@@ -26,6 +25,7 @@ import com.google.common.flogger.FluentLogger;
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.model.EppResource;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.Action.Method;
|
||||
import google.registry.request.Parameter;
|
||||
@@ -74,16 +74,17 @@ public class ResaveEntityAction implements Runnable {
|
||||
public void run() {
|
||||
logger.atInfo().log(
|
||||
"Re-saving entity %s which was enqueued at %s.", resourceKey, requestedTime);
|
||||
tm().transact(() -> {
|
||||
ImmutableObject entity = ofy().load().key(resourceKey).now();
|
||||
ofy().save().entity(
|
||||
(entity instanceof EppResource)
|
||||
? ((EppResource) entity).cloneProjectedAtTime(tm().getTransactionTime()) : entity
|
||||
);
|
||||
if (!resaveTimes.isEmpty()) {
|
||||
asyncTaskEnqueuer.enqueueAsyncResave(entity, requestedTime, resaveTimes);
|
||||
}
|
||||
});
|
||||
tm().transact(
|
||||
() -> {
|
||||
ImmutableObject entity = tm().loadByKey(VKey.from(resourceKey));
|
||||
tm().put(
|
||||
(entity instanceof EppResource)
|
||||
? ((EppResource) entity).cloneProjectedAtTime(tm().getTransactionTime())
|
||||
: entity);
|
||||
if (!resaveTimes.isEmpty()) {
|
||||
asyncTaskEnqueuer.enqueueAsyncResave(entity, requestedTime, resaveTimes);
|
||||
}
|
||||
});
|
||||
response.setPayload("Entity re-saved.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.batch;
|
||||
|
||||
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
||||
|
||||
import com.google.api.services.dataflow.Dataflow;
|
||||
import com.google.api.services.dataflow.model.LaunchFlexTemplateParameter;
|
||||
import com.google.api.services.dataflow.model.LaunchFlexTemplateRequest;
|
||||
import com.google.api.services.dataflow.model.LaunchFlexTemplateResponse;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.request.auth.Auth;
|
||||
import javax.inject.Inject;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.DateTimeZone;
|
||||
|
||||
/**
|
||||
* Wipes out all Cloud Datastore data in a Nomulus GCP environment.
|
||||
*
|
||||
* <p>This class is created for the QA environment, where migration testing with production data
|
||||
* will happen. A regularly scheduled wipeout is a prerequisite to using production data there.
|
||||
*/
|
||||
@Action(
|
||||
service = Action.Service.BACKEND,
|
||||
path = "/_dr/task/wipeOutDatastore",
|
||||
auth = Auth.AUTH_INTERNAL_OR_ADMIN)
|
||||
public class WipeoutDatastoreAction implements Runnable {
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
private static final String PIPELINE_NAME = "bulk_delete_datastore_pipeline";
|
||||
|
||||
// As a short-lived class, hardcode allowed projects here instead of using config files.
|
||||
private static final ImmutableSet<String> ALLOWED_PROJECTS =
|
||||
ImmutableSet.of("domain-registry-qa");
|
||||
|
||||
private final String projectId;
|
||||
private final String jobRegion;
|
||||
private final Response response;
|
||||
private final Dataflow dataflow;
|
||||
private final String stagingBucketUrl;
|
||||
|
||||
@Inject
|
||||
WipeoutDatastoreAction(
|
||||
@Config("projectId") String projectId,
|
||||
@Config("defaultJobRegion") String jobRegion,
|
||||
@Config("beamStagingBucketUrl") String stagingBucketUrl,
|
||||
Response response,
|
||||
Dataflow dataflow) {
|
||||
this.projectId = projectId;
|
||||
this.jobRegion = jobRegion;
|
||||
this.stagingBucketUrl = stagingBucketUrl;
|
||||
this.response = response;
|
||||
this.dataflow = dataflow;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
response.setContentType(PLAIN_TEXT_UTF_8);
|
||||
|
||||
if (!ALLOWED_PROJECTS.contains(projectId)) {
|
||||
response.setStatus(SC_FORBIDDEN);
|
||||
response.setPayload("Wipeout is not allowed in " + projectId);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
LaunchFlexTemplateParameter parameters =
|
||||
new LaunchFlexTemplateParameter()
|
||||
// Job name must be unique and in [-a-z0-9].
|
||||
.setJobName(
|
||||
"bulk-delete-datastore-"
|
||||
+ DateTime.now(DateTimeZone.UTC).toString("yyyy-MM-dd'T'HH-mm-ss'Z'"))
|
||||
.setContainerSpecGcsPath(
|
||||
String.format("%s/%s_metadata.json", stagingBucketUrl, PIPELINE_NAME))
|
||||
.setParameters(ImmutableMap.of("kindsToDelete", "*"));
|
||||
LaunchFlexTemplateResponse launchResponse =
|
||||
dataflow
|
||||
.projects()
|
||||
.locations()
|
||||
.flexTemplates()
|
||||
.launch(
|
||||
projectId,
|
||||
jobRegion,
|
||||
new LaunchFlexTemplateRequest().setLaunchParameter(parameters))
|
||||
.execute();
|
||||
response.setStatus(SC_OK);
|
||||
response.setPayload("Launched " + launchResponse.getJob().getName());
|
||||
} catch (Exception e) {
|
||||
String msg = String.format("Failed to launch %s.", PIPELINE_NAME);
|
||||
logger.atSevere().withCause(e).log(msg);
|
||||
response.setStatus(SC_INTERNAL_SERVER_ERROR);
|
||||
response.setPayload(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -505,7 +505,7 @@ public class DatastoreV1 {
|
||||
}
|
||||
|
||||
@StartBundle
|
||||
public void startBundle(StartBundleContext c) throws Exception {
|
||||
public void startBundle(StartBundleContext c) {
|
||||
datastore =
|
||||
datastoreFactory.getDatastore(
|
||||
c.getPipelineOptions(), v1Options.getProjectId(), v1Options.getLocalhost());
|
||||
@@ -548,7 +548,7 @@ public class DatastoreV1 {
|
||||
}
|
||||
|
||||
@StartBundle
|
||||
public void startBundle(StartBundleContext c) throws Exception {
|
||||
public void startBundle(StartBundleContext c) {
|
||||
datastore =
|
||||
datastoreFactory.getDatastore(
|
||||
c.getPipelineOptions(), options.getProjectId(), options.getLocalhost());
|
||||
@@ -556,7 +556,7 @@ public class DatastoreV1 {
|
||||
}
|
||||
|
||||
@ProcessElement
|
||||
public void processElement(ProcessContext c) throws Exception {
|
||||
public void processElement(ProcessContext c) {
|
||||
Query query = c.element();
|
||||
|
||||
// If query has a user set limit, then do not split.
|
||||
@@ -626,7 +626,7 @@ public class DatastoreV1 {
|
||||
}
|
||||
|
||||
@StartBundle
|
||||
public void startBundle(StartBundleContext c) throws Exception {
|
||||
public void startBundle(StartBundleContext c) {
|
||||
datastore =
|
||||
datastoreFactory.getDatastore(
|
||||
c.getPipelineOptions(), options.getProjectId(), options.getLocalhost());
|
||||
|
||||
@@ -93,7 +93,7 @@ public final class BackupPaths {
|
||||
checkArgument(!isNullOrEmpty(exportDir), "Null or empty exportDir.");
|
||||
checkArgument(!isNullOrEmpty(kind), "Null or empty kind.");
|
||||
checkArgument(shard >= 0, "Negative shard %s not allowed.", shard);
|
||||
return String.format(EXPORT_PATTERN_TEMPLATE, exportDir, kind, Integer.toString(shard));
|
||||
return String.format(EXPORT_PATTERN_TEMPLATE, exportDir, kind, shard);
|
||||
}
|
||||
|
||||
/** Returns an {@link ImmutableList} of regex patterns that match all CommitLog files. */
|
||||
|
||||
@@ -403,12 +403,6 @@ public final class RegistryConfig {
|
||||
return config.cloudSql.jdbcUrl;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Config("cloudSqlUsername")
|
||||
public static String providesCloudSqlUsername(RegistryConfigSettings config) {
|
||||
return config.cloudSql.username;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Config("cloudSqlInstanceConnectionName")
|
||||
public static String providesCloudSqlInstanceConnectionName(RegistryConfigSettings config) {
|
||||
@@ -1342,12 +1336,6 @@ public final class RegistryConfig {
|
||||
return config.registryTool.clientSecret;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Config("toolsCloudSqlUsername")
|
||||
public static String providesToolsCloudSqlUsername(RegistryConfigSettings config) {
|
||||
return config.registryTool.username;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Config("rdapTos")
|
||||
public static ImmutableList<String> provideRdapTos(RegistryConfigSettings config) {
|
||||
|
||||
@@ -123,6 +123,7 @@ public class RegistryConfigSettings {
|
||||
/** Configuration for Cloud SQL. */
|
||||
public static class CloudSql {
|
||||
public String jdbcUrl;
|
||||
// TODO(05012021): remove username field after it is removed from all yaml files.
|
||||
public String username;
|
||||
public String instanceConnectionName;
|
||||
public boolean replicateTransactions;
|
||||
@@ -221,6 +222,7 @@ public class RegistryConfigSettings {
|
||||
public static class RegistryTool {
|
||||
public String clientId;
|
||||
public String clientSecret;
|
||||
// TODO(05012021): remove username field after it is removed from all yaml files.
|
||||
public String username;
|
||||
}
|
||||
|
||||
|
||||
@@ -225,8 +225,6 @@ cloudSql:
|
||||
# If jdbcUrl in this file is moved elsewhere, be sure to move this notice
|
||||
# with it until the change is applied.
|
||||
jdbcUrl: jdbc:postgresql://localhost
|
||||
# Username for the database user.
|
||||
username: username
|
||||
# This name is used by Cloud SQL when connecting to the database.
|
||||
instanceConnectionName: project-id:region:instance-id
|
||||
# Set this to true to replicate cloud SQL transactions to datastore in the
|
||||
@@ -447,7 +445,6 @@ registryTool:
|
||||
clientId: YOUR_CLIENT_ID
|
||||
# OAuth client secret used by the tool.
|
||||
clientSecret: YOUR_CLIENT_SECRET
|
||||
username: toolusername
|
||||
|
||||
# Configuration options for checking SSL certificates.
|
||||
sslCertificateValidation:
|
||||
|
||||
@@ -385,6 +385,12 @@
|
||||
<url-pattern>/_dr/task/wipeOutCloudSql</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<!-- Action to wipeout Cloud Datastore data -->
|
||||
<servlet-mapping>
|
||||
<servlet-name>backend-servlet</servlet-name>
|
||||
<url-pattern>/_dr/task/wipeOutDatastore</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<!-- Security config -->
|
||||
<security-constraint>
|
||||
<web-resource-collection>
|
||||
|
||||
@@ -91,4 +91,13 @@
|
||||
<target>backend</target>
|
||||
</cron>
|
||||
|
||||
<cron>
|
||||
<url><![CDATA[/_dr/task/wipeOutDatastore]]></url>
|
||||
<description>
|
||||
This job runs an action that deletes all data in Cloud Datastore.
|
||||
</description>
|
||||
<schedule>every saturday 03:07</schedule>
|
||||
<target>backend</target>
|
||||
</cron>
|
||||
|
||||
</cronentries>
|
||||
|
||||
@@ -16,7 +16,6 @@ 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;
|
||||
@@ -26,24 +25,17 @@ 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 google.registry.util.Clock;
|
||||
import google.registry.util.ProxyHttpHeaders;
|
||||
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;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/**
|
||||
* Container and validation for TLS certificate and IP-allow-listing.
|
||||
@@ -54,10 +46,6 @@ import org.joda.time.DateTime;
|
||||
* <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
|
||||
* <dd>This field should contain the host and port of the connecting client. It is validated
|
||||
* during an EPP login command against an IP allow list that is transmitted out of band.
|
||||
@@ -66,30 +54,22 @@ import org.joda.time.DateTime;
|
||||
public class TlsCredentials implements TransportCredentials {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
private static final DateTime CERT_ENFORCEMENT_START_TIME =
|
||||
DateTime.parse("2021-03-01T16:00:00Z");
|
||||
|
||||
private final boolean requireSslCertificates;
|
||||
private final Optional<String> clientCertificateHash;
|
||||
private final Optional<String> clientCertificate;
|
||||
private final Optional<InetAddress> clientInetAddr;
|
||||
private final CertificateChecker certificateChecker;
|
||||
private final Clock clock;
|
||||
|
||||
@Inject
|
||||
public TlsCredentials(
|
||||
@Config("requireSslCertificates") boolean requireSslCertificates,
|
||||
@Header(ProxyHttpHeaders.CERTIFICATE_HASH) Optional<String> clientCertificateHash,
|
||||
@Header(ProxyHttpHeaders.FULL_CERTIFICATE) Optional<String> clientCertificate,
|
||||
@Header(ProxyHttpHeaders.IP_ADDRESS) Optional<String> clientAddress,
|
||||
CertificateChecker certificateChecker,
|
||||
Clock clock) {
|
||||
CertificateChecker certificateChecker) {
|
||||
this.requireSslCertificates = requireSslCertificates;
|
||||
this.clientCertificateHash = clientCertificateHash;
|
||||
this.clientCertificate = clientCertificate;
|
||||
this.clientInetAddr = clientAddress.map(TlsCredentials::parseInetAddress);
|
||||
this.certificateChecker = certificateChecker;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
static InetAddress parseInetAddress(String asciiAddr) {
|
||||
@@ -103,7 +83,7 @@ public class TlsCredentials implements TransportCredentials {
|
||||
@Override
|
||||
public void validate(Registrar registrar, String password) throws AuthenticationErrorException {
|
||||
validateIp(registrar);
|
||||
validateCertificate(registrar);
|
||||
validateCertificateHash(registrar);
|
||||
validatePassword(registrar, password);
|
||||
}
|
||||
|
||||
@@ -137,89 +117,8 @@ public class TlsCredentials implements TransportCredentials {
|
||||
throw new BadRegistrarIpAddressException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies client SSL certificate is permitted to issue commands as {@code registrar}.
|
||||
*
|
||||
* @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) {
|
||||
// TODO(Sarahbot@): Remove this if statement after March 1. After March 1, exception
|
||||
// should be thrown in all environments.
|
||||
// throw exception in unit tests and Sandbox
|
||||
if (RegistryEnvironment.get().equals(RegistryEnvironment.UNITTEST)
|
||||
|| RegistryEnvironment.get().equals(RegistryEnvironment.SANDBOX)
|
||||
|| clock.nowUtc().isAfter(CERT_ENFORCEMENT_START_TIME)) {
|
||||
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 {
|
||||
logger.atWarning().log(
|
||||
"Error validating certificate for %s, attempting to validate using certificate hash.",
|
||||
registrar.getClientId());
|
||||
// Check the certificate hash as a failover
|
||||
// TODO(sarahbot): Remove hash checks once certificate checks are working.
|
||||
void validateCertificateHash(Registrar registrar) throws AuthenticationErrorException {
|
||||
if (!registrar.getClientCertificateHash().isPresent()
|
||||
&& !registrar.getFailoverClientCertificateHash().isPresent()) {
|
||||
if (requireSslCertificates) {
|
||||
@@ -247,6 +146,20 @@ public class TlsCredentials implements TransportCredentials {
|
||||
registrar.getFailoverClientCertificateHash());
|
||||
throw new BadRegistrarCertificateException();
|
||||
}
|
||||
if (requireSslCertificates) {
|
||||
String passedCert =
|
||||
clientCertificateHash.equals(registrar.getClientCertificateHash())
|
||||
? registrar.getClientCertificate().get()
|
||||
: registrar.getFailoverClientCertificate().get();
|
||||
try {
|
||||
certificateChecker.validateCertificate(passedCert);
|
||||
} catch (InsecureCertificateException e) {
|
||||
logger.atWarning().log(
|
||||
"Registrar certificate used for %s does not meet certificate requirements: %s",
|
||||
registrar.getClientId(), e.getMessage());
|
||||
throw new CertificateContainsSecurityViolationsException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validatePassword(Registrar registrar, String password)
|
||||
@@ -256,26 +169,9 @@ 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();
|
||||
@@ -336,14 +232,6 @@ public class TlsCredentials implements TransportCredentials {
|
||||
return extractOptionalHeader(req, ProxyHttpHeaders.CERTIFICATE_HASH);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Header(ProxyHttpHeaders.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, ProxyHttpHeaders.FULL_CERTIFICATE);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Header(ProxyHttpHeaders.IP_ADDRESS)
|
||||
static Optional<String> provideIpAddress(HttpServletRequest req) {
|
||||
|
||||
@@ -208,13 +208,21 @@ public final class DomainDeleteFlow implements TransactionalFlow {
|
||||
// Enqueue the deletion poll message if the delete is asynchronous or if requested by a
|
||||
// superuser (i.e. the registrar didn't request this delete and thus should be notified even if
|
||||
// it is synchronous).
|
||||
if (!durationUntilDelete.equals(Duration.ZERO) || isSuperuser) {
|
||||
if (durationUntilDelete.isLongerThan(Duration.ZERO) || isSuperuser) {
|
||||
PollMessage.OneTime deletePollMessage =
|
||||
createDeletePollMessage(existingDomain, historyEntry, deletionTime);
|
||||
entitiesToSave.add(deletePollMessage);
|
||||
builder.setDeletePollMessage(deletePollMessage.createVKey());
|
||||
}
|
||||
|
||||
// Send a second poll message immediately if the domain is being deleted asynchronously by a
|
||||
// registrar other than the sponsoring registrar (which will necessarily be a superuser).
|
||||
if (durationUntilDelete.isLongerThan(Duration.ZERO)
|
||||
&& !clientId.equals(existingDomain.getPersistedCurrentSponsorClientId())) {
|
||||
entitiesToSave.add(
|
||||
createImmediateDeletePollMessage(existingDomain, historyEntry, now, deletionTime));
|
||||
}
|
||||
|
||||
// Cancel any grace periods that were still active, and set the expiration time accordingly.
|
||||
DateTime newExpirationTime = existingDomain.getRegistrationExpirationTime();
|
||||
for (GracePeriod gracePeriod : existingDomain.getGracePeriods()) {
|
||||
@@ -346,6 +354,19 @@ public final class DomainDeleteFlow implements TransactionalFlow {
|
||||
.build();
|
||||
}
|
||||
|
||||
private PollMessage.OneTime createImmediateDeletePollMessage(
|
||||
DomainBase existingDomain, HistoryEntry historyEntry, DateTime now, DateTime deletionTime) {
|
||||
return new PollMessage.OneTime.Builder()
|
||||
.setClientId(existingDomain.getPersistedCurrentSponsorClientId())
|
||||
.setEventTime(now)
|
||||
.setParent(historyEntry)
|
||||
.setMsg(
|
||||
String.format(
|
||||
"Domain %s was deleted by registry administrator with final deletion effective: %s",
|
||||
existingDomain.getDomainName(), deletionTime))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private ImmutableList<FeeTransformResponseExtension> getResponseExtensions(
|
||||
DomainBase existingDomain, DateTime now) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import static google.registry.flows.domain.DomainFlowUtils.handleFeeRequest;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.loadForeignKeyedDesignatedContacts;
|
||||
import static google.registry.model.EppResourceUtils.loadByForeignKey;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
@@ -100,9 +101,11 @@ public final class DomainInfoFlow implements Flow {
|
||||
verifyOptionalAuthInfo(authInfo, domain);
|
||||
flowCustomLogic.afterValidation(
|
||||
AfterValidationParameters.newBuilder().setDomain(domain).build());
|
||||
// Prefetch all referenced resources. Calling values() blocks until loading is done.
|
||||
tm().loadByKeys(domain.getNameservers());
|
||||
tm().loadByKeys(domain.getReferencedContacts());
|
||||
// In ofy, refetch all referenced resources.
|
||||
if (tm().isOfy()) {
|
||||
tm().loadByKeys(domain.getNameservers());
|
||||
tm().loadByKeys(domain.getReferencedContacts());
|
||||
}
|
||||
// Registrars can only see a few fields on unauthorized domains.
|
||||
// This is a policy decision that is left up to us by the rfcs.
|
||||
DomainInfoData.Builder infoBuilder =
|
||||
@@ -110,14 +113,16 @@ public final class DomainInfoFlow implements Flow {
|
||||
.setFullyQualifiedDomainName(domain.getDomainName())
|
||||
.setRepoId(domain.getRepoId())
|
||||
.setCurrentSponsorClientId(domain.getCurrentSponsorClientId())
|
||||
.setRegistrant(tm().loadByKey(domain.getRegistrant()).getContactId());
|
||||
.setRegistrant(
|
||||
transactIfJpaTm(() -> tm().loadByKey(domain.getRegistrant())).getContactId());
|
||||
// If authInfo is non-null, then the caller is authorized to see the full information since we
|
||||
// will have already verified the authInfo is valid.
|
||||
if (clientId.equals(domain.getCurrentSponsorClientId()) || authInfo.isPresent()) {
|
||||
HostsRequest hostsRequest = ((Info) resourceCommand).getHostsRequest();
|
||||
infoBuilder
|
||||
.setStatusValues(domain.getStatusValues())
|
||||
.setContacts(loadForeignKeyedDesignatedContacts(domain.getContacts()))
|
||||
.setContacts(
|
||||
transactIfJpaTm(() -> loadForeignKeyedDesignatedContacts(domain.getContacts())))
|
||||
.setNameservers(hostsRequest.requestDelegated() ? domain.loadNameserverHostNames() : null)
|
||||
.setSubordinateHosts(
|
||||
hostsRequest.requestSubordinate() ? domain.getSubordinateHosts() : null)
|
||||
|
||||
@@ -25,7 +25,6 @@ import static google.registry.flows.domain.DomainFlowUtils.updateAutorenewRecurr
|
||||
import static google.registry.flows.domain.DomainTransferUtils.createLosingTransferPollMessage;
|
||||
import static google.registry.flows.domain.DomainTransferUtils.createTransferResponse;
|
||||
import static google.registry.model.ResourceTransferUtils.denyPendingTransfer;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.model.reporting.DomainTransactionRecord.TransactionReportField.TRANSFER_SUCCESSFUL;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
@@ -39,7 +38,6 @@ import google.registry.flows.FlowModule.Superuser;
|
||||
import google.registry.flows.FlowModule.TargetId;
|
||||
import google.registry.flows.TransactionalFlow;
|
||||
import google.registry.flows.annotations.ReportingSpec;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.domain.metadata.MetadataExtension;
|
||||
import google.registry.model.eppcommon.AuthInfo;
|
||||
@@ -100,11 +98,11 @@ public final class DomainTransferCancelFlow implements TransactionalFlow {
|
||||
HistoryEntry historyEntry = buildHistoryEntry(existingDomain, registry, now);
|
||||
DomainBase newDomain =
|
||||
denyPendingTransfer(existingDomain, TransferStatus.CLIENT_CANCELLED, now, clientId);
|
||||
ofy().save().<ImmutableObject>entities(
|
||||
newDomain,
|
||||
historyEntry,
|
||||
createLosingTransferPollMessage(
|
||||
targetId, newDomain.getTransferData(), null, historyEntry));
|
||||
tm().putAll(
|
||||
newDomain,
|
||||
historyEntry,
|
||||
createLosingTransferPollMessage(
|
||||
targetId, newDomain.getTransferData(), null, historyEntry));
|
||||
// Reopen the autorenew event and poll message that we closed for the implicit transfer. This
|
||||
// may recreate the autorenew poll message if it was deleted when the transfer request was made.
|
||||
updateAutorenewRecurrenceEndTime(existingDomain, END_OF_TIME);
|
||||
|
||||
@@ -16,15 +16,13 @@ package google.registry.flows.poll;
|
||||
|
||||
import static google.registry.flows.FlowUtils.validateClientIsLoggedIn;
|
||||
import static google.registry.flows.poll.PollFlowUtils.ackPollMessage;
|
||||
import static google.registry.flows.poll.PollFlowUtils.getPollMessagesQuery;
|
||||
import static google.registry.flows.poll.PollFlowUtils.getPollMessageCount;
|
||||
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_NO_MESSAGES;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.model.poll.PollMessageExternalKeyConverter.makePollMessageExternalId;
|
||||
import static google.registry.model.poll.PollMessageExternalKeyConverter.parsePollMessageExternalId;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.util.DateTimeUtils.isBeforeOrAt;
|
||||
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.flows.EppException;
|
||||
import google.registry.flows.EppException.AuthorizationErrorException;
|
||||
import google.registry.flows.EppException.ObjectDoesNotExistException;
|
||||
@@ -39,6 +37,8 @@ import google.registry.model.poll.MessageQueueInfo;
|
||||
import google.registry.model.poll.PollMessage;
|
||||
import google.registry.model.poll.PollMessageExternalKeyConverter;
|
||||
import google.registry.model.poll.PollMessageExternalKeyConverter.PollMessageExternalKeyParseException;
|
||||
import google.registry.persistence.VKey;
|
||||
import java.util.Optional;
|
||||
import javax.inject.Inject;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
@@ -71,7 +71,7 @@ public class PollAckFlow implements TransactionalFlow {
|
||||
throw new MissingMessageIdException();
|
||||
}
|
||||
|
||||
Key<PollMessage> pollMessageKey;
|
||||
VKey<PollMessage> pollMessageKey;
|
||||
// Try parsing the messageId, and throw an exception if it's invalid.
|
||||
try {
|
||||
pollMessageKey = parsePollMessageExternalId(messageId);
|
||||
@@ -84,12 +84,13 @@ public class PollAckFlow implements TransactionalFlow {
|
||||
// Load the message to be acked. If a message is queued to be delivered in the future, we treat
|
||||
// it as if it doesn't exist yet. Same for if the message ID year isn't the same as the actual
|
||||
// poll message's event time (that means they're passing in an old already-acked ID).
|
||||
PollMessage pollMessage = ofy().load().key(pollMessageKey).now();
|
||||
if (pollMessage == null
|
||||
|| !isBeforeOrAt(pollMessage.getEventTime(), now)
|
||||
|| !makePollMessageExternalId(pollMessage).equals(messageId)) {
|
||||
Optional<PollMessage> maybePollMessage = tm().loadByKeyIfPresent(pollMessageKey);
|
||||
if (!maybePollMessage.isPresent()
|
||||
|| !isBeforeOrAt(maybePollMessage.get().getEventTime(), now)
|
||||
|| !makePollMessageExternalId(maybePollMessage.get()).equals(messageId)) {
|
||||
throw new MessageDoesNotExistException(messageId);
|
||||
}
|
||||
PollMessage pollMessage = maybePollMessage.get();
|
||||
|
||||
// Make sure this client is authorized to ack this message. It could be that the message is
|
||||
// supposed to go to a different registrar.
|
||||
@@ -106,8 +107,11 @@ public class PollAckFlow implements TransactionalFlow {
|
||||
// acked, then we return a special status code indicating that. Note that the query will
|
||||
// include the message being acked.
|
||||
|
||||
int messageCount = tm().doTransactionless(() -> getPollMessagesQuery(clientId, now).count());
|
||||
if (!includeAckedMessageInCount) {
|
||||
int messageCount = tm().doTransactionless(() -> getPollMessageCount(clientId, now));
|
||||
// Within the same transaction, Datastore will not reflect the updated count (potentially
|
||||
// reduced by one thanks to the acked poll message). SQL will, however, so we shouldn't reduce
|
||||
// the count in the SQL case.
|
||||
if (!includeAckedMessageInCount && tm().isOfy()) {
|
||||
messageCount--;
|
||||
}
|
||||
if (messageCount <= 0) {
|
||||
|
||||
@@ -16,25 +16,56 @@ package google.registry.flows.poll;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
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.isBeforeOrAt;
|
||||
|
||||
import com.googlecode.objectify.cmd.Query;
|
||||
import google.registry.model.poll.PollMessage;
|
||||
import java.util.Optional;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Static utility functions for poll flows. */
|
||||
public final class PollFlowUtils {
|
||||
|
||||
private PollFlowUtils() {}
|
||||
public static final String SQL_POLL_MESSAGE_QUERY =
|
||||
"FROM PollMessage WHERE clientId = :registrarId AND eventTime <= :now ORDER BY eventTime ASC";
|
||||
private static final String SQL_POLL_MESSAGE_COUNT_QUERY =
|
||||
"SELECT COUNT(*) FROM PollMessage WHERE clientId = :registrarId AND eventTime <= :now";
|
||||
|
||||
/** Returns a query for poll messages for the logged in registrar which are not in the future. */
|
||||
public static Query<PollMessage> getPollMessagesQuery(String clientId, DateTime now) {
|
||||
return ofy().load()
|
||||
.type(PollMessage.class)
|
||||
.filter("clientId", clientId)
|
||||
.filter("eventTime <=", now.toDate())
|
||||
.order("eventTime");
|
||||
/** Returns the number of poll messages for the given registrar that are not in the future. */
|
||||
public static int getPollMessageCount(String registrarId, DateTime now) {
|
||||
if (tm().isOfy()) {
|
||||
return datastorePollMessageQuery(registrarId, now).count();
|
||||
} else {
|
||||
return jpaTm()
|
||||
.transact(
|
||||
() ->
|
||||
jpaTm()
|
||||
.query(SQL_POLL_MESSAGE_COUNT_QUERY, Long.class)
|
||||
.setParameter("registrarId", registrarId)
|
||||
.setParameter("now", now)
|
||||
.getSingleResult()
|
||||
.intValue());
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the first (by event time) poll message not in the future for this registrar. */
|
||||
public static Optional<PollMessage> getFirstPollMessage(String registrarId, DateTime now) {
|
||||
if (tm().isOfy()) {
|
||||
return Optional.ofNullable(datastorePollMessageQuery(registrarId, now).first().now());
|
||||
} else {
|
||||
return jpaTm()
|
||||
.transact(
|
||||
() ->
|
||||
jpaTm()
|
||||
.query(SQL_POLL_MESSAGE_QUERY, PollMessage.class)
|
||||
.setParameter("registrarId", registrarId)
|
||||
.setParameter("now", now)
|
||||
.setMaxResults(1)
|
||||
.getResultStream()
|
||||
.findFirst());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,4 +105,16 @@ public final class PollFlowUtils {
|
||||
}
|
||||
return includeAckedMessageInCount;
|
||||
}
|
||||
|
||||
/** A Datastore query for poll messages from the given registrar that are not in the future. */
|
||||
public static Query<PollMessage> datastorePollMessageQuery(String registrarId, DateTime now) {
|
||||
return ofy()
|
||||
.load()
|
||||
.type(PollMessage.class)
|
||||
.filter("clientId", registrarId)
|
||||
.filter("eventTime <=", now.toDate())
|
||||
.order("eventTime");
|
||||
}
|
||||
|
||||
private PollFlowUtils() {}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
package google.registry.flows.poll;
|
||||
|
||||
import static google.registry.flows.FlowUtils.validateClientIsLoggedIn;
|
||||
import static google.registry.flows.poll.PollFlowUtils.getPollMessagesQuery;
|
||||
import static google.registry.flows.poll.PollFlowUtils.getFirstPollMessage;
|
||||
import static google.registry.flows.poll.PollFlowUtils.getPollMessageCount;
|
||||
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACK_MESSAGE;
|
||||
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_NO_MESSAGES;
|
||||
import static google.registry.model.poll.PollMessageExternalKeyConverter.makePollMessageExternalId;
|
||||
@@ -31,6 +32,7 @@ import google.registry.model.poll.MessageQueueInfo;
|
||||
import google.registry.model.poll.PollMessage;
|
||||
import google.registry.model.poll.PollMessageExternalKeyConverter;
|
||||
import google.registry.util.Clock;
|
||||
import java.util.Optional;
|
||||
import javax.inject.Inject;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
@@ -63,18 +65,20 @@ public class PollRequestFlow implements Flow {
|
||||
}
|
||||
// Return the oldest message from the queue.
|
||||
DateTime now = clock.nowUtc();
|
||||
PollMessage pollMessage = getPollMessagesQuery(clientId, now).first().now();
|
||||
if (pollMessage == null) {
|
||||
Optional<PollMessage> maybePollMessage = getFirstPollMessage(clientId, now);
|
||||
if (!maybePollMessage.isPresent()) {
|
||||
return responseBuilder.setResultFromCode(SUCCESS_WITH_NO_MESSAGES).build();
|
||||
}
|
||||
PollMessage pollMessage = maybePollMessage.get();
|
||||
return responseBuilder
|
||||
.setResultFromCode(SUCCESS_WITH_ACK_MESSAGE)
|
||||
.setMessageQueueInfo(new MessageQueueInfo.Builder()
|
||||
.setQueueDate(pollMessage.getEventTime())
|
||||
.setMsg(pollMessage.getMsg())
|
||||
.setQueueLength(getPollMessagesQuery(clientId, now).count())
|
||||
.setMessageId(makePollMessageExternalId(pollMessage))
|
||||
.build())
|
||||
.setMessageQueueInfo(
|
||||
new MessageQueueInfo.Builder()
|
||||
.setQueueDate(pollMessage.getEventTime())
|
||||
.setMsg(pollMessage.getMsg())
|
||||
.setQueueLength(getPollMessageCount(clientId, now))
|
||||
.setMessageId(makePollMessageExternalId(pollMessage))
|
||||
.build())
|
||||
.setMultipleResData(pollMessage.getResponseData())
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -39,8 +39,6 @@ public final class InMemoryKeyring implements Keyring {
|
||||
private final String marksdbLordnPassword;
|
||||
private final String marksdbSmdrlLoginAndPassword;
|
||||
private final String jsonCredential;
|
||||
private final String cloudSqlPassword;
|
||||
private final String toolsCloudSqlPassword;
|
||||
|
||||
public InMemoryKeyring(
|
||||
PGPKeyPair rdeStagingKey,
|
||||
@@ -83,8 +81,6 @@ public final class InMemoryKeyring implements Keyring {
|
||||
this.marksdbSmdrlLoginAndPassword =
|
||||
checkNotNull(marksdbSmdrlLoginAndPassword, "marksdbSmdrlLoginAndPassword");
|
||||
this.jsonCredential = checkNotNull(jsonCredential, "jsonCredential");
|
||||
this.cloudSqlPassword = checkNotNull(cloudSqlPassword, "cloudSqlPassword");
|
||||
this.toolsCloudSqlPassword = checkNotNull(toolsCloudSqlPassword, "toolsCloudSqlPassword");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -157,16 +153,6 @@ public final class InMemoryKeyring implements Keyring {
|
||||
return jsonCredential;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCloudSqlPassword() {
|
||||
return cloudSqlPassword;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToolsCloudSqlPassword() {
|
||||
return toolsCloudSqlPassword;
|
||||
}
|
||||
|
||||
/** Does nothing. */
|
||||
@Override
|
||||
public void close() {}
|
||||
|
||||
@@ -36,18 +36,6 @@ public final class KeyModule {
|
||||
String value();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Key("cloudSqlPassword")
|
||||
static String providesCloudSqlPassword(Keyring keyring) {
|
||||
return keyring.getCloudSqlPassword();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Key("toolsCloudSqlPassword")
|
||||
static String providesToolsCloudSqlPassword(Keyring keyring) {
|
||||
return keyring.getToolsCloudSqlPassword();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Key("brdaReceiverKey")
|
||||
static PGPPublicKey provideBrdaReceiverKey(Keyring keyring) {
|
||||
|
||||
@@ -28,12 +28,6 @@ import org.bouncycastle.openpgp.PGPPublicKey;
|
||||
@ThreadSafe
|
||||
public interface Keyring extends AutoCloseable {
|
||||
|
||||
/** Returns the password which is used by App Engine to connect to the Cloud SQL database. */
|
||||
String getCloudSqlPassword();
|
||||
|
||||
/** Returns the password which is used by nomulus tool to connect to the Cloud SQL database. */
|
||||
String getToolsCloudSqlPassword();
|
||||
|
||||
/**
|
||||
* Returns the key which should be used to sign RDE deposits being uploaded to a third-party.
|
||||
*
|
||||
|
||||
@@ -21,6 +21,10 @@ import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
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.keyring.api.KeySerializer;
|
||||
@@ -29,8 +33,11 @@ import google.registry.keyring.api.KeyringException;
|
||||
import google.registry.model.server.KmsSecret;
|
||||
import google.registry.model.server.KmsSecretRevision;
|
||||
import google.registry.model.server.KmsSecretRevisionSqlDao;
|
||||
import google.registry.privileges.secretmanager.KeyringSecretStore;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
import javax.inject.Inject;
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPKeyPair;
|
||||
@@ -46,6 +53,8 @@ import org.bouncycastle.openpgp.PGPPublicKey;
|
||||
*/
|
||||
public class KmsKeyring implements Keyring {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
/** Key labels for private key secrets. */
|
||||
enum PrivateKeyLabel {
|
||||
BRDA_SIGNING_PRIVATE,
|
||||
@@ -72,7 +81,6 @@ public class KmsKeyring implements Keyring {
|
||||
|
||||
/** Key labels for string secrets. */
|
||||
enum StringKeyLabel {
|
||||
CLOUD_SQL_PASSWORD_STRING,
|
||||
SAFE_BROWSING_API_KEY,
|
||||
ICANN_REPORTING_PASSWORD_STRING,
|
||||
JSON_CREDENTIAL_STRING,
|
||||
@@ -80,8 +88,7 @@ public class KmsKeyring implements Keyring {
|
||||
MARKSDB_LORDN_PASSWORD_STRING,
|
||||
MARKSDB_SMDRL_LOGIN_STRING,
|
||||
RDE_SSH_CLIENT_PRIVATE_STRING,
|
||||
RDE_SSH_CLIENT_PUBLIC_STRING,
|
||||
TOOLS_CLOUD_SQL_PASSWORD_STRING;
|
||||
RDE_SSH_CLIENT_PUBLIC_STRING;
|
||||
|
||||
String getLabel() {
|
||||
return UPPER_UNDERSCORE.to(LOWER_HYPHEN, name());
|
||||
@@ -89,20 +96,13 @@ public class KmsKeyring implements Keyring {
|
||||
}
|
||||
|
||||
private final KmsConnection kmsConnection;
|
||||
private final KeyringSecretStore secretStore;
|
||||
|
||||
@Inject
|
||||
KmsKeyring(@Config("defaultKmsConnection") KmsConnection kmsConnection) {
|
||||
KmsKeyring(
|
||||
@Config("defaultKmsConnection") KmsConnection kmsConnection, KeyringSecretStore secretStore) {
|
||||
this.kmsConnection = kmsConnection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCloudSqlPassword() {
|
||||
return getString(StringKeyLabel.CLOUD_SQL_PASSWORD_STRING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToolsCloudSqlPassword() {
|
||||
return getString(StringKeyLabel.TOOLS_CLOUD_SQL_PASSWORD_STRING);
|
||||
this.secretStore = secretStore;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -204,7 +204,7 @@ public class KmsKeyring implements Keyring {
|
||||
return getKeyPair(keyLabel).getPrivateKey();
|
||||
}
|
||||
|
||||
private byte[] getDecryptedData(String keyName) {
|
||||
private byte[] getDecryptedDataFromDatastore(String keyName) {
|
||||
String encryptedData;
|
||||
if (tm().isOfy()) {
|
||||
KmsSecret secret =
|
||||
@@ -225,4 +225,56 @@ public class KmsKeyring implements Keyring {
|
||||
String.format("CloudKMS decrypt operation failed for secret %s", keyName), e);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] getDataFromSecretStore(String keyName) {
|
||||
try {
|
||||
return secretStore.getSecret(keyName);
|
||||
} catch (Exception e) {
|
||||
return new byte[0];
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] getDecryptedData(String keyName) {
|
||||
byte[] dsData = getDecryptedDataFromDatastore(keyName);
|
||||
byte[] secretStoreData = getDataFromSecretStore(keyName);
|
||||
|
||||
if (Arrays.equals(dsData, secretStoreData)) {
|
||||
logger.atInfo().log("Values for %s in Datastore and Secret Manager match.", keyName);
|
||||
return secretStoreData;
|
||||
}
|
||||
logger.atWarning().log("Values for %s in Datastore and Secret Manager do not match.", keyName);
|
||||
return dsData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the tasks to migrate secrets from Datastore to Secret Manager.
|
||||
*
|
||||
* <p>The keys in the returned {@link ImmutableMap} are the names of the secrets that need
|
||||
* migration. The values in the map are {@link Runnable Runnables} that copy secret data from
|
||||
* Datastore to Secret Manager for their corresponding keys. Only secrets that are absent in
|
||||
* Secret Manager or have inconsistent values are included in the returned map.
|
||||
*/
|
||||
public ImmutableMap<String, Runnable> migrationPlan() {
|
||||
|
||||
ImmutableMap.Builder<String, Runnable> tasks = new ImmutableMap.Builder<>();
|
||||
|
||||
ImmutableList<String> labels =
|
||||
Streams.concat(
|
||||
Stream.of(PrivateKeyLabel.values()).map(PrivateKeyLabel::getLabel),
|
||||
Stream.of(PublicKeyLabel.values()).map(PublicKeyLabel::getLabel),
|
||||
Stream.of(StringKeyLabel.values()).map(StringKeyLabel::getLabel))
|
||||
.collect(ImmutableList.toImmutableList());
|
||||
|
||||
for (String keyName : labels) {
|
||||
byte[] dsData = getDecryptedDataFromDatastore(keyName);
|
||||
byte[] secretStoreData = getDataFromSecretStore(keyName);
|
||||
if (Arrays.equals(dsData, secretStoreData)) {
|
||||
logger.atInfo().log("%s is already up to date.\n", keyName);
|
||||
continue;
|
||||
}
|
||||
logger.atInfo().log("%s needs to be migrated.\n", keyName);
|
||||
tasks.put(keyName, () -> secretStore.createOrUpdateSecret(keyName, dsData));
|
||||
}
|
||||
return tasks.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import static google.registry.keyring.kms.KmsKeyring.PublicKeyLabel.BRDA_SIGNING
|
||||
import static google.registry.keyring.kms.KmsKeyring.PublicKeyLabel.RDE_RECEIVER_PUBLIC;
|
||||
import static google.registry.keyring.kms.KmsKeyring.PublicKeyLabel.RDE_SIGNING_PUBLIC;
|
||||
import static google.registry.keyring.kms.KmsKeyring.PublicKeyLabel.RDE_STAGING_PUBLIC;
|
||||
import static google.registry.keyring.kms.KmsKeyring.StringKeyLabel.CLOUD_SQL_PASSWORD_STRING;
|
||||
import static google.registry.keyring.kms.KmsKeyring.StringKeyLabel.ICANN_REPORTING_PASSWORD_STRING;
|
||||
import static google.registry.keyring.kms.KmsKeyring.StringKeyLabel.JSON_CREDENTIAL_STRING;
|
||||
import static google.registry.keyring.kms.KmsKeyring.StringKeyLabel.MARKSDB_DNL_LOGIN_STRING;
|
||||
@@ -33,7 +32,6 @@ import static google.registry.keyring.kms.KmsKeyring.StringKeyLabel.MARKSDB_SMDR
|
||||
import static google.registry.keyring.kms.KmsKeyring.StringKeyLabel.RDE_SSH_CLIENT_PRIVATE_STRING;
|
||||
import static google.registry.keyring.kms.KmsKeyring.StringKeyLabel.RDE_SSH_CLIENT_PUBLIC_STRING;
|
||||
import static google.registry.keyring.kms.KmsKeyring.StringKeyLabel.SAFE_BROWSING_API_KEY;
|
||||
import static google.registry.keyring.kms.KmsKeyring.StringKeyLabel.TOOLS_CLOUD_SQL_PASSWORD_STRING;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
|
||||
@@ -45,6 +43,7 @@ import google.registry.keyring.kms.KmsKeyring.PublicKeyLabel;
|
||||
import google.registry.keyring.kms.KmsKeyring.StringKeyLabel;
|
||||
import google.registry.model.server.KmsSecret;
|
||||
import google.registry.model.server.KmsSecretRevision;
|
||||
import google.registry.privileges.secretmanager.KeyringSecretStore;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -61,20 +60,19 @@ import org.bouncycastle.openpgp.PGPPublicKey;
|
||||
public final class KmsUpdater {
|
||||
|
||||
private final KmsConnection kmsConnection;
|
||||
private final KeyringSecretStore secretStore;
|
||||
private final HashMap<String, byte[]> secretValues;
|
||||
|
||||
@Inject
|
||||
public KmsUpdater(@Config("defaultKmsConnection") KmsConnection kmsConnection) {
|
||||
public KmsUpdater(
|
||||
@Config("defaultKmsConnection") KmsConnection kmsConnection, KeyringSecretStore secretStore) {
|
||||
this.kmsConnection = kmsConnection;
|
||||
this.secretStore = secretStore;
|
||||
|
||||
// Use LinkedHashMap to preserve insertion order on update() to simplify testing and debugging
|
||||
this.secretValues = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
public KmsUpdater setCloudSqlPassword(String password) {
|
||||
return setString(password, CLOUD_SQL_PASSWORD_STRING);
|
||||
}
|
||||
|
||||
public KmsUpdater setRdeSigningKey(PGPKeyPair keyPair) throws IOException, PGPException {
|
||||
return setKeyPair(keyPair, RDE_SIGNING_PRIVATE, RDE_SIGNING_PUBLIC);
|
||||
}
|
||||
@@ -107,10 +105,6 @@ public final class KmsUpdater {
|
||||
return setString(apiKey, SAFE_BROWSING_API_KEY);
|
||||
}
|
||||
|
||||
public KmsUpdater setToolsCloudSqlPassword(String password) {
|
||||
return setString(password, TOOLS_CLOUD_SQL_PASSWORD_STRING);
|
||||
}
|
||||
|
||||
public KmsUpdater setIcannReportingPassword(String password) {
|
||||
return setString(password, ICANN_REPORTING_PASSWORD_STRING);
|
||||
}
|
||||
@@ -142,6 +136,19 @@ public final class KmsUpdater {
|
||||
checkState(!secretValues.isEmpty(), "At least one Keyring value must be persisted");
|
||||
|
||||
persistEncryptedValues(encryptValues(secretValues));
|
||||
|
||||
// Errors when writing to secret store can be thrown to the top, since writes are always
|
||||
// executed by a human user using the UpdateKmsKeyringCommand.
|
||||
try {
|
||||
secretValues
|
||||
.entrySet()
|
||||
.forEach(e -> secretStore.createOrUpdateSecret(e.getKey(), e.getValue()));
|
||||
} catch (RuntimeException e) {
|
||||
throw new RuntimeException(
|
||||
"Failed to persist secrets to Secret Manager. "
|
||||
+ "Please check the status of Secret Manager and re-run the command.",
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -194,30 +194,6 @@ public final class EppResourceUtils {
|
||||
return ForeignKeyIndex.load(clazz, uniqueIds, now).keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads resources that match some filter and that have {@link EppResource#deletionTime} that is
|
||||
* not before "now".
|
||||
*
|
||||
* <p>This is an eventually consistent query.
|
||||
*
|
||||
* @param clazz the resource type to load
|
||||
* @param now the logical time of the check
|
||||
* @param filterDefinition the filter to apply when loading resources
|
||||
* @param filterValue the acceptable value for the filter
|
||||
*/
|
||||
public static <T extends EppResource> Iterable<T> queryNotDeleted(
|
||||
Class<T> clazz, DateTime now, String filterDefinition, Object filterValue) {
|
||||
return ofy()
|
||||
.load()
|
||||
.type(clazz)
|
||||
.filter(filterDefinition, filterValue)
|
||||
.filter("deletionTime >", now.toDate())
|
||||
.list()
|
||||
.stream()
|
||||
.map(EppResourceUtils.transformAtTime(now))
|
||||
.collect(toImmutableSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Function that transforms an EppResource to the given DateTime, suitable for use with
|
||||
* Iterables.transform() over a collection of EppResources.
|
||||
|
||||
@@ -17,6 +17,7 @@ package google.registry.model;
|
||||
import static com.google.common.collect.Iterables.transform;
|
||||
import static com.google.common.collect.Maps.transformValues;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static java.lang.annotation.ElementType.FIELD;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
import static java.util.stream.Collectors.toCollection;
|
||||
@@ -187,7 +188,10 @@ public abstract class ImmutableObject implements Cloneable {
|
||||
/** Helper function to recursively hydrate an ImmutableObject. */
|
||||
private static Object hydrate(Object value) {
|
||||
if (value instanceof Key) {
|
||||
return hydrate(ofy().load().key((Key<?>) value).now());
|
||||
if (tm().isOfy()) {
|
||||
return hydrate(ofy().load().key((Key<?>) value).now());
|
||||
}
|
||||
return value;
|
||||
} else if (value instanceof Map) {
|
||||
return transformValues((Map<?, ?>) value, ImmutableObject::hydrate);
|
||||
} else if (value instanceof Collection) {
|
||||
|
||||
@@ -18,7 +18,6 @@ import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.model.registry.Registry.TldState.GENERAL_AVAILABILITY;
|
||||
import static google.registry.model.registry.Registry.TldState.START_DATE_SUNRISE;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
@@ -41,10 +40,10 @@ import google.registry.model.registry.Registry;
|
||||
import google.registry.model.registry.Registry.TldState;
|
||||
import google.registry.model.registry.label.PremiumList;
|
||||
import google.registry.model.registry.label.PremiumListDualDao;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.util.CidrAddressBlock;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
import org.joda.money.CurrencyUnit;
|
||||
@@ -225,15 +224,14 @@ public final class OteAccountBuilder {
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists all the OT&E entities to datastore.
|
||||
* Persists all the OT&E entities to the database.
|
||||
*
|
||||
* @return map from the new clientIds created to the new TLDs they have access to. Can be used to
|
||||
* go over all the newly created Registrars / Registries / RegistrarContacts if any
|
||||
* post-creation work is needed.
|
||||
*/
|
||||
public ImmutableMap<String, String> buildAndPersist() {
|
||||
// save all the entitiesl in a single transaction
|
||||
tm().transact(this::saveAllEntities);
|
||||
saveAllEntities();
|
||||
return clientIdToTld;
|
||||
}
|
||||
|
||||
@@ -246,30 +244,38 @@ public final class OteAccountBuilder {
|
||||
|
||||
/** Saves all the OT&E entities we created. */
|
||||
private void saveAllEntities() {
|
||||
tm().assertInTransaction();
|
||||
|
||||
// use ImmutableObject instead of Registry so that the Key generation doesn't break
|
||||
ImmutableList<ImmutableObject> registries = ImmutableList.of(sunriseTld, gaTld, eapTld);
|
||||
ImmutableList<Registry> registries = ImmutableList.of(sunriseTld, gaTld, eapTld);
|
||||
ImmutableList<RegistrarContact> contacts = contactsBuilder.build();
|
||||
|
||||
if (!replaceExisting) {
|
||||
ImmutableList<Key<ImmutableObject>> keys =
|
||||
Streams.concat(registries.stream(), registrars.stream(), contacts.stream())
|
||||
.map(Key::create)
|
||||
.collect(toImmutableList());
|
||||
Set<Key<ImmutableObject>> existingKeys = ofy().load().keys(keys).keySet();
|
||||
checkState(
|
||||
existingKeys.isEmpty(),
|
||||
"Found existing object(s) conflicting with OT&E objects: %s",
|
||||
existingKeys);
|
||||
}
|
||||
// Save the Registries (TLDs) first
|
||||
ofy().save().entities(registries).now();
|
||||
// Now we can set the allowedTlds for the registrars
|
||||
registrars = registrars.stream().map(this::addAllowedTld).collect(toImmutableList());
|
||||
// and we can save the registrars and contacts!
|
||||
ofy().save().entities(registrars);
|
||||
ofy().save().entities(contacts);
|
||||
tm().transact(
|
||||
() -> {
|
||||
if (!replaceExisting) {
|
||||
ImmutableList<VKey<? extends ImmutableObject>> keys =
|
||||
Streams.concat(
|
||||
registries.stream()
|
||||
.map(registry -> Registry.createVKey(registry.getTldStr())),
|
||||
registrars.stream().map(Registrar::createVKey),
|
||||
contacts.stream().map(RegistrarContact::createVKey))
|
||||
.collect(toImmutableList());
|
||||
ImmutableMap<VKey<? extends ImmutableObject>, ImmutableObject> existingObjects =
|
||||
tm().loadByKeysIfPresent(keys);
|
||||
checkState(
|
||||
existingObjects.isEmpty(),
|
||||
"Found existing object(s) conflicting with OT&E objects: %s",
|
||||
existingObjects.keySet());
|
||||
}
|
||||
// Save the Registries (TLDs) first
|
||||
tm().putAll(registries);
|
||||
});
|
||||
// Now we can set the allowedTlds for the registrars in a new transaction
|
||||
tm().transact(
|
||||
() -> {
|
||||
registrars = registrars.stream().map(this::addAllowedTld).collect(toImmutableList());
|
||||
// and we can save the registrars and contacts!
|
||||
tm().putAll(registrars);
|
||||
tm().putAll(contacts);
|
||||
});
|
||||
}
|
||||
|
||||
private Registrar addAllowedTld(Registrar registrar) {
|
||||
|
||||
@@ -17,7 +17,6 @@ package google.registry.model;
|
||||
import static com.google.common.base.Predicates.equalTo;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static google.registry.model.eppcommon.EppXmlTransformer.unmarshal;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.util.CollectionUtils.isNullOrEmpty;
|
||||
import static google.registry.util.DomainNameUtils.ACE_PREFIX;
|
||||
|
||||
@@ -28,7 +27,6 @@ import com.google.common.collect.ImmutableCollection;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Multiset;
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.cmd.Query;
|
||||
import google.registry.model.domain.DomainCommand;
|
||||
import google.registry.model.domain.fee.FeeCreateCommandExtension;
|
||||
import google.registry.model.domain.launch.LaunchCreateExtension;
|
||||
@@ -39,6 +37,7 @@ import google.registry.model.eppinput.EppInput.ResourceCommandWrapper;
|
||||
import google.registry.model.host.HostCommand;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.model.reporting.HistoryEntry.Type;
|
||||
import google.registry.model.reporting.HistoryEntryDao;
|
||||
import google.registry.xml.XmlException;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
@@ -196,16 +195,10 @@ public class OteStats {
|
||||
* <p>Stops when it notices that all tests have passed.
|
||||
*/
|
||||
private OteStats recordRegistrarHistory(String registrarName) {
|
||||
ImmutableCollection<String> clientIds =
|
||||
ImmutableCollection<String> registrarIds =
|
||||
OteAccountBuilder.createClientIdToTldMap(registrarName).keySet();
|
||||
|
||||
Query<HistoryEntry> query =
|
||||
ofy()
|
||||
.load()
|
||||
.type(HistoryEntry.class)
|
||||
.filter("clientId in", clientIds)
|
||||
.order("modificationTime");
|
||||
for (HistoryEntry historyEntry : query) {
|
||||
for (HistoryEntry historyEntry : HistoryEntryDao.loadHistoryObjectsByRegistrars(registrarIds)) {
|
||||
try {
|
||||
record(historyEntry);
|
||||
} catch (XmlException e) {
|
||||
|
||||
@@ -17,7 +17,6 @@ package google.registry.model;
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
@@ -107,14 +106,14 @@ public final class ResourceTransferUtils {
|
||||
/** Update the relevant {@link ForeignKeyIndex} to cache the new deletion time. */
|
||||
public static <R extends EppResource> void updateForeignKeyIndexDeletionTime(R resource) {
|
||||
if (resource instanceof ForeignKeyedEppResource) {
|
||||
ofy().save().entity(ForeignKeyIndex.create(resource, resource.getDeletionTime()));
|
||||
tm().insert(ForeignKeyIndex.create(resource, resource.getDeletionTime()));
|
||||
}
|
||||
}
|
||||
|
||||
/** If there is a transfer out, delete the server-approve entities and enqueue a poll message. */
|
||||
public static <R extends EppResource & ResourceWithTransferData>
|
||||
void handlePendingTransferOnDelete(
|
||||
R resource, R newResource, DateTime now, HistoryEntry historyEntry) {
|
||||
R resource, R newResource, DateTime now, HistoryEntry historyEntry) {
|
||||
if (resource.getStatusValues().contains(StatusValue.PENDING_TRANSFER)) {
|
||||
TransferData oldTransferData = resource.getTransferData();
|
||||
tm().delete(oldTransferData.getServerApproveEntities());
|
||||
|
||||
@@ -18,7 +18,8 @@ import static com.google.common.base.MoreObjects.firstNonNull;
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
|
||||
import static google.registry.util.CollectionUtils.forceEmptyToNull;
|
||||
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
@@ -772,10 +773,10 @@ public abstract class BillingEvent extends ImmutableObject
|
||||
Modification instance = getInstance();
|
||||
checkNotNull(instance.reason);
|
||||
checkNotNull(instance.eventRef);
|
||||
BillingEvent.OneTime billingEvent = ofy().load().key(instance.eventRef).now();
|
||||
checkArgument(Objects.equals(
|
||||
instance.cost.getCurrencyUnit(),
|
||||
billingEvent.cost.getCurrencyUnit()),
|
||||
BillingEvent.OneTime billingEvent =
|
||||
transactIfJpaTm(() -> tm().loadByKey(VKey.from(instance.eventRef)));
|
||||
checkArgument(
|
||||
Objects.equals(instance.cost.getCurrencyUnit(), billingEvent.cost.getCurrencyUnit()),
|
||||
"Referenced billing event is in a different currency");
|
||||
return super.build();
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ public abstract class CrossTldSingleton extends ImmutableObject {
|
||||
|
||||
public static final long SINGLETON_ID = 1; // There is always exactly one of these.
|
||||
|
||||
@Id @Transient long id = SINGLETON_ID;
|
||||
@Id @javax.persistence.Id long id = SINGLETON_ID;
|
||||
|
||||
@Transient @Parent Key<EntityGroupRoot> parent = getCrossTldKey();
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import google.registry.persistence.VKey;
|
||||
import google.registry.schema.replay.DatastoreAndSqlEntity;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.EnumType;
|
||||
import javax.persistence.Enumerated;
|
||||
@@ -280,8 +281,8 @@ public class Cursor extends ImmutableObject implements DatastoreAndSqlEntity {
|
||||
/**
|
||||
* Returns the current time for a given cursor, or {@code START_OF_TIME} if the cursor is null.
|
||||
*/
|
||||
public static DateTime getCursorTimeOrStartOfTime(Cursor cursor) {
|
||||
return cursor != null ? cursor.getCursorTime() : START_OF_TIME;
|
||||
public static DateTime getCursorTimeOrStartOfTime(Optional<Cursor> cursor) {
|
||||
return cursor.map(Cursor::getCursorTime).orElse(START_OF_TIME);
|
||||
}
|
||||
|
||||
public DateTime getCursorTime() {
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
|
||||
package google.registry.model.contact;
|
||||
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
||||
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.annotation.EntitySubclass;
|
||||
import google.registry.model.ImmutableObject;
|
||||
@@ -114,6 +116,12 @@ public class ContactHistory extends HistoryEntry implements SqlEntity {
|
||||
return Optional.of(asHistoryEntry());
|
||||
}
|
||||
|
||||
// Used to fill out the contactBase field during asynchronous replay
|
||||
public static void beforeSqlSave(ContactHistory contactHistory) {
|
||||
contactHistory.contactBase =
|
||||
jpaTm().loadByKey(VKey.createSql(ContactResource.class, contactHistory.getContactRepoId()));
|
||||
}
|
||||
|
||||
/** Class to represent the composite primary key of {@link ContactHistory} entity. */
|
||||
public static class ContactHistoryId extends ImmutableObject implements Serializable {
|
||||
|
||||
|
||||
@@ -23,8 +23,9 @@ import static com.google.common.collect.Sets.difference;
|
||||
import static com.google.common.collect.Sets.intersection;
|
||||
import static google.registry.model.EppResourceUtils.projectResourceOntoBuilderAtTime;
|
||||
import static google.registry.model.EppResourceUtils.setAutomaticTransferSuccessProperties;
|
||||
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.util.CollectionUtils.forceEmptyToNull;
|
||||
import static google.registry.util.CollectionUtils.nullToEmpty;
|
||||
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
|
||||
@@ -337,7 +338,7 @@ public class DomainContent extends EppResource
|
||||
|
||||
@PostLoad
|
||||
@SuppressWarnings("UnusedMethod")
|
||||
private final void postLoad() {
|
||||
private void postLoad() {
|
||||
// Reconstitute the contact list.
|
||||
ImmutableSet.Builder<DesignatedContact> contactsBuilder = new ImmutableSet.Builder<>();
|
||||
|
||||
@@ -670,13 +671,11 @@ public class DomainContent extends EppResource
|
||||
|
||||
/** Loads and returns the fully qualified host names of all linked nameservers. */
|
||||
public ImmutableSortedSet<String> loadNameserverHostNames() {
|
||||
return ofy()
|
||||
.load()
|
||||
.keys(getNameservers().stream().map(VKey::getOfyKey).collect(toImmutableSet()))
|
||||
.values()
|
||||
.stream()
|
||||
.map(HostResource::getHostName)
|
||||
.collect(toImmutableSortedSet(Ordering.natural()));
|
||||
return transactIfJpaTm(
|
||||
() ->
|
||||
tm().loadByKeys(getNameservers()).values().stream()
|
||||
.map(HostResource::getHostName)
|
||||
.collect(toImmutableSortedSet(Ordering.natural())));
|
||||
}
|
||||
|
||||
/** A key to the registrant who registered this domain. */
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package google.registry.model.domain;
|
||||
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
||||
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
@@ -263,6 +264,12 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
|
||||
return Optional.of(asHistoryEntry());
|
||||
}
|
||||
|
||||
// Used to fill out the domainContent field during asynchronous replay
|
||||
public static void beforeSqlSave(DomainHistory domainHistory) {
|
||||
domainHistory.domainContent =
|
||||
jpaTm().loadByKey(VKey.createSql(DomainBase.class, domainHistory.getDomainRepoId()));
|
||||
}
|
||||
|
||||
/** Class to represent the composite primary key of {@link DomainHistory} entity. */
|
||||
public static class DomainHistoryId extends ImmutableObject implements Serializable {
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
|
||||
package google.registry.model.host;
|
||||
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
||||
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.annotation.EntitySubclass;
|
||||
import google.registry.model.ImmutableObject;
|
||||
@@ -115,6 +117,12 @@ public class HostHistory extends HistoryEntry implements SqlEntity {
|
||||
return Optional.of(asHistoryEntry());
|
||||
}
|
||||
|
||||
// Used to fill out the hostBase field during asynchronous replay
|
||||
public static void beforeSqlSave(HostHistory hostHistory) {
|
||||
hostHistory.hostBase =
|
||||
jpaTm().loadByKey(VKey.createSql(HostResource.class, hostHistory.getHostRepoId()));
|
||||
}
|
||||
|
||||
/** Class to represent the composite primary key of {@link HostHistory} entity. */
|
||||
public static class HostHistoryId extends ImmutableObject implements Serializable {
|
||||
|
||||
|
||||
@@ -48,8 +48,10 @@ import google.registry.model.contact.ContactResource;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.host.HostResource;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.persistence.transaction.CriteriaQueryBuilder;
|
||||
import google.registry.schema.replay.DatastoreOnlyEntity;
|
||||
import google.registry.util.NonFinalForTesting;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -193,8 +195,8 @@ public abstract class ForeignKeyIndex<E extends EppResource> extends BackupGroup
|
||||
* has been soft deleted.
|
||||
*/
|
||||
public static <E extends EppResource> ImmutableMap<String, ForeignKeyIndex<E>> load(
|
||||
Class<E> clazz, Iterable<String> foreignKeys, final DateTime now) {
|
||||
return loadIndexesFromStore(clazz, foreignKeys).entrySet().stream()
|
||||
Class<E> clazz, Collection<String> foreignKeys, final DateTime now) {
|
||||
return loadIndexesFromStore(clazz, foreignKeys, true).entrySet().stream()
|
||||
.filter(e -> now.isBefore(e.getValue().getDeletionTime()))
|
||||
.collect(entriesToImmutableMap());
|
||||
}
|
||||
@@ -204,30 +206,36 @@ public abstract class ForeignKeyIndex<E extends EppResource> extends BackupGroup
|
||||
* keys, regardless of whether or not they have been soft-deleted.
|
||||
*
|
||||
* <p>Used by both the cached (w/o deletion check) and the non-cached (with deletion check) calls.
|
||||
*
|
||||
* <p>Note that in the cached case, we wish to run this outside of any transaction because we may
|
||||
* be loading many entities, going over the Datastore limit on the number of enrolled entity
|
||||
* groups per transaction (25). If we require consistency, however, we must use a transaction.
|
||||
*
|
||||
* @param inTransaction whether or not to use an Objectify transaction
|
||||
*/
|
||||
private static <E extends EppResource>
|
||||
ImmutableMap<String, ForeignKeyIndex<E>> loadIndexesFromStore(
|
||||
Class<E> clazz, Iterable<String> foreignKeys) {
|
||||
Class<E> clazz, Collection<String> foreignKeys, boolean inTransaction) {
|
||||
if (tm().isOfy()) {
|
||||
Class<ForeignKeyIndex<E>> fkiClass = mapToFkiClass(clazz);
|
||||
return ImmutableMap.copyOf(
|
||||
tm().doTransactionless(() -> ofy().load().type(mapToFkiClass(clazz)).ids(foreignKeys)));
|
||||
inTransaction
|
||||
? ofy().load().type(fkiClass).ids(foreignKeys)
|
||||
: tm().doTransactionless(() -> ofy().load().type(fkiClass).ids(foreignKeys)));
|
||||
} else {
|
||||
String property = RESOURCE_CLASS_TO_FKI_PROPERTY.get(clazz);
|
||||
ImmutableList<ForeignKeyIndex<E>> indexes =
|
||||
tm().transact(
|
||||
() -> {
|
||||
String entityName =
|
||||
jpaTm().getEntityManager().getMetamodel().entity(clazz).getName();
|
||||
return jpaTm()
|
||||
.query(
|
||||
String.format(
|
||||
"FROM %s WHERE %s IN :propertyValue", entityName, property),
|
||||
clazz)
|
||||
.setParameter("propertyValue", foreignKeys)
|
||||
.getResultStream()
|
||||
.map(e -> ForeignKeyIndex.create(e, e.getDeletionTime()))
|
||||
.collect(toImmutableList());
|
||||
});
|
||||
() ->
|
||||
jpaTm()
|
||||
.getEntityManager()
|
||||
.createQuery(
|
||||
CriteriaQueryBuilder.create(clazz)
|
||||
.whereFieldIsIn(property, foreignKeys)
|
||||
.build())
|
||||
.getResultStream()
|
||||
.map(e -> ForeignKeyIndex.create(e, e.getDeletionTime()))
|
||||
.collect(toImmutableList()));
|
||||
// We need to find and return the entities with the maximum deletionTime for each foreign key.
|
||||
return Multimaps.index(indexes, ForeignKeyIndex::getForeignKey).asMap().entrySet().stream()
|
||||
.map(
|
||||
@@ -250,7 +258,8 @@ public abstract class ForeignKeyIndex<E extends EppResource> extends BackupGroup
|
||||
return Optional.ofNullable(
|
||||
loadIndexesFromStore(
|
||||
RESOURCE_CLASS_TO_FKI_CLASS.inverse().get(key.getKind()),
|
||||
ImmutableSet.of(foreignKey))
|
||||
ImmutableSet.of(foreignKey),
|
||||
false)
|
||||
.get(foreignKey));
|
||||
}
|
||||
|
||||
@@ -266,7 +275,7 @@ public abstract class ForeignKeyIndex<E extends EppResource> extends BackupGroup
|
||||
Streams.stream(keys).map(v -> v.getSqlKey().toString()).collect(toImmutableSet());
|
||||
ImmutableSet<VKey<ForeignKeyIndex<?>>> typedKeys = ImmutableSet.copyOf(keys);
|
||||
ImmutableMap<String, ? extends ForeignKeyIndex<? extends EppResource>> existingFkis =
|
||||
loadIndexesFromStore(resourceClass, foreignKeys);
|
||||
loadIndexesFromStore(resourceClass, foreignKeys, false);
|
||||
// ofy() omits keys that don't have values in Datastore, so re-add them in
|
||||
// here with Optional.empty() values.
|
||||
return Maps.asMap(
|
||||
@@ -318,7 +327,7 @@ public abstract class ForeignKeyIndex<E extends EppResource> extends BackupGroup
|
||||
* reasons, and are OK with the trade-offs in loss of transactional consistency.
|
||||
*/
|
||||
public static <E extends EppResource> ImmutableMap<String, ForeignKeyIndex<E>> loadCached(
|
||||
Class<E> clazz, Iterable<String> foreignKeys, final DateTime now) {
|
||||
Class<E> clazz, Collection<String> foreignKeys, final DateTime now) {
|
||||
if (!RegistryConfig.isEppResourceCachingEnabled()) {
|
||||
return tm().doTransactionless(() -> load(clazz, foreignKeys, now));
|
||||
}
|
||||
|
||||
@@ -37,12 +37,17 @@ import google.registry.model.domain.DomainHistory;
|
||||
import google.registry.model.host.HostHistory;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.persistence.transaction.QueryComposer;
|
||||
import google.registry.persistence.transaction.TransactionManager;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.persistence.NoResultException;
|
||||
import javax.persistence.NonUniqueResultException;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Datastore implementation of {@link TransactionManager}. */
|
||||
@@ -302,6 +307,11 @@ public class DatastoreTransactionManager implements TransactionManager {
|
||||
syncIfTransactionless(getOfy().deleteWithoutBackup().entity(entity));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> QueryComposer<T> createQueryComposer(Class<T> entity) {
|
||||
return new DatastoreQueryComposerImpl(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearSessionCache() {
|
||||
getOfy().clearSessionCache();
|
||||
@@ -363,4 +373,50 @@ public class DatastoreTransactionManager implements TransactionManager {
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
private static class DatastoreQueryComposerImpl<T> extends QueryComposer<T> {
|
||||
DatastoreQueryComposerImpl(Class<T> entityClass) {
|
||||
super(entityClass);
|
||||
}
|
||||
|
||||
Query<T> buildQuery() {
|
||||
Query<T> result = ofy().load().type(entityClass);
|
||||
for (WhereClause pred : predicates) {
|
||||
result = result.filter(pred.fieldName + pred.comparator.getDatastoreString(), pred.value);
|
||||
}
|
||||
|
||||
if (orderBy != null) {
|
||||
result = result.order(orderBy);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<T> first() {
|
||||
return Optional.ofNullable(buildQuery().first().now());
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getSingleResult() {
|
||||
List<T> results = buildQuery().limit(2).list();
|
||||
if (results.size() == 0) {
|
||||
// The exception text here is the same as what we get for JPA queries.
|
||||
throw new NoResultException("No entity found for query");
|
||||
} else if (results.size() > 1) {
|
||||
throw new NonUniqueResultException("More than one result found for getSingleResult query");
|
||||
}
|
||||
return results.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<T> stream() {
|
||||
return Streams.stream(buildQuery());
|
||||
}
|
||||
|
||||
@Override
|
||||
public long count() {
|
||||
return buildQuery().count();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,10 +41,11 @@ public class EntityWritePriorities {
|
||||
*/
|
||||
static final ImmutableMap<String, Integer> CLASS_PRIORITIES =
|
||||
ImmutableMap.of(
|
||||
"ContactResource", -15,
|
||||
"HistoryEntry", -10,
|
||||
"AllocationToken", -9,
|
||||
"DomainBase", 10);
|
||||
"ContactResource", 8,
|
||||
"HostResource", 9,
|
||||
"DomainBase", 10,
|
||||
"HistoryEntry", 20);
|
||||
|
||||
// The beginning of the range of priority numbers reserved for delete. This must be greater than
|
||||
// any of the values in CLASS_PRIORITIES by enough overhead to accommodate any negative values in
|
||||
|
||||
@@ -106,7 +106,7 @@ public abstract class PollMessage extends ImmutableObject
|
||||
@Column(name = "poll_message_id")
|
||||
Long id;
|
||||
|
||||
@Parent @DoNotHydrate @Transient Key<HistoryEntry> parent;
|
||||
@Parent @DoNotHydrate @Transient Key<? extends HistoryEntry> parent;
|
||||
|
||||
/** The registrar that this poll message will be delivered to. */
|
||||
@Index
|
||||
@@ -134,7 +134,7 @@ public abstract class PollMessage extends ImmutableObject
|
||||
|
||||
@Ignore Long hostHistoryRevisionId;
|
||||
|
||||
public Key<HistoryEntry> getParentKey() {
|
||||
public Key<? extends HistoryEntry> getParentKey() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@ public abstract class PollMessage extends ImmutableObject
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B setParentKey(Key<HistoryEntry> parentKey) {
|
||||
public B setParentKey(Key<? extends HistoryEntry> parentKey) {
|
||||
getInstance().parent = parentKey;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
+14
-12
@@ -24,6 +24,7 @@ import google.registry.model.contact.ContactResource;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.host.HostResource;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.persistence.VKey;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -78,14 +79,14 @@ public class PollMessageExternalKeyConverter {
|
||||
/**
|
||||
* Returns an Objectify Key to a PollMessage corresponding with the external ID.
|
||||
*
|
||||
* <p>Note that the year field that is included at the end of the poll message isn't actually
|
||||
* used for anything; it exists solely to create unique externally visible IDs for autorenews. We
|
||||
* thus ignore it (for now) for backwards compatibility reasons, so that registrars can still ACK
|
||||
* <p>Note that the year field that is included at the end of the poll message isn't actually used
|
||||
* for anything; it exists solely to create unique externally visible IDs for autorenews. We thus
|
||||
* ignore it (for now) for backwards compatibility reasons, so that registrars can still ACK
|
||||
* existing poll message IDs they may have lying around.
|
||||
*
|
||||
* @throws PollMessageExternalKeyParseException if the external key has an invalid format.
|
||||
*/
|
||||
public static Key<PollMessage> parsePollMessageExternalId(String externalKey) {
|
||||
public static VKey<PollMessage> parsePollMessageExternalId(String externalKey) {
|
||||
List<String> idComponents = Splitter.on('-').splitToList(externalKey);
|
||||
if (idComponents.size() != 6) {
|
||||
throw new PollMessageExternalKeyParseException();
|
||||
@@ -96,16 +97,17 @@ public class PollMessageExternalKeyConverter {
|
||||
if (resourceClazz == null) {
|
||||
throw new PollMessageExternalKeyParseException();
|
||||
}
|
||||
return Key.create(
|
||||
return VKey.from(
|
||||
Key.create(
|
||||
Key.create(
|
||||
null,
|
||||
resourceClazz,
|
||||
String.format("%s-%s", idComponents.get(1), idComponents.get(2))),
|
||||
HistoryEntry.class,
|
||||
Long.parseLong(idComponents.get(3))),
|
||||
PollMessage.class,
|
||||
Long.parseLong(idComponents.get(4)));
|
||||
Key.create(
|
||||
null,
|
||||
resourceClazz,
|
||||
String.format("%s-%s", idComponents.get(1), idComponents.get(2))),
|
||||
HistoryEntry.class,
|
||||
Long.parseLong(idComponents.get(3))),
|
||||
PollMessage.class,
|
||||
Long.parseLong(idComponents.get(4))));
|
||||
// Note that idComponents.get(5) is entirely ignored; we never use the year field internally.
|
||||
} catch (NumberFormatException e) {
|
||||
throw new PollMessageExternalKeyParseException();
|
||||
|
||||
@@ -103,6 +103,15 @@ public final class RdeRevision extends BackupGroupRoot implements NonReplicatedE
|
||||
return revisionOptional.map(rdeRevision -> rdeRevision.revision + 1).orElse(0);
|
||||
}
|
||||
|
||||
/** Returns the latest revision of the report already generated for the given triplet. */
|
||||
public static Optional<Integer> getCurrentRevision(String tld, DateTime date, RdeMode mode) {
|
||||
int nextRevision = getNextRevision(tld, date, mode);
|
||||
if (nextRevision == 0) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(nextRevision - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the revision ID for a given triplet.
|
||||
*
|
||||
|
||||
@@ -16,14 +16,16 @@ package google.registry.model.registry.label;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static google.registry.model.DatabaseMigrationUtils.suppressExceptionUnlessInTest;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.google.common.collect.Streams;
|
||||
import google.registry.model.DatabaseMigrationUtils;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule.TransitionId;
|
||||
import google.registry.model.registry.Registry;
|
||||
import google.registry.model.registry.label.PremiumList.PremiumListEntry;
|
||||
import google.registry.schema.tld.PremiumListSqlDao;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.joda.money.BigMoney;
|
||||
import org.joda.money.CurrencyUnit;
|
||||
import org.joda.money.Money;
|
||||
|
||||
@@ -46,8 +48,7 @@ public class PremiumListDualDao {
|
||||
* or absent if no such list exists.
|
||||
*/
|
||||
public static Optional<PremiumList> getLatestRevision(String premiumListName) {
|
||||
// TODO(gbrodman): Use Sarah's DB scheduler instead of this isOfy check
|
||||
if (tm().isOfy()) {
|
||||
if (DatabaseMigrationUtils.isDatastore(TransitionId.DOMAIN_LABEL_LISTS)) {
|
||||
return PremiumListDatastoreDao.getLatestRevision(premiumListName);
|
||||
} else {
|
||||
return PremiumListSqlDao.getLatestRevision(premiumListName);
|
||||
@@ -68,16 +69,14 @@ public class PremiumListDualDao {
|
||||
}
|
||||
String premiumListName = registry.getPremiumList().getName();
|
||||
Optional<Money> primaryResult;
|
||||
// TODO(gbrodman): Use Sarah's DB scheduler instead of this isOfy check
|
||||
if (tm().isOfy()) {
|
||||
if (DatabaseMigrationUtils.isDatastore(TransitionId.DOMAIN_LABEL_LISTS)) {
|
||||
primaryResult =
|
||||
PremiumListDatastoreDao.getPremiumPrice(premiumListName, label, registry.getTldStr());
|
||||
} else {
|
||||
primaryResult = PremiumListSqlDao.getPremiumPrice(premiumListName, label);
|
||||
}
|
||||
// Also load the value from the secondary DB, compare the two results, and log if different.
|
||||
// TODO(gbrodman): Use Sarah's DB scheduler instead of this isOfy check
|
||||
if (tm().isOfy()) {
|
||||
if (DatabaseMigrationUtils.isDatastore(TransitionId.DOMAIN_LABEL_LISTS)) {
|
||||
suppressExceptionUnlessInTest(
|
||||
() -> {
|
||||
Optional<Money> secondaryResult =
|
||||
@@ -120,8 +119,7 @@ public class PremiumListDualDao {
|
||||
*/
|
||||
public static PremiumList save(String name, List<String> inputData) {
|
||||
PremiumList result;
|
||||
// TODO(gbrodman): Use Sarah's DB scheduler instead of this isOfy check
|
||||
if (tm().isOfy()) {
|
||||
if (DatabaseMigrationUtils.isDatastore(TransitionId.DOMAIN_LABEL_LISTS)) {
|
||||
result = PremiumListDatastoreDao.save(name, inputData);
|
||||
suppressExceptionUnlessInTest(
|
||||
() -> PremiumListSqlDao.save(name, inputData), "Error when saving premium list to SQL.");
|
||||
@@ -141,8 +139,7 @@ public class PremiumListDualDao {
|
||||
* secondary database.
|
||||
*/
|
||||
public static void delete(PremiumList premiumList) {
|
||||
// TODO(gbrodman): Use Sarah's DB scheduler instead of this isOfy check
|
||||
if (tm().isOfy()) {
|
||||
if (DatabaseMigrationUtils.isDatastore(TransitionId.DOMAIN_LABEL_LISTS)) {
|
||||
PremiumListDatastoreDao.delete(premiumList);
|
||||
suppressExceptionUnlessInTest(
|
||||
() -> PremiumListSqlDao.delete(premiumList),
|
||||
@@ -159,8 +156,7 @@ public class PremiumListDualDao {
|
||||
public static boolean exists(String premiumListName) {
|
||||
// It may seem like overkill, but loading the list has ways been the way we check existence and
|
||||
// given that we usually load the list around the time we check existence, we'll hit the cache
|
||||
// TODO(gbrodman): Use Sarah's DB scheduler instead of this isOfy check
|
||||
if (tm().isOfy()) {
|
||||
if (DatabaseMigrationUtils.isDatastore(TransitionId.DOMAIN_LABEL_LISTS)) {
|
||||
return PremiumListDatastoreDao.getLatestRevision(premiumListName).isPresent();
|
||||
} else {
|
||||
return PremiumListSqlDao.getLatestRevision(premiumListName).isPresent();
|
||||
@@ -179,8 +175,7 @@ public class PremiumListDualDao {
|
||||
() ->
|
||||
new IllegalArgumentException(
|
||||
String.format("No premium list with name %s.", premiumListName)));
|
||||
// TODO(gbrodman): Use Sarah's DB scheduler instead of this isOfy check
|
||||
if (tm().isOfy()) {
|
||||
if (DatabaseMigrationUtils.isDatastore(TransitionId.DOMAIN_LABEL_LISTS)) {
|
||||
return PremiumListDatastoreDao.loadPremiumListEntriesUncached(premiumList);
|
||||
} else {
|
||||
CurrencyUnit currencyUnit = premiumList.getCurrency();
|
||||
@@ -188,7 +183,7 @@ public class PremiumListDualDao {
|
||||
.map(
|
||||
premiumEntry ->
|
||||
new PremiumListEntry.Builder()
|
||||
.setPrice(Money.of(currencyUnit, premiumEntry.getPrice()))
|
||||
.setPrice(BigMoney.of(currencyUnit, premiumEntry.getPrice()).toMoney())
|
||||
.setLabel(premiumEntry.getDomainLabel())
|
||||
.build())
|
||||
.collect(toImmutableList());
|
||||
|
||||
@@ -16,6 +16,7 @@ package google.registry.model.registry.label;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Strings.isNullOrEmpty;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static google.registry.config.RegistryConfig.getDomainLabelListCacheDuration;
|
||||
import static google.registry.model.registry.label.ReservationType.FULLY_BLOCKED;
|
||||
@@ -122,6 +123,12 @@ public final class ReservedList
|
||||
return new ReservedListEntry.Builder(clone(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(
|
||||
"%s,%s%s", label, reservationType, isNullOrEmpty(comment) ? "" : " # " + comment);
|
||||
}
|
||||
|
||||
/** A builder for constructing {@link ReservedListEntry} objects, since they are immutable. */
|
||||
private static class Builder
|
||||
extends DomainLabelEntry.Builder<ReservedListEntry, ReservedListEntry.Builder> {
|
||||
|
||||
@@ -21,7 +21,9 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
|
||||
import com.google.common.collect.ImmutableCollection;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Streams;
|
||||
import google.registry.model.EppResource;
|
||||
import google.registry.model.contact.ContactHistory;
|
||||
import google.registry.model.contact.ContactResource;
|
||||
@@ -30,7 +32,11 @@ import google.registry.model.domain.DomainHistory;
|
||||
import google.registry.model.host.HostHistory;
|
||||
import google.registry.model.host.HostResource;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.persistence.transaction.CriteriaQueryBuilder;
|
||||
import java.util.Comparator;
|
||||
import java.util.stream.Stream;
|
||||
import javax.persistence.criteria.CriteriaBuilder;
|
||||
import javax.persistence.criteria.CriteriaQuery;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/**
|
||||
@@ -85,21 +91,56 @@ public class HistoryEntryDao {
|
||||
}
|
||||
}
|
||||
|
||||
/** Loads all history objects from all time from the given registrars. */
|
||||
public static Iterable<? extends HistoryEntry> loadHistoryObjectsByRegistrars(
|
||||
ImmutableCollection<String> registrarIds) {
|
||||
if (tm().isOfy()) {
|
||||
return ofy()
|
||||
.load()
|
||||
.type(HistoryEntry.class)
|
||||
.filter("clientId in", registrarIds)
|
||||
.order("modificationTime");
|
||||
} else {
|
||||
return jpaTm()
|
||||
.transact(
|
||||
() ->
|
||||
Streams.concat(
|
||||
loadHistoryObjectFromSqlByRegistrars(ContactHistory.class, registrarIds),
|
||||
loadHistoryObjectFromSqlByRegistrars(DomainHistory.class, registrarIds),
|
||||
loadHistoryObjectFromSqlByRegistrars(HostHistory.class, registrarIds))
|
||||
.sorted(Comparator.comparing(HistoryEntry::getModificationTime))
|
||||
.collect(toImmutableList()));
|
||||
}
|
||||
}
|
||||
|
||||
private static Stream<? extends HistoryEntry> loadHistoryObjectFromSqlByRegistrars(
|
||||
Class<? extends HistoryEntry> historyClass, ImmutableCollection<String> registrarIds) {
|
||||
return jpaTm()
|
||||
.getEntityManager()
|
||||
.createQuery(
|
||||
CriteriaQueryBuilder.create(historyClass)
|
||||
.whereFieldIsIn("clientId", registrarIds)
|
||||
.build())
|
||||
.getResultStream();
|
||||
}
|
||||
|
||||
private static Iterable<? extends HistoryEntry> loadHistoryObjectsForResourceFromSql(
|
||||
VKey<? extends EppResource> parentKey, DateTime afterTime, DateTime beforeTime) {
|
||||
// The class we're searching from is based on which parent type (e.g. Domain) we have
|
||||
Class<? extends HistoryEntry> historyClass = getHistoryClassFromParent(parentKey.getKind());
|
||||
// The field representing repo ID unfortunately varies by history class
|
||||
String repoIdFieldName = getRepoIdFieldNameFromHistoryClass(historyClass);
|
||||
String tableName = jpaTm().getEntityManager().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);
|
||||
CriteriaBuilder criteriaBuilder = jpaTm().getEntityManager().getCriteriaBuilder();
|
||||
CriteriaQuery<? extends HistoryEntry> criteriaQuery =
|
||||
CriteriaQueryBuilder.create(historyClass)
|
||||
.where("modificationTime", criteriaBuilder::greaterThanOrEqualTo, afterTime)
|
||||
.where("modificationTime", criteriaBuilder::lessThanOrEqualTo, beforeTime)
|
||||
.where(repoIdFieldName, criteriaBuilder::equal, parentKey.getSqlKey().toString())
|
||||
.build();
|
||||
|
||||
return jpaTm()
|
||||
.query(queryString, historyClass)
|
||||
.setParameter("afterTime", afterTime)
|
||||
.setParameter("beforeTime", beforeTime)
|
||||
.setParameter("parentKey", parentKey.getSqlKey().toString())
|
||||
.getEntityManager()
|
||||
.createQuery(criteriaQuery)
|
||||
.getResultStream()
|
||||
.sorted(Comparator.comparing(HistoryEntry::getModificationTime))
|
||||
.collect(toImmutableList());
|
||||
@@ -127,15 +168,14 @@ public class HistoryEntryDao {
|
||||
|
||||
private static Iterable<? extends HistoryEntry> loadAllHistoryObjectsFromSql(
|
||||
Class<? extends HistoryEntry> historyClass, DateTime afterTime, DateTime beforeTime) {
|
||||
CriteriaBuilder criteriaBuilder = jpaTm().getEntityManager().getCriteriaBuilder();
|
||||
return jpaTm()
|
||||
.query(
|
||||
String.format(
|
||||
"SELECT entry FROM %s entry WHERE entry.modificationTime >= :afterTime AND "
|
||||
+ "entry.modificationTime <= :beforeTime",
|
||||
jpaTm().getEntityManager().getMetamodel().entity(historyClass).getName()),
|
||||
historyClass)
|
||||
.setParameter("afterTime", afterTime)
|
||||
.setParameter("beforeTime", beforeTime)
|
||||
.getEntityManager()
|
||||
.createQuery(
|
||||
CriteriaQueryBuilder.create(historyClass)
|
||||
.where("modificationTime", criteriaBuilder::greaterThanOrEqualTo, afterTime)
|
||||
.where("modificationTime", criteriaBuilder::lessThanOrEqualTo, beforeTime)
|
||||
.build())
|
||||
.getResultList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,19 +15,20 @@
|
||||
package google.registry.model.server;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
|
||||
import static google.registry.util.DateTimeUtils.isAtOrAfter;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.annotation.Entity;
|
||||
import com.googlecode.objectify.annotation.Id;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.annotations.NotBackedUp;
|
||||
import google.registry.model.annotations.NotBackedUp.Reason;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.schema.replay.DatastoreOnlyEntity;
|
||||
import google.registry.util.RequestStatusChecker;
|
||||
import google.registry.util.RequestStatusCheckerImpl;
|
||||
@@ -190,12 +191,17 @@ public class Lock extends ImmutableObject implements DatastoreOnlyEntity, Serial
|
||||
// access to resources like GCS that can't be transactionally rolled back. Therefore, the lock
|
||||
// must be definitively acquired before it is used, even when called inside another transaction.
|
||||
AcquireResult acquireResult =
|
||||
tm().transactNew(
|
||||
ofyTm()
|
||||
.transactNew(
|
||||
() -> {
|
||||
DateTime now = tm().getTransactionTime();
|
||||
DateTime now = ofyTm().getTransactionTime();
|
||||
|
||||
// Checking if an unexpired lock still exists - if so, the lock can't be acquired.
|
||||
Lock lock = ofy().load().type(Lock.class).id(lockId).now();
|
||||
Lock lock =
|
||||
ofyTm()
|
||||
.loadByKeyIfPresent(
|
||||
VKey.createOfy(Lock.class, Key.create(Lock.class, lockId)))
|
||||
.orElse(null);
|
||||
if (lock != null) {
|
||||
logger.atInfo().log(
|
||||
"Loaded existing lock: %s for request: %s", lock.lockId, lock.requestLogId);
|
||||
@@ -218,7 +224,7 @@ public class Lock extends ImmutableObject implements DatastoreOnlyEntity, Serial
|
||||
// Locks are not parented under an EntityGroupRoot (so as to avoid write
|
||||
// contention) and
|
||||
// don't need to be backed up.
|
||||
ofy().saveWithoutBackup().entity(newLock);
|
||||
ofyTm().putWithoutBackup(newLock);
|
||||
|
||||
return AcquireResult.create(now, lock, newLock, lockState);
|
||||
});
|
||||
@@ -231,21 +237,26 @@ public class Lock extends ImmutableObject implements DatastoreOnlyEntity, Serial
|
||||
/** Release the lock. */
|
||||
public void release() {
|
||||
// Just use the default clock because we aren't actually doing anything that will use the clock.
|
||||
tm().transact(
|
||||
ofyTm()
|
||||
.transact(
|
||||
() -> {
|
||||
// To release a lock, check that no one else has already obtained it and if not
|
||||
// delete it. If the lock in Datastore was different then this lock is gone already;
|
||||
// this can happen if release() is called around the expiration time and the lock
|
||||
// expires underneath us.
|
||||
Lock loadedLock = ofy().load().type(Lock.class).id(lockId).now();
|
||||
Lock loadedLock =
|
||||
ofyTm()
|
||||
.loadByKeyIfPresent(
|
||||
VKey.createOfy(Lock.class, Key.create(Lock.class, lockId)))
|
||||
.orElse(null);
|
||||
if (Lock.this.equals(loadedLock)) {
|
||||
// Use noBackupOfy() so that we don't create a commit log entry for deleting the
|
||||
// lock.
|
||||
logger.atInfo().log("Deleting lock: %s", lockId);
|
||||
ofy().deleteWithoutBackup().entity(Lock.this);
|
||||
ofyTm().deleteWithoutBackup(Lock.this);
|
||||
|
||||
lockMetrics.recordRelease(
|
||||
resourceName, tld, new Duration(acquiredTime, tm().getTransactionTime()));
|
||||
resourceName, tld, new Duration(acquiredTime, ofyTm().getTransactionTime()));
|
||||
} else {
|
||||
logger.atSevere().log(
|
||||
"The lock we acquired was transferred to someone else before we"
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
package google.registry.model.server;
|
||||
|
||||
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
@@ -35,10 +33,10 @@ import google.registry.model.common.CrossTldSingleton;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.schema.replay.NonReplicatedEntity;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.PostLoad;
|
||||
import javax.persistence.Transient;
|
||||
|
||||
@@ -67,22 +65,28 @@ public class ServerSecret extends CrossTldSingleton implements NonReplicatedEnti
|
||||
});
|
||||
|
||||
private static ServerSecret retrieveAndSaveSecret() {
|
||||
VKey<ServerSecret> key =
|
||||
VKey<ServerSecret> vkey =
|
||||
VKey.create(
|
||||
ServerSecret.class,
|
||||
SINGLETON_ID,
|
||||
Key.create(getCrossTldKey(), ServerSecret.class, SINGLETON_ID));
|
||||
if (tm().isOfy()) {
|
||||
// Attempt a quick load if we're in ofy first to short-circuit sans transaction
|
||||
Optional<ServerSecret> secretWithoutTransaction = tm().loadByKeyIfPresent(vkey);
|
||||
if (secretWithoutTransaction.isPresent()) {
|
||||
return secretWithoutTransaction.get();
|
||||
}
|
||||
}
|
||||
return tm().transact(
|
||||
() -> {
|
||||
// transactionally create a new ServerSecret (once per app setup) if necessary.
|
||||
// return the ofy() result during Datastore-primary phase
|
||||
ServerSecret secret =
|
||||
ofyTm().loadByKeyIfPresent(key).orElseGet(() -> create(UUID.randomUUID()));
|
||||
// During a dual-write period, write it to both Datastore and SQL
|
||||
// even if we didn't have to retrieve it from the DB
|
||||
ofyTm().transact(() -> ofyTm().putWithoutBackup(secret));
|
||||
jpaTm().transact(() -> jpaTm().putWithoutBackup(secret));
|
||||
return secret;
|
||||
// Make sure we're in a transaction and attempt to load any existing secret, then
|
||||
// create it if it's absent.
|
||||
Optional<ServerSecret> secret = tm().loadByKeyIfPresent(vkey);
|
||||
if (!secret.isPresent()) {
|
||||
secret = Optional.of(create(UUID.randomUUID()));
|
||||
tm().insertWithoutBackup(secret.get());
|
||||
}
|
||||
return secret.get();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -102,7 +106,6 @@ public class ServerSecret extends CrossTldSingleton implements NonReplicatedEnti
|
||||
@Transient long leastSignificant;
|
||||
|
||||
/** The UUID value itself. */
|
||||
@Id
|
||||
@Column(columnDefinition = "uuid")
|
||||
@Ignore
|
||||
UUID secret;
|
||||
|
||||
@@ -19,7 +19,6 @@ import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.collect.Iterables.isEmpty;
|
||||
import static google.registry.model.DatabaseMigrationUtils.suppressExceptionUnlessInTest;
|
||||
import static google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabase.DATASTORE;
|
||||
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
|
||||
import static google.registry.model.ofy.ObjectifyService.allocateId;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
@@ -35,9 +34,6 @@ import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.MapDifference;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.model.DatabaseMigrationUtils;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabase;
|
||||
import google.registry.model.common.DatabaseTransitionSchedule.TransitionId;
|
||||
import google.registry.util.CollectionUtils;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -50,42 +46,24 @@ public class SignedMarkRevocationListDao {
|
||||
/**
|
||||
* Loads the {@link SignedMarkRevocationList}.
|
||||
*
|
||||
* <p>Loads the list from the specified primary database, and attempts to load from the secondary
|
||||
* database. If the load the secondary database fails, or the list from the secondary database
|
||||
* does not match the list from the primary database, the error will be logged but no exception
|
||||
* will be thrown.
|
||||
* <p>Loads the list from Cloud SQL, and attempts to load from Datastore. If the load from
|
||||
* Datastore fails, or the list from Datastore does not match the list from Cloud SQL, the error
|
||||
* will be logged but no exception will be thrown.
|
||||
*/
|
||||
static SignedMarkRevocationList load() {
|
||||
PrimaryDatabase primaryDatabase =
|
||||
tm().transactNew(
|
||||
() ->
|
||||
DatabaseMigrationUtils.getPrimaryDatabase(
|
||||
TransitionId.SIGNED_MARK_REVOCATION_LIST));
|
||||
Optional<SignedMarkRevocationList> primaryList =
|
||||
primaryDatabase.equals(DATASTORE) ? loadFromDatastore() : loadFromCloudSql();
|
||||
Optional<SignedMarkRevocationList> primaryList = loadFromCloudSql();
|
||||
if (!primaryList.isPresent()) {
|
||||
throw new IllegalStateException(
|
||||
String.format(
|
||||
"SignedMarkRevocationList not found in the primary database (%s).",
|
||||
primaryDatabase.name()));
|
||||
return SignedMarkRevocationList.create(START_OF_TIME, ImmutableMap.of());
|
||||
}
|
||||
suppressExceptionUnlessInTest(
|
||||
() -> loadAndCompare(primaryDatabase, primaryList.get()),
|
||||
String.format(
|
||||
"Error loading and comparing the SignedMarkRevocationList from the secondary database"
|
||||
+ " (%s).",
|
||||
primaryDatabase.equals(DATASTORE) ? "Cloud SQL" : "Datastore"));
|
||||
() -> loadAndCompare(primaryList.get()),
|
||||
"Error loading and comparing the SignedMarkRevocationList from Datastore");
|
||||
return primaryList.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the list from the secondary database and compares it to the list from the primary
|
||||
* database.
|
||||
*/
|
||||
private static void loadAndCompare(
|
||||
PrimaryDatabase primaryDatabase, SignedMarkRevocationList primaryList) {
|
||||
Optional<SignedMarkRevocationList> secondaryList =
|
||||
primaryDatabase.equals(DATASTORE) ? loadFromCloudSql() : loadFromDatastore();
|
||||
/** Loads the list from Datastore and compares it to the list from Cloud SQL. */
|
||||
private static void loadAndCompare(SignedMarkRevocationList primaryList) {
|
||||
Optional<SignedMarkRevocationList> secondaryList = loadFromDatastore();
|
||||
if (secondaryList.isPresent() && !isNullOrEmpty(secondaryList.get().revokes)) {
|
||||
MapDifference<String, DateTime> diff =
|
||||
Maps.difference(primaryList.revokes, secondaryList.get().revokes);
|
||||
@@ -93,11 +71,9 @@ public class SignedMarkRevocationListDao {
|
||||
if (diff.entriesDiffering().size() > 10) {
|
||||
String message =
|
||||
String.format(
|
||||
"Unequal SignedMarkRevocationList detected, %s list with revision id"
|
||||
+ " %d has %d different records than the current primary database list.",
|
||||
primaryDatabase.equals(DATASTORE) ? "Cloud SQL" : "Datastore",
|
||||
secondaryList.get().revisionId,
|
||||
diff.entriesDiffering().size());
|
||||
"Unequal SignedMarkRevocationList detected, Datastore list with revision id"
|
||||
+ " %d has %d different records than the current Cloud SQL list.",
|
||||
secondaryList.get().revisionId, diff.entriesDiffering().size());
|
||||
throw new IllegalStateException(message);
|
||||
} else {
|
||||
StringBuilder diffMessage =
|
||||
@@ -107,21 +83,15 @@ public class SignedMarkRevocationListDao {
|
||||
(label, valueDiff) ->
|
||||
diffMessage.append(
|
||||
String.format(
|
||||
"SMD %s has key %s in %s and key %s in secondary database.\n",
|
||||
label,
|
||||
valueDiff.leftValue(),
|
||||
primaryDatabase.name(),
|
||||
valueDiff.rightValue())));
|
||||
"SMD %s has key %s in Cloud SQL and key %s in Datastore.\n",
|
||||
label, valueDiff.leftValue(), valueDiff.rightValue())));
|
||||
throw new IllegalStateException(diffMessage.toString());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (primaryList.size() != 0) {
|
||||
throw new IllegalStateException(
|
||||
String.format(
|
||||
"SignedMarkRevocationList in %s is empty while it is not empty in the primary"
|
||||
+ " database.",
|
||||
primaryDatabase.equals(DATASTORE) ? "Cloud SQL" : "Datastore"));
|
||||
"SignedMarkRevocationList in Datastore is empty while it is not empty in Cloud SQL.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,29 +142,16 @@ public class SignedMarkRevocationListDao {
|
||||
/**
|
||||
* Save the given {@link SignedMarkRevocationList}
|
||||
*
|
||||
* <p>Saves the list to the specified primary database, and attempts to save to the secondary
|
||||
* database. If the save to the secondary database fails, the error will be logged but no
|
||||
* exception will be thrown.
|
||||
* <p>Saves the list to Cloud SQL, and attempts to save to Datastore. If the save to Datastore
|
||||
* fails, the error will be logged but no exception will be thrown.
|
||||
*/
|
||||
static void save(SignedMarkRevocationList signedMarkRevocationList) {
|
||||
PrimaryDatabase primaryDatabase =
|
||||
tm().transactNew(
|
||||
() ->
|
||||
DatabaseMigrationUtils.getPrimaryDatabase(
|
||||
TransitionId.SIGNED_MARK_REVOCATION_LIST));
|
||||
if (primaryDatabase.equals(DATASTORE)) {
|
||||
saveToDatastore(signedMarkRevocationList.revokes, signedMarkRevocationList.creationTime);
|
||||
suppressExceptionUnlessInTest(
|
||||
() -> SignedMarkRevocationListDao.saveToCloudSql(signedMarkRevocationList),
|
||||
"Error inserting signed mark revocations into secondary database (Cloud SQL).");
|
||||
} else {
|
||||
SignedMarkRevocationListDao.saveToCloudSql(signedMarkRevocationList);
|
||||
suppressExceptionUnlessInTest(
|
||||
() ->
|
||||
saveToDatastore(
|
||||
signedMarkRevocationList.revokes, signedMarkRevocationList.creationTime),
|
||||
"Error inserting signed mark revocations into secondary database (Datastore).");
|
||||
}
|
||||
}
|
||||
|
||||
private static void saveToCloudSql(SignedMarkRevocationList signedMarkRevocationList) {
|
||||
|
||||
@@ -25,15 +25,11 @@ import com.googlecode.objectify.annotation.Entity;
|
||||
import google.registry.model.annotations.NotBackedUp;
|
||||
import google.registry.model.annotations.NotBackedUp.Reason;
|
||||
import google.registry.model.common.CrossTldSingleton;
|
||||
import google.registry.model.tmch.TmchCrl.TmchCrlId;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.schema.replay.NonReplicatedEntity;
|
||||
import java.io.Serializable;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.IdClass;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Datastore singleton for ICANN's TMCH CA certificate revocation list (CRL). */
|
||||
@@ -41,22 +37,26 @@ import org.joda.time.DateTime;
|
||||
@javax.persistence.Entity
|
||||
@Immutable
|
||||
@NotBackedUp(reason = Reason.EXTERNALLY_SOURCED)
|
||||
@IdClass(TmchCrlId.class)
|
||||
public final class TmchCrl extends CrossTldSingleton implements NonReplicatedEntity {
|
||||
|
||||
@Id String crl;
|
||||
@Column(name = "certificateRevocations", nullable = false)
|
||||
String crl;
|
||||
|
||||
@Id DateTime updated;
|
||||
@Column(name = "updateTimestamp", nullable = false)
|
||||
DateTime updated;
|
||||
|
||||
@Id String url;
|
||||
@Column(nullable = false)
|
||||
String url;
|
||||
|
||||
/** Returns the singleton instance of this entity, without memoization. */
|
||||
public static Optional<TmchCrl> get() {
|
||||
VKey<TmchCrl> key =
|
||||
VKey.create(
|
||||
TmchCrl.class, SINGLETON_ID, Key.create(getCrossTldKey(), TmchCrl.class, SINGLETON_ID));
|
||||
// return the ofy() result during Datastore-primary phase
|
||||
return ofyTm().transact(() -> ofyTm().loadByKeyIfPresent(key));
|
||||
return tm().transact(
|
||||
() ->
|
||||
tm().loadByKeyIfPresent(
|
||||
VKey.create(
|
||||
TmchCrl.class,
|
||||
SINGLETON_ID,
|
||||
Key.create(getCrossTldKey(), TmchCrl.class, SINGLETON_ID))));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,13 +75,7 @@ public final class TmchCrl extends CrossTldSingleton implements NonReplicatedEnt
|
||||
tmchCrl.crl = checkNotNull(crl, "crl");
|
||||
tmchCrl.url = checkNotNull(url, "url");
|
||||
ofyTm().transactNew(() -> ofyTm().putWithoutBackup(tmchCrl));
|
||||
jpaTm()
|
||||
.transactNew(
|
||||
() -> {
|
||||
// Delete the old one and insert the new one
|
||||
jpaTm().query("DELETE FROM TmchCrl").executeUpdate();
|
||||
jpaTm().putWithoutBackup(tmchCrl);
|
||||
});
|
||||
jpaTm().transactNew(() -> jpaTm().putWithoutBackup(tmchCrl));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -99,26 +93,4 @@ public final class TmchCrl extends CrossTldSingleton implements NonReplicatedEnt
|
||||
public final DateTime getUpdated() {
|
||||
return updated;
|
||||
}
|
||||
|
||||
static class TmchCrlId implements Serializable {
|
||||
|
||||
@Column(name = "certificateRevocations")
|
||||
String crl;
|
||||
|
||||
@Column(name = "updateTimestamp")
|
||||
DateTime updated;
|
||||
|
||||
String url;
|
||||
|
||||
/** Hibernate requires this default constructor. */
|
||||
private TmchCrlId() {}
|
||||
|
||||
static TmchCrlId create(String crl, DateTime updated, String url) {
|
||||
TmchCrlId result = new TmchCrlId();
|
||||
result.crl = crl;
|
||||
result.updated = updated;
|
||||
result.url = url;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import google.registry.batch.RelockDomainAction;
|
||||
import google.registry.batch.ResaveAllEppResourcesAction;
|
||||
import google.registry.batch.ResaveEntityAction;
|
||||
import google.registry.batch.WipeOutCloudSqlAction;
|
||||
import google.registry.batch.WipeoutDatastoreAction;
|
||||
import google.registry.cron.CommitLogFanoutAction;
|
||||
import google.registry.cron.CronModule;
|
||||
import google.registry.cron.TldFanoutAction;
|
||||
@@ -208,6 +209,8 @@ interface BackendRequestComponent {
|
||||
|
||||
WipeOutCloudSqlAction wipeOutCloudSqlAction();
|
||||
|
||||
WipeoutDatastoreAction wipeoutDatastoreAction();
|
||||
|
||||
@Subcomponent.Builder
|
||||
abstract class Builder implements RequestComponentBuilder<BackendRequestComponent> {
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import google.registry.keyring.api.KeyModule;
|
||||
import google.registry.keyring.kms.KmsModule;
|
||||
import google.registry.module.frontend.FrontendRequestComponent.FrontendRequestComponentModule;
|
||||
import google.registry.monitoring.whitebox.StackdriverModule;
|
||||
import google.registry.privileges.secretmanager.SecretManagerModule;
|
||||
import google.registry.request.Modules.Jackson2Module;
|
||||
import google.registry.request.Modules.NetHttpTransportModule;
|
||||
import google.registry.request.Modules.UrlFetchTransportModule;
|
||||
@@ -58,6 +59,7 @@ import javax.inject.Singleton;
|
||||
KeyringModule.class,
|
||||
KmsModule.class,
|
||||
NetHttpTransportModule.class,
|
||||
SecretManagerModule.class,
|
||||
ServerTridProviderModule.class,
|
||||
StackdriverModule.class,
|
||||
UrlFetchTransportModule.class,
|
||||
|
||||
@@ -30,6 +30,7 @@ import google.registry.keyring.api.KeyModule;
|
||||
import google.registry.keyring.kms.KmsModule;
|
||||
import google.registry.module.pubapi.PubApiRequestComponent.PubApiRequestComponentModule;
|
||||
import google.registry.monitoring.whitebox.StackdriverModule;
|
||||
import google.registry.privileges.secretmanager.SecretManagerModule;
|
||||
import google.registry.request.Modules.Jackson2Module;
|
||||
import google.registry.request.Modules.NetHttpTransportModule;
|
||||
import google.registry.request.Modules.UrlFetchTransportModule;
|
||||
@@ -56,6 +57,7 @@ import javax.inject.Singleton;
|
||||
KmsModule.class,
|
||||
NetHttpTransportModule.class,
|
||||
PubApiRequestComponentModule.class,
|
||||
SecretManagerModule.class,
|
||||
ServerTridProviderModule.class,
|
||||
StackdriverModule.class,
|
||||
UrlFetchTransportModule.class,
|
||||
|
||||
@@ -32,6 +32,7 @@ import google.registry.keyring.api.KeyModule;
|
||||
import google.registry.keyring.kms.KmsModule;
|
||||
import google.registry.module.tools.ToolsRequestComponent.ToolsRequestComponentModule;
|
||||
import google.registry.monitoring.whitebox.StackdriverModule;
|
||||
import google.registry.privileges.secretmanager.SecretManagerModule;
|
||||
import google.registry.request.Modules.DatastoreServiceModule;
|
||||
import google.registry.request.Modules.Jackson2Module;
|
||||
import google.registry.request.Modules.NetHttpTransportModule;
|
||||
@@ -61,6 +62,7 @@ import javax.inject.Singleton;
|
||||
KeyringModule.class,
|
||||
KmsModule.class,
|
||||
NetHttpTransportModule.class,
|
||||
SecretManagerModule.class,
|
||||
ServerTridProviderModule.class,
|
||||
StackdriverModule.class,
|
||||
ToolsRequestComponentModule.class,
|
||||
|
||||
@@ -51,7 +51,7 @@ public class HibernateSchemaExporter {
|
||||
}
|
||||
|
||||
/** Exports DDL script to the {@code outputFile} for the given {@code entityClasses}. */
|
||||
public void export(ImmutableList<Class> entityClasses, File outputFile) {
|
||||
public void export(ImmutableList<Class<?>> entityClasses, File outputFile) {
|
||||
// Configure Hibernate settings.
|
||||
Map<String, String> settings = Maps.newHashMap();
|
||||
settings.put(Environment.DIALECT, NomulusPostgreSQLDialect.class.getName());
|
||||
@@ -85,7 +85,7 @@ public class HibernateSchemaExporter {
|
||||
}
|
||||
}
|
||||
|
||||
private ImmutableList<Class> findAllConverters() {
|
||||
private ImmutableList<Class<?>> findAllConverters() {
|
||||
return PersistenceXmlUtility.getManagedClasses().stream()
|
||||
.filter(AttributeConverter.class::isAssignableFrom)
|
||||
.collect(toImmutableList());
|
||||
|
||||
@@ -31,7 +31,6 @@ import dagger.BindsOptionalOf;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.keyring.kms.KmsKeyring;
|
||||
import google.registry.persistence.transaction.CloudSqlCredentialSupplier;
|
||||
import google.registry.persistence.transaction.JpaTransactionManager;
|
||||
import google.registry.persistence.transaction.JpaTransactionManagerImpl;
|
||||
@@ -202,18 +201,11 @@ public abstract class PersistenceModule {
|
||||
@Singleton
|
||||
@AppEngineJpaTm
|
||||
static JpaTransactionManager provideAppEngineJpaTm(
|
||||
@Config("cloudSqlUsername") String username,
|
||||
KmsKeyring kmsKeyring,
|
||||
SqlCredentialStore credentialStore,
|
||||
@PartialCloudSqlConfigs ImmutableMap<String, String> cloudSqlConfigs,
|
||||
Clock clock) {
|
||||
HashMap<String, String> overrides = Maps.newHashMap(cloudSqlConfigs);
|
||||
validateAndSetCredential(
|
||||
credentialStore,
|
||||
new RobotUser(RobotId.NOMULUS),
|
||||
overrides,
|
||||
username,
|
||||
kmsKeyring.getCloudSqlPassword());
|
||||
setSqlCredential(credentialStore, new RobotUser(RobotId.NOMULUS), overrides);
|
||||
return new JpaTransactionManagerImpl(create(overrides), clock);
|
||||
}
|
||||
|
||||
@@ -258,20 +250,13 @@ public abstract class PersistenceModule {
|
||||
@Singleton
|
||||
@NomulusToolJpaTm
|
||||
static JpaTransactionManager provideNomulusToolJpaTm(
|
||||
@Config("toolsCloudSqlUsername") String username,
|
||||
KmsKeyring kmsKeyring,
|
||||
SqlCredentialStore credentialStore,
|
||||
@PartialCloudSqlConfigs ImmutableMap<String, String> cloudSqlConfigs,
|
||||
@CloudSqlClientCredential Credential credential,
|
||||
Clock clock) {
|
||||
CloudSqlCredentialSupplier.setupCredentialSupplier(credential);
|
||||
HashMap<String, String> overrides = Maps.newHashMap(cloudSqlConfigs);
|
||||
validateAndSetCredential(
|
||||
credentialStore,
|
||||
new RobotUser(RobotId.TOOL),
|
||||
overrides,
|
||||
username,
|
||||
kmsKeyring.getToolsCloudSqlPassword());
|
||||
setSqlCredential(credentialStore, new RobotUser(RobotId.TOOL), overrides);
|
||||
return new JpaTransactionManagerImpl(create(overrides), clock);
|
||||
}
|
||||
|
||||
@@ -349,35 +334,15 @@ public abstract class PersistenceModule {
|
||||
return emf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the credential from the Secret Manager matches the one currently in use, and
|
||||
* configures JPA with the credential from the Secret Manager.
|
||||
*
|
||||
* <p>This is a helper for the transition to the Secret Manager, and will be removed once data and
|
||||
* permissions are properly set up for all projects.
|
||||
*/
|
||||
private static void validateAndSetCredential(
|
||||
SqlCredentialStore credentialStore,
|
||||
SqlUser sqlUser,
|
||||
Map<String, String> overrides,
|
||||
String expectedLogin,
|
||||
String expectedPassword) {
|
||||
/** Configures JPA with the credential from the Secret Manager. */
|
||||
private static void setSqlCredential(
|
||||
SqlCredentialStore credentialStore, SqlUser sqlUser, Map<String, String> overrides) {
|
||||
try {
|
||||
SqlCredential credential = credentialStore.getCredential(sqlUser);
|
||||
checkState(
|
||||
credential.login().equals(expectedLogin),
|
||||
"Wrong login for %s. Expecting %s, found %s.",
|
||||
sqlUser.geUserName(),
|
||||
expectedLogin,
|
||||
credential.login());
|
||||
checkState(
|
||||
credential.password().equals(expectedPassword),
|
||||
"Wrong password for %s.",
|
||||
sqlUser.geUserName());
|
||||
overrides.put(Environment.USER, credential.login());
|
||||
overrides.put(Environment.PASS, credential.password());
|
||||
logger.atWarning().log("Credentials in the kerying and the secret manager match.");
|
||||
} catch (Throwable e) {
|
||||
// TODO(b/184631990): after SQL becomes primary, throw an exception to fail fast
|
||||
logger.atSevere().withCause(e).log("Failed to get SQL credential from Secret Manager");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ public class PersistenceXmlUtility {
|
||||
}
|
||||
|
||||
/** Returns all managed classes defined in persistence.xml. */
|
||||
public static ImmutableList<Class> getManagedClasses() {
|
||||
public static ImmutableList<Class<?>> getManagedClasses() {
|
||||
return getParsedPersistenceXmlDescriptor().getManagedClassNames().stream()
|
||||
.map(
|
||||
className -> {
|
||||
|
||||
@@ -64,7 +64,7 @@ public class VKey<T> extends ImmutableObject implements Serializable {
|
||||
}
|
||||
|
||||
/** Creates a {@link VKey} which only contains the ofy primary key. */
|
||||
public static <T> VKey<T> createOfy(Class<T> kind, Key<T> ofyKey) {
|
||||
public static <T> VKey<T> createOfy(Class<? extends T> kind, Key<T> ofyKey) {
|
||||
checkArgumentNotNull(kind, "kind must not be null");
|
||||
checkArgumentNotNull(ofyKey, "ofyKey must not be null");
|
||||
return new VKey<T>(kind, ofyKey, null);
|
||||
@@ -223,4 +223,14 @@ public class VKey<T> extends ImmutableObject implements Serializable {
|
||||
public static <T> VKey<T> from(Key<T> key) {
|
||||
return VKeyTranslatorFactory.createVKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a VKey from the string representation of an ofy key.
|
||||
*
|
||||
* <p>TODO(b/184350590): After migration, we'll want remove the ofy key dependency from this.
|
||||
*/
|
||||
@Nullable
|
||||
public static <T> VKey<T> fromWebsafeKey(String ofyKeyRepr) {
|
||||
return from(Key.create(ofyKeyRepr));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ public class DateTimeConverter implements AttributeConverter<DateTime, Timestamp
|
||||
@Override
|
||||
@Nullable
|
||||
public DateTime convertToEntityAttribute(@Nullable Timestamp dbData) {
|
||||
DateTime result = dbData == null ? null : new DateTime(dbData.getTime(), UTC);
|
||||
return result;
|
||||
return (dbData == null) ? null : new DateTime(dbData.getTime(), UTC);
|
||||
}
|
||||
}
|
||||
|
||||
+21
-5
@@ -18,6 +18,7 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.util.Collection;
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.criteria.CriteriaBuilder;
|
||||
import javax.persistence.criteria.CriteriaQuery;
|
||||
import javax.persistence.criteria.Expression;
|
||||
@@ -35,22 +36,23 @@ import javax.persistence.criteria.Root;
|
||||
public class CriteriaQueryBuilder<T> {
|
||||
|
||||
/** Functional interface that defines the 'where' operator, e.g. {@link CriteriaBuilder#equal}. */
|
||||
public interface WhereClause<U> {
|
||||
public interface WhereOperator<U> {
|
||||
Predicate predicate(Expression<U> expression, U object);
|
||||
}
|
||||
|
||||
private final CriteriaQuery<T> query;
|
||||
private final Root<T> root;
|
||||
private final Root<?> root;
|
||||
private final ImmutableList.Builder<Predicate> predicates = new ImmutableList.Builder<>();
|
||||
private final ImmutableList.Builder<Order> orders = new ImmutableList.Builder<>();
|
||||
|
||||
private CriteriaQueryBuilder(CriteriaQuery<T> query, Root<T> root) {
|
||||
private CriteriaQueryBuilder(CriteriaQuery<T> query, Root<?> root) {
|
||||
this.query = query;
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
/** Adds a WHERE clause to the query, given the specified operation, field, and value. */
|
||||
public <V> CriteriaQueryBuilder<T> where(String fieldName, WhereClause<V> whereClause, V value) {
|
||||
public <V> CriteriaQueryBuilder<T> where(
|
||||
String fieldName, WhereOperator<V> whereClause, V value) {
|
||||
Expression<V> expression = root.get(fieldName);
|
||||
return where(whereClause.predicate(expression, value));
|
||||
}
|
||||
@@ -94,9 +96,23 @@ public class CriteriaQueryBuilder<T> {
|
||||
|
||||
/** Creates a query builder that will SELECT from the given class. */
|
||||
public static <T> CriteriaQueryBuilder<T> create(Class<T> clazz) {
|
||||
CriteriaQuery<T> query = jpaTm().getEntityManager().getCriteriaBuilder().createQuery(clazz);
|
||||
return create(jpaTm().getEntityManager(), clazz);
|
||||
}
|
||||
|
||||
/** Creates a query builder for the given entity manager. */
|
||||
public static <T> CriteriaQueryBuilder<T> create(EntityManager em, Class<T> clazz) {
|
||||
CriteriaQuery<T> query = em.getCriteriaBuilder().createQuery(clazz);
|
||||
Root<T> root = query.from(clazz);
|
||||
query = query.select(root);
|
||||
return new CriteriaQueryBuilder<>(query, root);
|
||||
}
|
||||
|
||||
/** Creates a "count" query for the table for the class. */
|
||||
public static <T> CriteriaQueryBuilder<Long> createCount(EntityManager em, Class<T> clazz) {
|
||||
CriteriaBuilder builder = em.getCriteriaBuilder();
|
||||
CriteriaQuery<Long> query = builder.createQuery(Long.class);
|
||||
Root<T> root = query.from(clazz);
|
||||
query = query.select(builder.count(root));
|
||||
return new CriteriaQueryBuilder<>(query, root);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ public interface JpaTransactionManager extends TransactionManager {
|
||||
void transactNoRetry(Runnable work);
|
||||
|
||||
/** Deletes the entity by its id, throws exception if the entity is not deleted. */
|
||||
public abstract <T> void assertDelete(VKey<T> key);
|
||||
<T> void assertDelete(VKey<T> key);
|
||||
|
||||
/**
|
||||
* Releases all resources and shuts down.
|
||||
|
||||
+61
@@ -37,16 +37,19 @@ import google.registry.model.index.ForeignKeyIndex.ForeignKeyDomainIndex;
|
||||
import google.registry.model.index.ForeignKeyIndex.ForeignKeyHostIndex;
|
||||
import google.registry.model.ofy.DatastoreTransactionManager;
|
||||
import google.registry.model.server.KmsSecret;
|
||||
import google.registry.model.tmch.ClaimsListShard.ClaimsListSingleton;
|
||||
import google.registry.persistence.JpaRetries;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.util.Clock;
|
||||
import google.registry.util.Retrier;
|
||||
import google.registry.util.SystemSleeper;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.EntityManagerFactory;
|
||||
@@ -71,6 +74,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
// TODO(b/176108270): Remove this property after database migration.
|
||||
private static final ImmutableSet<Class<? extends ImmutableObject>> IGNORED_ENTITY_CLASSES =
|
||||
ImmutableSet.of(
|
||||
ClaimsListSingleton.class,
|
||||
EppResourceIndex.class,
|
||||
ForeignKeyContactIndex.class,
|
||||
ForeignKeyDomainIndex.class,
|
||||
@@ -475,6 +479,9 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
private int internalDelete(VKey<?> key) {
|
||||
checkArgumentNotNull(key, "key must be specified");
|
||||
assertInTransaction();
|
||||
if (IGNORED_ENTITY_CLASSES.contains(key.getKind())) {
|
||||
return 0;
|
||||
}
|
||||
EntityType<?> entityType = getEntityType(key.getKind());
|
||||
ImmutableSet<EntityId> entityIds = getEntityIdsFromSqlKey(entityType, key.getSqlKey());
|
||||
// TODO(b/179158393): use Criteria for query to leave not doubt about sql injection risk.
|
||||
@@ -527,6 +534,11 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
delete(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> QueryComposer<T> createQueryComposer(Class<T> entity) {
|
||||
return new JpaQueryComposerImpl<T>(entity, getEntityManager());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearSessionCache() {
|
||||
// This is an intended no-op method as there is no session cache in Postgresql.
|
||||
@@ -678,4 +690,53 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class JpaQueryComposerImpl<T> extends QueryComposer<T> {
|
||||
|
||||
EntityManager em;
|
||||
|
||||
JpaQueryComposerImpl(Class<T> entityClass, EntityManager em) {
|
||||
super(entityClass);
|
||||
this.em = em;
|
||||
}
|
||||
|
||||
private TypedQuery<T> buildQuery() {
|
||||
CriteriaQueryBuilder<T> queryBuilder = CriteriaQueryBuilder.create(em, entityClass);
|
||||
return addCriteria(queryBuilder);
|
||||
}
|
||||
|
||||
private <U> TypedQuery<U> addCriteria(CriteriaQueryBuilder<U> queryBuilder) {
|
||||
for (WhereClause<?> pred : predicates) {
|
||||
pred.addToCriteriaQueryBuilder(queryBuilder);
|
||||
}
|
||||
|
||||
if (orderBy != null) {
|
||||
queryBuilder.orderByAsc(orderBy);
|
||||
}
|
||||
|
||||
return em.createQuery(queryBuilder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<T> first() {
|
||||
List<T> results = buildQuery().setMaxResults(1).getResultList();
|
||||
return results.size() > 0 ? Optional.of(results.get(0)) : Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getSingleResult() {
|
||||
return buildQuery().getSingleResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<T> stream() {
|
||||
return buildQuery().getResultStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long count() {
|
||||
CriteriaQueryBuilder<Long> queryBuilder = CriteriaQueryBuilder.createCount(em, entityClass);
|
||||
return addCriteria(queryBuilder).getSingleResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.persistence.transaction;
|
||||
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
||||
|
||||
import com.google.common.base.Function;
|
||||
import google.registry.persistence.transaction.CriteriaQueryBuilder.WhereOperator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.persistence.criteria.CriteriaBuilder;
|
||||
|
||||
/**
|
||||
* Creates queries that can be used both for objectify and JPA.
|
||||
*
|
||||
* <p>Example usage:
|
||||
*
|
||||
* <pre>
|
||||
* tm().createQueryComposer(EntityType.class)
|
||||
* .where("fieldName", Comparator.EQ, "value")
|
||||
* .orderBy("fieldName")
|
||||
* .stream()
|
||||
* </pre>
|
||||
*/
|
||||
public abstract class QueryComposer<T> {
|
||||
|
||||
// The class whose entities we're querying. Note that this limits us to single table queries in
|
||||
// SQL. In datastore, there's really no other kind of query.
|
||||
protected Class<T> entityClass;
|
||||
|
||||
// Field to order by, if any. Null if we don't care about order.
|
||||
@Nullable protected String orderBy;
|
||||
|
||||
protected List<WhereClause<?>> predicates = new ArrayList<WhereClause<?>>();
|
||||
|
||||
protected QueryComposer(Class<T> entityClass) {
|
||||
this.entityClass = entityClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Introduce a "where" clause to the query.
|
||||
*
|
||||
* <p>Causes the query to return only results where the field and value have the relationship
|
||||
* specified by the comparator. For example, "field EQ value", "field GT value" etc.
|
||||
*/
|
||||
public <U extends Comparable<? super U>> QueryComposer<T> where(
|
||||
String fieldName, Comparator comparator, U value) {
|
||||
predicates.add(new WhereClause(fieldName, comparator, value));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Order the query results by the value of the specified field.
|
||||
*
|
||||
* <p>TODO(mmuller): add the ability to do descending sort order.
|
||||
*/
|
||||
public QueryComposer<T> orderBy(String fieldName) {
|
||||
orderBy = fieldName;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Returns the first result of the query or an empty optional if there is none. */
|
||||
public abstract Optional<T> first();
|
||||
|
||||
/**
|
||||
* Returns the one and only result of a query.
|
||||
*
|
||||
* <p>Throws a {@link javax.persistence.NonUniqueResultException} if there is more than one
|
||||
* result, throws {@link javax.persistence.NoResultException} if no results are found.
|
||||
*/
|
||||
public abstract T getSingleResult();
|
||||
|
||||
/** Returns the results of the query as a stream. */
|
||||
public abstract Stream<T> stream();
|
||||
|
||||
/** Returns the number of results of the query. */
|
||||
public abstract long count();
|
||||
|
||||
// We have to wrap the CriteriaQueryBuilder predicate factories in our own functions because at
|
||||
// the point where we pass them to the Comparator constructor, the compiler can't determine which
|
||||
// of the overloads to use since there is no "value" object for context.
|
||||
|
||||
public static <U extends Comparable<? super U>> WhereOperator<U> equal(
|
||||
CriteriaBuilder criteriaBuilder) {
|
||||
return criteriaBuilder::equal;
|
||||
}
|
||||
|
||||
public static <U extends Comparable<? super U>> WhereOperator<U> lessThan(
|
||||
CriteriaBuilder criteriaBuilder) {
|
||||
return criteriaBuilder::lessThan;
|
||||
}
|
||||
|
||||
public static <U extends Comparable<? super U>> WhereOperator<U> lessThanOrEqualTo(
|
||||
CriteriaBuilder criteriaBuilder) {
|
||||
return criteriaBuilder::lessThanOrEqualTo;
|
||||
}
|
||||
|
||||
public static <U extends Comparable<? super U>> WhereOperator<U> greaterThanOrEqualTo(
|
||||
CriteriaBuilder criteriaBuilder) {
|
||||
return criteriaBuilder::greaterThanOrEqualTo;
|
||||
}
|
||||
|
||||
public static <U extends Comparable<? super U>> WhereOperator<U> greaterThan(
|
||||
CriteriaBuilder criteriaBuilder) {
|
||||
return criteriaBuilder::greaterThan;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum used to specify comparison operations, e.g. {@code where("fieldName", Comparator.NE,
|
||||
* "someval")'}.
|
||||
*
|
||||
* <p>These contain values that specify the comparison behavior for both objectify and criteria
|
||||
* queries. For objectify, we provide a string to be appended to the field name in a {@code
|
||||
* filter()} expression. For criteria queries we provide a function that knows how to obtain a
|
||||
* {@link WhereOperator} from a {@link CriteriaBuilder}.
|
||||
*
|
||||
* <p>Note that the objectify strings for comparators other than equality are preceded by a space
|
||||
* because {@code filter()} expects the fieldname to be separated from the operator by a space.
|
||||
*/
|
||||
public enum Comparator {
|
||||
/**
|
||||
* Return only records whose field is equal to the value.
|
||||
*
|
||||
* <p>Note that the datastore string for this is empty, which is consistent with the way {@code
|
||||
* filter()} works (it uses an unadorned field name to check for equality).
|
||||
*/
|
||||
EQ("", QueryComposer::equal),
|
||||
|
||||
/** Return only records whose field is less than the value. */
|
||||
LT(" <", QueryComposer::lessThan),
|
||||
|
||||
/** Return only records whose field is less than or equal to the value. */
|
||||
LTE(" <=", QueryComposer::lessThanOrEqualTo),
|
||||
|
||||
/** Return only records whose field is greater than or equal to the value. */
|
||||
GTE(" >=", QueryComposer::greaterThanOrEqualTo),
|
||||
|
||||
/** Return only records whose field is greater than the value. */
|
||||
GT(" >", QueryComposer::greaterThan);
|
||||
|
||||
private final String datastoreString;
|
||||
|
||||
@SuppressWarnings("ImmutableEnumChecker") // Functions are immutable.
|
||||
private final Function<CriteriaBuilder, WhereOperator<?>> operatorFactory;
|
||||
|
||||
Comparator(
|
||||
String datastoreString, Function<CriteriaBuilder, WhereOperator<?>> operatorFactory) {
|
||||
this.datastoreString = datastoreString;
|
||||
this.operatorFactory = operatorFactory;
|
||||
}
|
||||
|
||||
public String getDatastoreString() {
|
||||
return datastoreString;
|
||||
}
|
||||
|
||||
public Function<CriteriaBuilder, WhereOperator<?>> getComparisonFactory() {
|
||||
return operatorFactory;
|
||||
}
|
||||
};
|
||||
|
||||
protected static class WhereClause<U extends Comparable<? super U>> {
|
||||
public String fieldName;
|
||||
public Comparator comparator;
|
||||
public U value;
|
||||
|
||||
WhereClause(String fieldName, Comparator comparator, U value) {
|
||||
this.fieldName = fieldName;
|
||||
this.comparator = comparator;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public void addToCriteriaQueryBuilder(CriteriaQueryBuilder queryBuilder) {
|
||||
CriteriaBuilder criteriaBuilder = jpaTm().getEntityManager().getCriteriaBuilder();
|
||||
queryBuilder.where(
|
||||
fieldName, comparator.getComparisonFactory().apply(criteriaBuilder), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,7 +165,7 @@ public class Transaction extends ImmutableObject implements Buildable {
|
||||
enum Type {
|
||||
UPDATE,
|
||||
DELETE
|
||||
};
|
||||
}
|
||||
|
||||
/** Write the changes in the mutation to the datastore. */
|
||||
public abstract void writeToDatastore();
|
||||
|
||||
@@ -273,6 +273,9 @@ public interface TransactionManager {
|
||||
*/
|
||||
void deleteWithoutBackup(Object entity);
|
||||
|
||||
/** Returns a QueryComposer which can be used to perform queries against the current database. */
|
||||
<T> QueryComposer<T> createQueryComposer(Class<T> entity);
|
||||
|
||||
/** Clears the session cache if the underlying database is Datastore, otherwise it is a no-op. */
|
||||
void clearSessionCache();
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// 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.privileges.secretmanager;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Strings.isNullOrEmpty;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Optional;
|
||||
import javax.inject.Inject;
|
||||
|
||||
/**
|
||||
* Storage for 'keyring' secrets, backed by the Secret Manager.
|
||||
*
|
||||
* <p>This store is for secrets and credentials that must be set up manually and/or do not require
|
||||
* non-disruptive password changes, e.g., passwords to regulatory reporting websites, which are used
|
||||
* by cron jobs.
|
||||
*
|
||||
* <p>In contrast, the {@link SqlCredentialStore} is designed to support non-disruptive credential
|
||||
* changes with Cloud SQL.
|
||||
*/
|
||||
public class KeyringSecretStore {
|
||||
static final String SECRET_NAME_PREFIX = "keyring-";
|
||||
|
||||
private final SecretManagerClient csmClient;
|
||||
|
||||
@Inject
|
||||
public KeyringSecretStore(SecretManagerClient csmClient) {
|
||||
this.csmClient = csmClient;
|
||||
}
|
||||
|
||||
public void createOrUpdateSecret(String label, byte[] data) {
|
||||
String secretId = decorateLabel(label);
|
||||
csmClient.createSecretIfAbsent(secretId);
|
||||
csmClient.addSecretVersion(secretId, new String(data, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public byte[] getSecret(String label) {
|
||||
return csmClient
|
||||
.getSecretData(decorateLabel(label), Optional.empty())
|
||||
.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
static String decorateLabel(String label) {
|
||||
checkArgument(!isNullOrEmpty(label), "null or empty label");
|
||||
return SECRET_NAME_PREFIX + label;
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,15 @@ public interface SecretManagerClient {
|
||||
/** Returns the {@link SecretVersionState} of all secrets with {@code secretId}. */
|
||||
Iterable<SecretVersionState> listSecretVersions(String secretId);
|
||||
|
||||
/** Creates a secret if it does not already exists. */
|
||||
default void createSecretIfAbsent(String secretId) {
|
||||
try {
|
||||
createSecret(secretId);
|
||||
} catch (SecretAlreadyExistsException ignore) {
|
||||
// Not a problem.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the version strings of all secrets in the given {@code state} with {@code secretId}.
|
||||
*/
|
||||
|
||||
+2
-11
@@ -17,7 +17,6 @@ package google.registry.privileges.secretmanager;
|
||||
import com.google.cloud.secretmanager.v1.SecretVersionName;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.privileges.secretmanager.SecretManagerClient.NoSuchSecretResourceException;
|
||||
import google.registry.privileges.secretmanager.SecretManagerClient.SecretAlreadyExistsException;
|
||||
import java.util.Optional;
|
||||
import javax.inject.Inject;
|
||||
|
||||
@@ -74,17 +73,9 @@ public class SqlCredentialStore {
|
||||
}
|
||||
}
|
||||
|
||||
private void createSecretIfAbsent(String secretId) {
|
||||
try {
|
||||
csmClient.createSecret(secretId);
|
||||
} catch (SecretAlreadyExistsException ignore) {
|
||||
// Not a problem.
|
||||
}
|
||||
}
|
||||
|
||||
private SecretVersionName saveCredentialData(SqlUser user, String password) {
|
||||
String credentialDataSecretId = getCredentialDataSecretId(user, dbInstance);
|
||||
createSecretIfAbsent(credentialDataSecretId);
|
||||
csmClient.createSecretIfAbsent(credentialDataSecretId);
|
||||
String credentialVersion =
|
||||
csmClient.addSecretVersion(
|
||||
credentialDataSecretId,
|
||||
@@ -94,7 +85,7 @@ public class SqlCredentialStore {
|
||||
|
||||
private void saveLiveLabel(SqlUser user, SecretVersionName dataVersionName) {
|
||||
String liveLabelSecretId = getLiveLabelSecretId(user, dbInstance);
|
||||
createSecretIfAbsent(liveLabelSecretId);
|
||||
csmClient.createSecretIfAbsent(liveLabelSecretId);
|
||||
csmClient.addSecretVersion(liveLabelSecretId, dataVersionName.toString());
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ public abstract class SqlUser {
|
||||
* Credential for RegistryTool. This is temporary, and will be removed when tool users are
|
||||
* assigned their personal credentials.
|
||||
*/
|
||||
TOOL;
|
||||
TOOL
|
||||
}
|
||||
|
||||
/** Information of a RobotUser for privilege management purposes. */
|
||||
|
||||
@@ -32,7 +32,6 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import javax.inject.Inject;
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPKeyPair;
|
||||
import org.bouncycastle.openpgp.PGPPrivateKey;
|
||||
import org.bouncycastle.openpgp.PGPPublicKey;
|
||||
@@ -79,12 +78,12 @@ public final class BrdaCopyAction implements Runnable {
|
||||
public void run() {
|
||||
try {
|
||||
copyAsRyde();
|
||||
} catch (IOException | PGPException e) {
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void copyAsRyde() throws IOException, PGPException {
|
||||
private void copyAsRyde() throws IOException {
|
||||
String prefix = RdeNamingUtils.makeRydeFilename(tld, watermark, THIN, 1, 0);
|
||||
GcsFilename xmlFilename = new GcsFilename(stagingBucket, prefix + ".xml.ghostryde");
|
||||
GcsFilename xmlLengthFilename = new GcsFilename(stagingBucket, prefix + ".xml.length");
|
||||
|
||||
@@ -16,6 +16,7 @@ package google.registry.rde;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
|
||||
|
||||
import com.google.common.base.Ascii;
|
||||
import com.google.common.base.Strings;
|
||||
@@ -172,7 +173,7 @@ final class DomainBaseToXjcConverter {
|
||||
if (registrant == null) {
|
||||
logger.atWarning().log("Domain %s has no registrant contact.", domainName);
|
||||
} else {
|
||||
ContactResource registrantContact = tm().loadByKey(registrant);
|
||||
ContactResource registrantContact = transactIfJpaTm(() -> tm().loadByKey(registrant));
|
||||
checkState(
|
||||
registrantContact != null,
|
||||
"Registrant contact %s on domain %s does not exist",
|
||||
@@ -305,7 +306,7 @@ final class DomainBaseToXjcConverter {
|
||||
"Contact key for type %s is null on domain %s",
|
||||
model.getType(),
|
||||
domainName);
|
||||
ContactResource contact = tm().loadByKey(model.getContactKey());
|
||||
ContactResource contact = transactIfJpaTm(() -> tm().loadByKey(model.getContactKey()));
|
||||
checkState(
|
||||
contact != null,
|
||||
"Contact %s on domain %s does not exist",
|
||||
|
||||
@@ -37,7 +37,6 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import javax.annotation.Nullable;
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPPrivateKey;
|
||||
import org.bouncycastle.openpgp.PGPPublicKey;
|
||||
import org.joda.time.DateTime;
|
||||
@@ -118,11 +117,8 @@ public final class Ghostryde {
|
||||
static final String INNER_FILENAME = "file.xml";
|
||||
static final DateTime INNER_MODIFICATION_TIME = DateTime.parse("2000-01-01TZ");
|
||||
|
||||
/**
|
||||
* Creates a ghostryde file from an in-memory byte array.
|
||||
*/
|
||||
public static byte[] encode(byte[] data, PGPPublicKey key)
|
||||
throws IOException, PGPException {
|
||||
/** Creates a ghostryde file from an in-memory byte array. */
|
||||
public static byte[] encode(byte[] data, PGPPublicKey key) throws IOException {
|
||||
checkNotNull(data, "data");
|
||||
checkArgument(key.isEncryptionKey(), "not an encryption key");
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
@@ -132,11 +128,8 @@ public final class Ghostryde {
|
||||
return output.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deciphers a ghostryde file from an in-memory byte array.
|
||||
*/
|
||||
public static byte[] decode(byte[] data, PGPPrivateKey key)
|
||||
throws IOException, PGPException {
|
||||
/** Deciphers a ghostryde file from an in-memory byte array. */
|
||||
public static byte[] decode(byte[] data, PGPPrivateKey key) throws IOException {
|
||||
checkNotNull(data, "data");
|
||||
ByteArrayInputStream dataStream = new ByteArrayInputStream(data);
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
|
||||
@@ -24,6 +24,7 @@ import static google.registry.request.Action.Method.POST;
|
||||
import static google.registry.util.DateTimeUtils.isBeforeOrAt;
|
||||
|
||||
import com.google.appengine.tools.cloudstorage.GcsFilename;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.gcs.GcsUtils;
|
||||
@@ -31,6 +32,7 @@ import google.registry.keyring.api.KeyModule.Key;
|
||||
import google.registry.model.common.Cursor;
|
||||
import google.registry.model.common.Cursor.CursorType;
|
||||
import google.registry.model.rde.RdeNamingUtils;
|
||||
import google.registry.model.rde.RdeRevision;
|
||||
import google.registry.model.registry.Registry;
|
||||
import google.registry.rde.EscrowTaskRunner.EscrowTask;
|
||||
import google.registry.request.Action;
|
||||
@@ -41,8 +43,8 @@ import google.registry.request.Response;
|
||||
import google.registry.request.auth.Auth;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Optional;
|
||||
import javax.inject.Inject;
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPPrivateKey;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Duration;
|
||||
@@ -57,6 +59,8 @@ import org.joda.time.Duration;
|
||||
auth = Auth.AUTH_INTERNAL_OR_ADMIN)
|
||||
public final class RdeReportAction implements Runnable, EscrowTask {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
static final String PATH = "/_dr/task/rdeReport";
|
||||
|
||||
@Inject GcsUtils gcsUtils;
|
||||
@@ -77,8 +81,9 @@ public final class RdeReportAction implements Runnable, EscrowTask {
|
||||
|
||||
@Override
|
||||
public void runWithLock(DateTime watermark) throws Exception {
|
||||
Cursor cursor =
|
||||
transactIfJpaTm(() -> tm().loadByKey(Cursor.createVKey(CursorType.RDE_UPLOAD, tld)));
|
||||
Optional<Cursor> cursor =
|
||||
transactIfJpaTm(
|
||||
() -> tm().loadByKeyIfPresent(Cursor.createVKey(CursorType.RDE_UPLOAD, tld)));
|
||||
DateTime cursorTime = getCursorTimeOrStartOfTime(cursor);
|
||||
if (isBeforeOrAt(cursorTime, watermark)) {
|
||||
throw new NoContentException(
|
||||
@@ -87,16 +92,21 @@ public final class RdeReportAction implements Runnable, EscrowTask {
|
||||
+ "last upload completion was at %s",
|
||||
tld, watermark, cursorTime));
|
||||
}
|
||||
String prefix = RdeNamingUtils.makeRydeFilename(tld, watermark, FULL, 1, 0);
|
||||
int revision =
|
||||
RdeRevision.getCurrentRevision(tld, watermark, FULL)
|
||||
.orElseThrow(
|
||||
() -> new IllegalStateException("RdeRevision was not set on generated deposit"));
|
||||
String prefix = RdeNamingUtils.makeRydeFilename(tld, watermark, FULL, 1, revision);
|
||||
GcsFilename reportFilename = new GcsFilename(bucket, prefix + "-report.xml.ghostryde");
|
||||
verify(gcsUtils.existsAndNotEmpty(reportFilename), "Missing file: %s", reportFilename);
|
||||
reporter.send(readReportFromGcs(reportFilename));
|
||||
response.setContentType(PLAIN_TEXT_UTF_8);
|
||||
response.setPayload(String.format("OK %s %s\n", tld, watermark));
|
||||
logger.atInfo().log("Successfully sent report %s.", reportFilename);
|
||||
}
|
||||
|
||||
/** Reads and decrypts the XML file from cloud storage. */
|
||||
private byte[] readReportFromGcs(GcsFilename reportFilename) throws IOException, PGPException {
|
||||
private byte[] readReportFromGcs(GcsFilename reportFilename) throws IOException {
|
||||
try (InputStream gcsInput = gcsUtils.openInputStream(reportFilename);
|
||||
InputStream ghostrydeDecoder = Ghostryde.decoder(gcsInput, stagingDecryptionKey)) {
|
||||
return ByteStreams.toByteArray(ghostrydeDecoder);
|
||||
|
||||
@@ -20,8 +20,8 @@ import static com.google.appengine.tools.cloudstorage.GcsServiceFactory.createGc
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.common.base.Verify.verify;
|
||||
import static google.registry.model.common.Cursor.getCursorTimeOrStartOfTime;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.google.appengine.tools.cloudstorage.GcsFilename;
|
||||
@@ -210,7 +210,11 @@ public final class RdeStagingReducer extends Reducer<PendingDeposit, DepositFrag
|
||||
tm().transact(
|
||||
() -> {
|
||||
Registry registry = Registry.get(tld);
|
||||
Cursor cursor = ofy().load().key(Cursor.createKey(key.cursor(), registry)).now();
|
||||
Optional<Cursor> cursor =
|
||||
transactIfJpaTm(
|
||||
() ->
|
||||
tm().loadByKeyIfPresent(
|
||||
Cursor.createVKey(key.cursor(), registry.getTldStr())));
|
||||
DateTime position = getCursorTimeOrStartOfTime(cursor);
|
||||
checkState(key.interval() != null, "Interval must be present");
|
||||
DateTime newPosition = key.watermark().plus(key.interval());
|
||||
|
||||
@@ -64,6 +64,7 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
import java.util.Optional;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import org.bouncycastle.openpgp.PGPKeyPair;
|
||||
@@ -133,7 +134,8 @@ public final class RdeUploadAction implements Runnable, EscrowTask {
|
||||
@Override
|
||||
public void runWithLock(final DateTime watermark) throws Exception {
|
||||
logger.atInfo().log("Verifying readiness to upload the RDE deposit.");
|
||||
Cursor cursor = transactIfJpaTm(() -> tm().loadByKey(Cursor.createVKey(RDE_STAGING, tld)));
|
||||
Optional<Cursor> cursor =
|
||||
transactIfJpaTm(() -> tm().loadByKeyIfPresent(Cursor.createVKey(RDE_STAGING, tld)));
|
||||
DateTime stagingCursorTime = getCursorTimeOrStartOfTime(cursor);
|
||||
if (isBeforeOrAt(stagingCursorTime, watermark)) {
|
||||
throw new NoContentException(
|
||||
@@ -158,8 +160,10 @@ public final class RdeUploadAction implements Runnable, EscrowTask {
|
||||
sftpCursorTime,
|
||||
timeSinceLastSftp.getStandardMinutes()));
|
||||
}
|
||||
int revision = RdeRevision.getNextRevision(tld, watermark, FULL) - 1;
|
||||
verify(revision >= 0, "RdeRevision was not set on generated deposit");
|
||||
int revision =
|
||||
RdeRevision.getCurrentRevision(tld, watermark, FULL)
|
||||
.orElseThrow(
|
||||
() -> new IllegalStateException("RdeRevision was not set on generated deposit"));
|
||||
final String name = RdeNamingUtils.makeRydeFilename(tld, watermark, FULL, 1, revision);
|
||||
final GcsFilename xmlFilename = new GcsFilename(bucket, name + ".xml.ghostryde");
|
||||
final GcsFilename xmlLengthFilename = new GcsFilename(bucket, name + ".xml.length");
|
||||
|
||||
@@ -74,9 +74,8 @@ public final class RydeEncoder extends FilterOutputStream {
|
||||
OutputStream kompressor = closer.register(openCompressor(encryptLayer));
|
||||
OutputStream fileLayer =
|
||||
closer.register(openPgpFileWriter(kompressor, filenamePrefix + ".tar", modified));
|
||||
OutputStream tarLayer =
|
||||
this.out =
|
||||
closer.register(openTarWriter(fileLayer, dataLength, filenamePrefix + ".xml", modified));
|
||||
this.out = tarLayer;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -128,7 +128,7 @@ public final class ActivityReportingQueryBuilder implements QueryBuilder {
|
||||
return queriesBuilder.build();
|
||||
}
|
||||
|
||||
public void prepareForQuery(YearMonth yearMonth) throws Exception {
|
||||
public void prepareForQuery(YearMonth yearMonth) throws InterruptedException {
|
||||
dnsCountQueryCoordinator.prepareForQuery(yearMonth);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -35,5 +35,5 @@ public class BasicDnsCountQueryCoordinator implements DnsCountQueryCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareForQuery(YearMonth yearMonth) throws Exception {}
|
||||
public void prepareForQuery(YearMonth yearMonth) {}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ public interface DnsCountQueryCoordinator {
|
||||
/**
|
||||
* Class to carry parameters for a new coordinator.
|
||||
*
|
||||
* If your report query requires any additional parameters, add them here.
|
||||
* <p>If your report query requires any additional parameters, add them here.
|
||||
*/
|
||||
class Params {
|
||||
public BigqueryConnection bigquery;
|
||||
@@ -49,6 +49,12 @@ public interface DnsCountQueryCoordinator {
|
||||
/** Creates the string used to query bigtable for DNS count information. */
|
||||
String createQuery(YearMonth yearMonth);
|
||||
|
||||
/** Do any necessry preparation for the DNS query. */
|
||||
void prepareForQuery(YearMonth yearMonth) throws Exception;
|
||||
/**
|
||||
* Do any necessary preparation for the DNS query.
|
||||
*
|
||||
* <p>This potentially throws {@link InterruptedException} because some implementations use
|
||||
* interruptible futures to prepare the query (and the correct thing to do with such exceptions is
|
||||
* to handle them correctly or propagate them as-is, no {@link RuntimeException} wrapping).
|
||||
*/
|
||||
void prepareForQuery(YearMonth yearMonth) throws InterruptedException;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ package google.registry.reporting.icann;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
|
||||
import static google.registry.model.common.Cursor.getCursorTimeOrStartOfTime;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.request.Action.Method.POST;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
||||
@@ -107,10 +106,10 @@ public final class IcannReportingUploadAction implements Runnable {
|
||||
|
||||
// If cursor time is before now, upload the corresponding report
|
||||
cursors.entrySet().stream()
|
||||
.filter(entry -> getCursorTimeOrStartOfTime(entry.getKey()).isBefore(clock.nowUtc()))
|
||||
.filter(entry -> entry.getKey().getCursorTime().isBefore(clock.nowUtc()))
|
||||
.forEach(
|
||||
entry -> {
|
||||
DateTime cursorTime = getCursorTimeOrStartOfTime(entry.getKey());
|
||||
DateTime cursorTime = entry.getKey().getCursorTime();
|
||||
uploadReport(
|
||||
cursorTime,
|
||||
entry.getKey().getType(),
|
||||
@@ -278,9 +277,15 @@ public final class IcannReportingUploadAction implements Runnable {
|
||||
}
|
||||
|
||||
private void emailUploadResults(ImmutableMap<String, Boolean> reportSummary) {
|
||||
String subject = String.format(
|
||||
"ICANN Monthly report upload summary: %d/%d succeeded",
|
||||
reportSummary.values().stream().filter((b) -> b).count(), reportSummary.size());
|
||||
if (reportSummary.size() == 0) {
|
||||
logger.atInfo().log("No uploads were attempted today; skipping notification email.");
|
||||
return;
|
||||
}
|
||||
String subject =
|
||||
String.format(
|
||||
"ICANN Monthly report upload summary: %d/%d succeeded",
|
||||
// This filter() does in fact do something: It counts only the trues.
|
||||
reportSummary.values().stream().filter((b) -> b).count(), reportSummary.size());
|
||||
String body =
|
||||
String.format(
|
||||
"Report Filename - Upload status:\n%s",
|
||||
|
||||
@@ -17,7 +17,9 @@ package google.registry.reporting.spec11;
|
||||
import static com.google.common.base.Throwables.getRootCause;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.io.Resources.getResource;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.persistence.transaction.QueryComposer.Comparator;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
@@ -129,17 +131,20 @@ public class Spec11EmailUtils {
|
||||
private RegistrarThreatMatches filterOutNonPublishedMatches(
|
||||
RegistrarThreatMatches registrarThreatMatches) {
|
||||
ImmutableList<ThreatMatch> filteredMatches =
|
||||
registrarThreatMatches.threatMatches().stream()
|
||||
.filter(
|
||||
threatMatch ->
|
||||
ofy()
|
||||
.load()
|
||||
.type(DomainBase.class)
|
||||
.filter("fullyQualifiedDomainName", threatMatch.fullyQualifiedDomainName())
|
||||
.first()
|
||||
.now()
|
||||
.shouldPublishToDns())
|
||||
.collect(toImmutableList());
|
||||
transactIfJpaTm(
|
||||
() -> {
|
||||
return registrarThreatMatches.threatMatches().stream()
|
||||
.filter(
|
||||
threatMatch ->
|
||||
tm().createQueryComposer(DomainBase.class)
|
||||
.where(
|
||||
"fullyQualifiedDomainName",
|
||||
Comparator.EQ,
|
||||
threatMatch.fullyQualifiedDomainName())
|
||||
.getSingleResult()
|
||||
.shouldPublishToDns())
|
||||
.collect(toImmutableList());
|
||||
});
|
||||
return RegistrarThreatMatches.create(registrarThreatMatches.clientId(), filteredMatches);
|
||||
}
|
||||
|
||||
|
||||
+39
-20
@@ -19,6 +19,9 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.collect.Streams.stream;
|
||||
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 com.google.appengine.api.users.User;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
@@ -316,30 +319,46 @@ public class AuthenticatedRegistrarAccessor {
|
||||
logger.atInfo().log("Checking registrar contacts for user ID %s", user.getUserId());
|
||||
|
||||
// Find all registrars that have a registrar contact with this user's ID.
|
||||
ImmutableList<Key<Registrar>> accessibleClientIds =
|
||||
stream(ofy().load().type(RegistrarContact.class).filter("gaeUserId", user.getUserId()))
|
||||
.map(RegistrarContact::getParent)
|
||||
.collect(toImmutableList());
|
||||
// Filter out disabled registrars (note that pending registrars still allow console login).
|
||||
ofy().load().keys(accessibleClientIds).values().stream()
|
||||
.filter(registrar -> registrar.getState() != State.DISABLED)
|
||||
.forEach(registrar -> builder.put(registrar.getClientId(), Role.OWNER));
|
||||
if (tm().isOfy()) {
|
||||
ImmutableList<Key<Registrar>> accessibleClientIds =
|
||||
stream(ofy().load().type(RegistrarContact.class).filter("gaeUserId", user.getUserId()))
|
||||
.map(RegistrarContact::getParent)
|
||||
.collect(toImmutableList());
|
||||
// Filter out disabled registrars (note that pending registrars still allow console login).
|
||||
ofy().load().keys(accessibleClientIds).values().stream()
|
||||
.filter(registrar -> registrar.getState() != State.DISABLED)
|
||||
.forEach(registrar -> builder.put(registrar.getClientId(), Role.OWNER));
|
||||
} else {
|
||||
jpaTm()
|
||||
.transact(
|
||||
() ->
|
||||
jpaTm()
|
||||
.query(
|
||||
"SELECT r FROM Registrar r INNER JOIN RegistrarPoc rp ON "
|
||||
+ "r.clientIdentifier = rp.registrarId WHERE rp.gaeUserId = "
|
||||
+ ":gaeUserId AND r.state != :state",
|
||||
Registrar.class)
|
||||
.setParameter("gaeUserId", user.getUserId())
|
||||
.setParameter("state", State.DISABLED)
|
||||
.getResultStream()
|
||||
.forEach(registrar -> builder.put(registrar.getClientId(), Role.OWNER)));
|
||||
}
|
||||
|
||||
// Admins have ADMIN access to all registrars, and also OWNER access to the registry registrar
|
||||
// and all non-REAL or non-live registrars.
|
||||
if (isAdmin) {
|
||||
ofy()
|
||||
.load()
|
||||
.type(Registrar.class)
|
||||
.forEach(
|
||||
registrar -> {
|
||||
if (registrar.getType() != Registrar.Type.REAL
|
||||
|| !registrar.isLive()
|
||||
|| registrar.getClientId().equals(registryAdminClientId)) {
|
||||
builder.put(registrar.getClientId(), Role.OWNER);
|
||||
}
|
||||
builder.put(registrar.getClientId(), Role.ADMIN);
|
||||
});
|
||||
transactIfJpaTm(
|
||||
() ->
|
||||
tm().loadAllOf(Registrar.class)
|
||||
.forEach(
|
||||
registrar -> {
|
||||
if (registrar.getType() != Registrar.Type.REAL
|
||||
|| !registrar.isLive()
|
||||
|| registrar.getClientId().equals(registryAdminClientId)) {
|
||||
builder.put(registrar.getClientId(), Role.OWNER);
|
||||
}
|
||||
builder.put(registrar.getClientId(), Role.ADMIN);
|
||||
}));
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user