1
0
mirror of https://github.com/google/nomulus synced 2026-01-29 17:12:17 +00:00

Compare commits

...

23 Commits

Author SHA1 Message Date
Michael Muller
53ece5eda4 Enable filtering across all test tasks (#311)
The segregated test targets in core break the --tests filter.  Fix this by
defining a "testFilter" property and creating the FilteringTest task type that
applies it to the property set by "--tests".
2019-10-11 14:24:01 -04:00
Shicong Huang
6c220567c8 Write ClaimsList to Cloud SQL (#223)
* Rewrite ClaimsListShard with new API

* Write ClaimsList to Cloud SQL

* Add creationTimestamp
2019-10-11 12:31:34 -04:00
Weimin Yu
c3e3a1353b Allow schema-push to all env with Flyway (#309)
* Make Flyway schema task work with prod and sandbox

Also renamed the 'superuser' role to 'admin' since
we do not own super user in Cloud SQL.

* Allow pushing schema to all env with Flyway

Desktop schema push to production is needed in the short term.
Long-termly we need to decide if this should be kept for glass
breaking

Schema push to sandbox and production requires interactiveconfirmation.

Also fixed a typo in initialize_roles.sql.
2019-10-10 16:32:21 -04:00
Ben McIlwain
ce480a5191 Add Bloom filters to the Cloud SQL PremiumList schema (#306)
* Add Bloom filters to the Cloud SQL PremiumList schema

They are slightly different from the existing Bloom filters stored in Datastore
in that they now use an ASCII String encoding rather than the more generic
CharSequence, and there is no maximum size (whereas we previously had to live
within the 1 MB max entity size for Datastore).
2019-10-09 17:06:42 -04:00
Weimin Yu
f2a2b2d2e2 Modify Cloud SQL user management scripts (#302)
* Modify Cloud SQL user management scripts

Create readonly and readwrite roles that may be granted to users.
Also configured default privileges for tables created in the future.

Made sure arbitrary users may not create database or tables.

* Modify Cloud SQL user management scripts

Create readonly and readwrite roles that may be granted to users.
Also configured default privileges for tables created in the future.

Made sure arbitrary users may not create database or tables.
2019-10-09 16:02:42 -04:00
gbrodman
906b054f4b Load persistence.xml classes before adding test entities (#307)
* Load persistence.xml classes before adding test entities

* Also use persistence.xml in GenerateSqlSchemaCommand

* Add exception message

* remove duplicate line
2019-10-09 15:15:04 -04:00
Shicong Huang
a694e247cd Add support for nomulus tool to connect to Cloud SQL (#303) 2019-10-09 10:30:35 -04:00
Shicong Huang
022f397cd9 Implement ZonedDateTimeConverter (#287)
* Implement ZonedDateTimeConverter

* Use dedicated TestEntity for ZonedDateTimeConverterTest
2019-10-08 16:46:07 -04:00
Ben McIlwain
5dc058ec99 Automatically apply JPA type converters (#305)
* Automatically apply JPA type converters

* Include converters in tests and schema generation too
2019-10-08 13:16:39 -04:00
Ben McIlwain
0fd7cf29b5 Make it clear that registered domain lists are also exported to Drive (#304)
* Make it clear that registered domain lists are also exported to Drive
2019-10-08 13:15:55 -04:00
Ben McIlwain
bc7f3546c7 Add initial support for persisting premium lists to Cloud SQL (#285)
* Add initial support for persisting premium lists to Cloud SQL

This adds support to the `nomulus create_premium_list` command only; support for
`nomulus update_premium_list` will be in a subsequent PR.

The design goals for this PR were:
1. Do not change the existing codepaths for premium lists at all, especially not
   on the read path.
2. Write premium lists to Cloud SQL only if requested (i.e. not by default), and
   write to Datastore first so as to not be blocked by errors with Cloud SQL.
3. Reuse existing codepaths to the maximum possible extent (e.g. don't yet
   re-implement premium list parsing; take advantage of the existing logic), but
   also ...
4. Some duplication is OK, since the existing Datastore path will be deleted
   once this migration is complete, leaving only the codepaths for Cloud SQL.

* Refactor out common logic

* Add DAO test

* Add tests for parsing premium lists

* Use containsExactly

* Code review changes

* Format

* Re-generate schema

* Fix column names

* Make some tests pass

* Add SQL migration scripts

* Fix test errors
2019-10-08 11:47:22 -04:00
Ben McIlwain
658f61bd8f Fix errors in DB update README file (#301)
* Fix errors in DB update README file
2019-10-07 15:55:53 -04:00
Michael Muller
8ca8fff387 Refactor environment -> project map out of build (#300)
Move the project mapping into its own file so that it can be more easily
overriden.
2019-10-07 13:34:49 -04:00
Michael Muller
8bcfb1802e Add an explanation to dummied-out JPA init (#299)
* Add an explanation to dummied-out JPA init

Add a more elaborate explanation of why actual JpaTransactionManager
initialization was removed from the factory.
2019-10-07 12:47:24 -04:00
gbrodman
a259dee986 Add a DAO for RegistryLock objects (#290)
* Add a DAO for RegistryLock objects

* Add an index on verification code and remove old file

* Move to v4

* Use camelCase in index names

* Javadoc fixes

* Allow alteration of RegistryLock objects in-place

* save, load-modify, read in separate transactions

* Change the creation timestamp to be a CreateAutoTimestamp
2019-10-07 11:24:08 -04:00
Michael Muller
ebc8d54f94 Add persistence.xml to the war files (#293)
* Add persistence.xml to the war files
* Always use the DummyJpaTransactionManager

Use the DJTM until we get all of the dependencies set up for all of the
environments.

This shouldn't affect any of the unit tests, these use the
JpaTransactionManagerRule to set up a local database and connection.

This fixes the App Engine build.
2019-10-06 21:32:27 -04:00
sarahcaseybot
e9ce389269 Implement UpdateAutoTimestampConverter (#298)
* Implement a JPA-based converter for UpdateAutoTimestamp, allowing us to persist instances of this class.
2019-10-04 16:45:32 -04:00
Michael Muller
e07be9780e Implement CreateAutoTimestampConverter (#282)
* Implement CreateAutoTimestampConverter

Implement a JPA-based converter for CreateAutoTimestamp, allowing us to
persist instances of this class.

Note that converters appear to be required to convert to and from database
types that are generally known to JDBC.  For example, conversion to Timestamp
works, conversion to OffsetDateTime does not (even though this works through
the JDBC interface directly).
2019-10-04 16:31:14 -04:00
Michael Muller
31b5abb9a1 Improve error handling for environment property (#295)
* Improve error handling for environment property

Improve the error messages that we get for a bad or missing environment
property.  Move the property processing into the main build so that we do it
only once and configure all of the appengine deployment tasks to check that a
project has been defined and print a friendly error if it hasn't.

Note that even if the check isn't configured, this change prevents deployment
because the gcpProject will be set to null.  It just won't print as useful an
error message.

Tested: ran appengineDeployAll with and without the environment property, ran
"build" to verify that we don't get any complaints for non-deployment targets.

* Changes for review.

* Changes for review

* Changes for review.

* Changes for review.
2019-10-04 13:23:36 -04:00
Michael Muller
aa4e242a34 Give JpaTransactionManagerRule more parameters (#292)
* Give JpaTransactionManagerRule more parameters

Allow users of the rule to add annotated classes and properties, both useful
for testing.

* Change in response to review.

* Changes for review.

* Move test EntityManagerFactory create method

Move the test create method into the JpaTransactionManagerRuleTest.

* Remove nomulus SQL dialect from G.S.S.Command

Remove NomulusPostgreSQLDialect from GenerateSqlSchemaCommand (it has been
moved to its own top-level class).
2019-10-03 16:14:28 -04:00
Weimin Yu
964f264c9d Add maven-publish task for SQL schema jar (#289)
* Add maven-publish task for SQL schema jar

Add task to publish SQL schema jar with flyway scripts and
golden schema to a maven repo. This will be used
for pre-release testing in the future.

This task is not part of build and needs to be invoked explicitly.

User needs to provide schema_jar_repo and schema_version
properties.

* Merge branch 'master' of https://github.com/google/nomulus into publish-schema-jar

* Add maven-publish task for SQL schema jar

Add task to publish SQL schema jar with flyway scripts and
golden schema to a maven repo. This will be used
for pre-release testing in the future.

This task is not part of build and needs to be invoked explicitly.

User needs to provide schema_jar_repo and schema_version
properties.
2019-10-02 14:27:36 -04:00
gbrodman
cc018a6dac Don't crash on a null completion timestamp (#296)
* Don't crash on a null completion timestamp

* optional
2019-10-02 12:53:22 -04:00
Weimin Yu
c25adbbd9c Restrict nomulus user access to flyway table (#297)
* Restrict nomulus user access to flyway table

The regular read-write user should not have write permissions to
the flyway metadata table.
2019-10-02 11:05:46 -04:00
64 changed files with 2111 additions and 274 deletions

View File

@@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
apply from: "${rootDir.path}/projects.gradle"
apply plugin: 'war'
def environment = rootProject.environment
@@ -26,14 +25,19 @@ project.convention.plugins['war'].webAppDirName =
apply plugin: 'com.google.cloud.tools.appengine'
def coreResourcesDir = "${rootDir}/core/build/resources/main"
// Get the web.xml file for the service.
war {
webInf {
from "../../core/src/main/java/google/registry/env/common/${project.name}/WEB-INF"
from("${coreResourcesDir}/META-INF/persistence.xml") {
into "classes/META-INF"
}
}
}
def coreResourcesDir = "${rootDir}/core/build/resources/main"
war {
from("${coreResourcesDir}/google/registry/ui/html") {
include "*.html"
@@ -68,6 +72,10 @@ appengine {
if (!rootProject.prodOrSandboxEnv) {
version = 'GCLOUD_CONFIG'
}
// Don't set gcpProject directly, it gets overriden in ./build.gradle.
// Do -P environment={crash,alpha} instead. For sandbox/production,
// use Spinnaker.
projectId = gcpProject
}
}
@@ -92,4 +100,15 @@ explodeWar.doLast {
rootProject.deploy.dependsOn appengineDeployAll
rootProject.stage.dependsOn appengineStage
appengineDeployAll.dependsOn rootProject.verifyDeployment
// Impose verification for all of the deployment tasks. We haven't found a
// better way to do this other than to apply to each of them independently.
// If a new task gets added, it will still fail if "environment" is not defined
// because gcpProject is null. We just won't get as friendly an error message.
appengineDeployAll.configure rootProject.verifyDeploymentConfig
appengineDeploy.configure rootProject.verifyDeploymentConfig
appengineDeployCron.configure rootProject.verifyDeploymentConfig
appengineDeployDispatch.configure rootProject.verifyDeploymentConfig
appengineDeployDos.configure rootProject.verifyDeploymentConfig
appengineDeployIndex.configure rootProject.verifyDeploymentConfig
appengineDeployQueue.configure rootProject.verifyDeploymentConfig

View File

@@ -109,21 +109,61 @@ task deploy {
description = 'Deploys all services to App Engine.'
}
task verifyDeployment {
group = 'deployment'
description = 'Ensure that one cannot deploy to production or sandbox.'
doFirst {
if (rootProject.prodOrSandboxEnv) {
throw new GradleException("Cannot deploy to production or sandbox.");
}
}
}
task stage {
group = 'deployment'
description = 'Generates application directories for all services.'
}
// App-engine environment configuration. We set up all of the variables in
// the root project.
def environments = ['production', 'sandbox', 'alpha', 'crash']
def gcpProject = null
apply from: "${rootDir.path}/projects.gradle"
if (environment == '') {
// Keep the project null, this will prevent deployment. Set the
// environment to "alpha" because other code needs this property to
// explode the war file.
environment = 'alpha'
} else if (environment != 'production' && environment != 'sandbox') {
gcpProject = projects[environment]
if (gcpProject == null) {
throw new GradleException("-Penvironment must be one of " +
"${projects.keySet()}.")
}
}
rootProject.ext.environment = environment
rootProject.ext.gcpProject = gcpProject
rootProject.ext.prodOrSandboxEnv = environment in ['production', 'sandbox']
// Function to verify that the deployment parameters have been set.
def verifyDeploymentParams() {
if (prodOrSandboxEnv) {
// Do not deploy to prod or sandbox. Print a prominent error in bright red.
System.err.println('\033[31;1m-----------------------------------------------------------------')
System.err.println('*** DANGER WILL ROBINSON!')
System.err.println('*** You may not deploy to production or sandbox from gradle. Do a')
System.err.println('*** release from Spinnaker, see deployment playbook.')
System.err.println('-----------------------------------------------------------------')
throw new GradleException('Aborting. See prominent error above.')
} else if (gcpProject == null) {
def error = 'You must specify -P environment={alpha,crash}'
System.err.println("\033[33;1m${error}\033[0m")
throw GradleException("Aborting: ${error}")
}
}
// Closure that we can just drop into all of our deployment tasks.
rootProject.ext.verifyDeploymentConfig = {
doFirst { verifyDeploymentParams() }
}
// Subproject configuration.
allprojects {
// Skip no-op project
if (project.name == 'services') return

View File

@@ -558,10 +558,38 @@ artifacts {
testRuntime testJar
}
task fragileTest(type: Test) {
/**
* We have to break out the test suites because some of the tests conflict
* with one another, but unfortunately this breaks the "--tests" flag. The
* --tests flag only applies to the task named on the command line (usually
* just "test"), not for all tasks of type "Test".
*
* As a better solution, FilteringTest sets testNameIncludePatterns (the
* internal property that --tests sets) from the value of the "testFilter"
* property, allowing us to filter across all the tests in core without
* explicitly specifying a test task or causing errors because there are no
* matching tests in the main task.
*
* To use it, define "testFilter" to be a comma-separated collection of class
* names (wildcards are allowed):
*
* ./gradlew test -P testFilter=*.FooBar,google.registry.tools.ShellCommandTest
*/
class FilteringTest extends Test {
void setTests(List<String> tests) {
// Common exclude pattern. See README in parent directory for explanation.
exclude "**/*TestCase.*", "**/*TestSuite.*"
include tests
if (project.testFilter) {
testNameIncludePatterns = project.testFilter.split(',')
}
}
}
task fragileTest(type: FilteringTest) {
// Common exclude pattern. See README in parent directory for explanation.
exclude "**/*TestCase.*", "**/*TestSuite.*"
include fragileTestPatterns
tests = fragileTestPatterns
if (rootProject.findProperty("skipDockerIncompatibleTests") == "true") {
exclude dockerIncompatibleTestPatterns
@@ -571,10 +599,8 @@ task fragileTest(type: Test) {
forkEvery 1
}
task outcastTest(type: Test) {
// Common exclude pattern. See README in parent directory for explanation.
exclude "**/*TestCase.*", "**/*TestSuite.*"
include outcastTestPatterns
task outcastTest(type: FilteringTest) {
tests = outcastTestPatterns
// Sets the maximum number of test executors that may exist at the same time.
maxParallelForks 5
@@ -630,10 +656,8 @@ task registryTool(type: JavaExec) {
}
}
task generateGoldenImages(type: Test) {
// Common exclude pattern. See README in parent directory for explanation.
exclude "**/*TestCase.*", "**/*TestSuite.*"
include "**/webdriver/*"
task generateGoldenImages(type: FilteringTest) {
tests = ["**/webdriver/*"]
// Sets the maximum number of test executors that may exist at the same time.
maxParallelForks 5

View File

@@ -144,7 +144,7 @@
<cron>
<url><![CDATA[/_dr/cron/fanout?queue=retryable-cron-tasks&endpoint=/_dr/task/exportDomainLists&runInEmpty]]></url>
<description>
This job exports lists of all active domain names to Google Cloud Storage.
This job exports lists of all active domain names to Google Drive and Google Cloud Storage.
</description>
<schedule>every 12 hours synchronized</schedule>
<target>backend</target>

View File

@@ -112,7 +112,7 @@
<cron>
<url><![CDATA[/_dr/cron/fanout?queue=retryable-cron-tasks&endpoint=/_dr/task/exportDomainLists&runInEmpty]]></url>
<description>
This job exports lists of all active domain names to Google Cloud Storage.
This job exports lists of all active domain names to Google Drive and Google Cloud Storage.
</description>
<schedule>every 12 hours synchronized</schedule>
<target>backend</target>

View File

@@ -56,7 +56,7 @@ import javax.inject.Inject;
import org.joda.time.DateTime;
/**
* A mapreduce that exports the list of active domains on all real TLDs to Google Cloud Storage.
* A mapreduce that exports the list of active domains on all real TLDs to Google Drive and GCS.
*
* <p>Each TLD's active domain names are exported as a newline-delimited flat text file with the
* name TLD.txt into the domain-lists bucket. Note that this overwrites the files in place.

View File

@@ -0,0 +1,52 @@
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.registry;
import static com.google.common.base.Preconditions.checkNotNull;
import static google.registry.model.transaction.TransactionManagerFactory.jpaTm;
import google.registry.schema.domain.RegistryLock;
import javax.persistence.EntityManager;
/** Data access object for {@link google.registry.schema.domain.RegistryLock}. */
public final class RegistryLockDao {
/**
* Returns the most recent version of the {@link RegistryLock} referred to by the verification
* code (there may be two instances of the same code in the database--one after lock object
* creation and one after verification.
*/
public static RegistryLock getByVerificationCode(String verificationCode) {
return jpaTm()
.transact(
() -> {
EntityManager em = jpaTm().getEntityManager();
Long revisionId =
em.createQuery(
"SELECT MAX(revisionId) FROM RegistryLock WHERE verificationCode ="
+ " :verificationCode",
Long.class)
.setParameter("verificationCode", verificationCode)
.getSingleResult();
checkNotNull(revisionId, "No registry lock with this code");
return em.find(RegistryLock.class, revisionId);
});
}
public static void save(RegistryLock registryLock) {
checkNotNull(registryLock, "Null registry lock cannot be saved");
jpaTm().transact(() -> jpaTm().getEntityManager().persist(registryLock));
}
}

View File

@@ -0,0 +1,72 @@
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.tmch;
import static google.registry.model.transaction.TransactionManagerFactory.jpaTm;
import com.google.common.flogger.FluentLogger;
import google.registry.schema.tmch.ClaimsList;
import javax.persistence.EntityManager;
/** Data access object for {@link ClaimsList}. */
public class ClaimsListDao {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static void save(ClaimsList claimsList) {
jpaTm().transact(() -> jpaTm().getEntityManager().persist(claimsList));
}
/**
* Try to save the given {@link ClaimsList} into Cloud SQL. If the save fails, the error will be
* logged but no exception will be thrown.
*
* <p>This method is used during the dual-write phase of database migration as Datastore is still
* the authoritative database.
*/
public static void trySave(ClaimsList claimsList) {
try {
ClaimsListDao.save(claimsList);
logger.atInfo().log(
"Inserted %,d claims into Cloud SQL, created at %s",
claimsList.getLabelsToKeys().size(), claimsList.getTmdbGenerationTime());
} catch (Throwable e) {
logger.atSevere().withCause(e).log("Error inserting claims into Cloud SQL");
}
}
/**
* Returns the current revision of the {@link ClaimsList} in Cloud SQL. Throws exception if there
* is no claims in the table.
*/
public static ClaimsList getCurrent() {
return jpaTm()
.transact(
() -> {
EntityManager em = jpaTm().getEntityManager();
Long revisionId =
em.createQuery("SELECT MAX(revisionId) FROM ClaimsList", Long.class)
.getSingleResult();
return em.createQuery(
"FROM ClaimsList cl LEFT JOIN FETCH cl.labelsToKeys WHERE cl.revisionId ="
+ " :revisionId",
ClaimsList.class)
.setParameter("revisionId", revisionId)
.getSingleResult();
});
}
private ClaimsListDao() {}
}

View File

@@ -217,8 +217,7 @@ public class ClaimsListShard extends ImmutableObject {
});
}
public static ClaimsListShard create(
DateTime creationTime, ImmutableMap<String, String> labelsToKeys) {
public static ClaimsListShard create(DateTime creationTime, Map<String, String> labelsToKeys) {
ClaimsListShard instance = new ClaimsListShard();
instance.id = allocateId();
instance.creationTime = checkNotNull(creationTime);

View File

@@ -15,10 +15,7 @@
package google.registry.model.transaction;
import com.google.common.flogger.FluentLogger;
import google.registry.persistence.PersistenceModule.AppEngineEmf;
import google.registry.util.Clock;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
@@ -26,7 +23,6 @@ import javax.persistence.PersistenceException;
import org.joda.time.DateTime;
/** Implementation of {@link JpaTransactionManager} for JPA compatible database. */
@Singleton
public class JpaTransactionManagerImpl implements JpaTransactionManager {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -39,8 +35,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
private final ThreadLocal<TransactionInfo> transactionInfo =
ThreadLocal.withInitial(TransactionInfo::new);
@Inject
JpaTransactionManagerImpl(@AppEngineEmf EntityManagerFactory emf, Clock clock) {
public JpaTransactionManagerImpl(EntityManagerFactory emf, Clock clock) {
this.emf = emf;
this.clock = clock;
}

View File

@@ -14,11 +14,8 @@
package google.registry.model.transaction;
import com.google.appengine.api.utils.SystemProperty;
import com.google.appengine.api.utils.SystemProperty.Environment.Value;
import com.google.common.annotations.VisibleForTesting;
import google.registry.model.ofy.DatastoreTransactionManager;
import google.registry.persistence.DaggerPersistenceComponent;
/** Factory class to create {@link TransactionManager} instance. */
// TODO: Rename this to PersistenceFactory and move to persistence package.
@@ -30,11 +27,19 @@ public class TransactionManagerFactory {
private TransactionManagerFactory() {}
private static JpaTransactionManager createJpaTransactionManager() {
if (SystemProperty.environment.value() == Value.Production) {
return DaggerPersistenceComponent.create().jpaTransactionManager();
} else {
return DummyJpaTransactionManager.create();
}
// TODO(shicong): There is currently no environment where we want to create a real JPA
// transaction manager here. The unit tests that require one are all set up using
// JpaTransactionManagerRule which launches its own PostgreSQL instance. When we actually have
// PostgreSQL tables in production, ensure that all of the test environments are set up
// correctly and restore the code that creates a JpaTransactionManager when
// RegistryEnvironment.get() != UNITTEST.
//
// We removed the original code because it didn't work in sandbox (due to the absence of the
// persistence.xml file, which has since been added), and then (after adding this) didn't work
// in crash because the postgresql password hadn't been set up. Prior to restoring, we'll need
// to do setup in all environments, and we probably only want to do this once we're actually
// using Cloud SQL for one of the new tables.
return DummyJpaTransactionManager.create();
}
private static TransactionManager createTransactionManager() {
@@ -44,6 +49,15 @@ public class TransactionManagerFactory {
return new DatastoreTransactionManager(null);
}
/**
* Sets jpaTm to the implementation for Nomulus tool. Note that this method should be only used by
* {@link google.registry.tools.RegistryCli} to initialize jpaTm.
*/
public static void initForTool() {
// TODO(shicong): Uncomment the line below when we set up Cloud SQL instance in all environments
// jpaTm = DaggerPersistenceComponent.create().nomulusToolJpaTransactionManager();
}
/** Returns {@link TransactionManager} instance. */
public static TransactionManager tm() {
return TM;
@@ -51,11 +65,6 @@ public class TransactionManagerFactory {
/** Returns {@link JpaTransactionManager} instance. */
public static JpaTransactionManager jpaTm() {
// TODO: Returns corresponding TransactionManager based on the runtime environment.
// We have 3 kinds of runtime environment:
// 1. App Engine
// 2. Local JVM used by nomulus tool
// 3. Unit test
return jpaTm;
}
}

View File

@@ -0,0 +1,59 @@
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.persistence;
import static com.google.common.base.Charsets.US_ASCII;
import static com.google.common.hash.Funnels.stringFunnel;
import com.google.common.hash.BloomFilter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import javax.annotation.Nullable;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
/** JPA converter for ASCII String {@link BloomFilter}s. */
@Converter(autoApply = true)
public class BloomFilterConverter implements AttributeConverter<BloomFilter<String>, byte[]> {
@Override
@Nullable
public byte[] convertToDatabaseColumn(@Nullable BloomFilter<String> entity) {
if (entity == null) {
return null;
}
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
entity.writeTo(bos);
} catch (IOException e) {
throw new UncheckedIOException("Error saving Bloom filter data", e);
}
return bos.toByteArray();
}
@Override
@Nullable
public BloomFilter<String> convertToEntityAttribute(@Nullable byte[] columnValue) {
if (columnValue == null) {
return null;
}
try {
return BloomFilter.readFrom(new ByteArrayInputStream(columnValue), stringFunnel(US_ASCII));
} catch (IOException e) {
throw new UncheckedIOException("Error loading Bloom filter data", e);
}
}
}

View File

@@ -0,0 +1,49 @@
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.persistence;
import static com.google.common.base.MoreObjects.firstNonNull;
import static google.registry.model.transaction.TransactionManagerFactory.jpaTm;
import google.registry.model.CreateAutoTimestamp;
import google.registry.util.DateTimeUtils;
import java.sql.Timestamp;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import javax.annotation.Nullable;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import org.joda.time.DateTime;
/** JPA converter to for storing/retrieving CreateAutoTimestamp objects. */
@Converter(autoApply = true)
public class CreateAutoTimestampConverter
implements AttributeConverter<CreateAutoTimestamp, Timestamp> {
@Override
public Timestamp convertToDatabaseColumn(CreateAutoTimestamp entity) {
DateTime dateTime = firstNonNull(entity.getTimestamp(), jpaTm().getTransactionTime());
return Timestamp.from(DateTimeUtils.toZonedDateTime(dateTime).toInstant());
}
@Override
@Nullable
public CreateAutoTimestamp convertToEntityAttribute(@Nullable Timestamp columnValue) {
if (columnValue == null) {
return null;
}
ZonedDateTime zdt = ZonedDateTime.ofInstant(columnValue.toInstant(), ZoneOffset.UTC);
return CreateAutoTimestamp.create(DateTimeUtils.toJodaDateTime(zdt));
}
}

View File

@@ -0,0 +1,27 @@
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.persistence;
import java.sql.Types;
import org.hibernate.dialect.PostgreSQL95Dialect;
/** Nomulus mapping rules for column types in Postgresql. */
public class NomulusPostgreSQLDialect extends PostgreSQL95Dialect {
public NomulusPostgreSQLDialect() {
super();
registerColumnType(Types.VARCHAR, "text");
registerColumnType(Types.TIMESTAMP_WITH_TIMEZONE, "timestamptz");
registerColumnType(Types.TIMESTAMP, "timestamptz");
}
}

View File

@@ -18,7 +18,9 @@ import dagger.Component;
import google.registry.config.CredentialModule;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.keyring.kms.KmsModule;
import google.registry.model.transaction.JpaTransactionManagerImpl;
import google.registry.model.transaction.JpaTransactionManager;
import google.registry.persistence.PersistenceModule.AppEngineJpaTm;
import google.registry.persistence.PersistenceModule.NomulusToolJpaTm;
import google.registry.util.UtilsModule;
import javax.inject.Singleton;
import javax.persistence.EntityManagerFactory;
@@ -34,5 +36,10 @@ import javax.persistence.EntityManagerFactory;
UtilsModule.class
})
public interface PersistenceComponent {
JpaTransactionManagerImpl jpaTransactionManager();
@AppEngineJpaTm
JpaTransactionManager appEngineJpaTransactionManager();
@NomulusToolJpaTm
JpaTransactionManager nomulusToolJpaTransactionManager();
}

View File

@@ -29,11 +29,14 @@ import dagger.Module;
import dagger.Provides;
import google.registry.config.RegistryConfig.Config;
import google.registry.keyring.kms.KmsKeyring;
import google.registry.model.transaction.JpaTransactionManager;
import google.registry.model.transaction.JpaTransactionManagerImpl;
import google.registry.util.Clock;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashMap;
import java.util.Map;
import javax.inject.Qualifier;
import javax.inject.Singleton;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import org.hibernate.cfg.Environment;
@@ -61,6 +64,8 @@ public class PersistenceModule {
// SessionFactory is created. Setting it to 'none' to turn off the feature.
properties.put(Environment.HBM2DDL_AUTO, "none");
// Hibernate converts any date to this timezone when writing to the database.
properties.put(Environment.JDBC_TIME_ZONE, "UTC");
properties.put(
Environment.PHYSICAL_NAMING_STRATEGY, NomulusNamingStrategy.class.getCanonicalName());
@@ -70,28 +75,60 @@ public class PersistenceModule {
properties.put(HIKARI_MINIMUM_IDLE, getHibernateHikariMinimumIdle());
properties.put(HIKARI_MAXIMUM_POOL_SIZE, getHibernateHikariMaximumPoolSize());
properties.put(HIKARI_IDLE_TIMEOUT, getHibernateHikariIdleTimeout());
properties.put(Environment.DIALECT, NomulusPostgreSQLDialect.class.getName());
return properties.build();
}
@Provides
@AppEngineEmf
public static EntityManagerFactory providesAppEngineEntityManagerFactory(
@Singleton
@AppEngineJpaTm
public static JpaTransactionManager providesAppEngineJpaTm(
@Config("cloudSqlJdbcUrl") String jdbcUrl,
@Config("cloudSqlUsername") String username,
@Config("cloudSqlInstanceConnectionName") String instanceConnectionName,
KmsKeyring kmsKeyring,
@DefaultHibernateConfigs ImmutableMap<String, String> defaultConfigs) {
String password = kmsKeyring.getCloudSqlPassword();
@DefaultHibernateConfigs ImmutableMap<String, String> defaultConfigs,
Clock clock) {
HashMap<String, String> overrides = Maps.newHashMap(defaultConfigs);
// For Java users, the Cloud SQL JDBC Socket Factory can provide authenticated connections.
// See https://github.com/GoogleCloudPlatform/cloud-sql-jdbc-socket-factory for details.
overrides.put("socketFactory", "com.google.cloud.sql.postgres.SocketFactory");
overrides.put("cloudSqlInstance", instanceConnectionName);
EntityManagerFactory emf = create(jdbcUrl, username, password, ImmutableMap.copyOf(overrides));
Runtime.getRuntime().addShutdownHook(new Thread(emf::close));
return emf;
overrides.put(Environment.URL, jdbcUrl);
overrides.put(Environment.USER, username);
overrides.put(Environment.PASS, kmsKeyring.getCloudSqlPassword());
return new JpaTransactionManagerImpl(create(overrides), clock);
}
@Provides
@Singleton
@NomulusToolJpaTm
public static JpaTransactionManager providesNomulusToolJpaTm(
@Config("toolsCloudSqlJdbcUrl") String jdbcUrl,
@Config("toolsCloudSqlUsername") String username,
@Config("cloudSqlInstanceConnectionName") String instanceConnectionName,
KmsKeyring kmsKeyring,
@DefaultHibernateConfigs ImmutableMap<String, String> defaultConfigs,
Clock clock) {
// Cloud SQL JDBC Socket Factory library requires the jdbc url to include all connection
// information, otherwise the connection initialization will fail. See here for more details:
// https://github.com/GoogleCloudPlatform/cloud-sql-jdbc-socket-factory
String fullJdbcUrl =
new StringBuilder(jdbcUrl)
.append("?cloudSqlInstance=" + instanceConnectionName)
.append("&socketFactory=com.google.cloud.sql.postgres.SocketFactory")
.append("&user=" + username)
.append("&password=" + kmsKeyring.getCloudSqlPassword())
.toString();
HashMap<String, String> overrides = Maps.newHashMap(defaultConfigs);
overrides.put(Environment.URL, fullJdbcUrl);
return new JpaTransactionManagerImpl(create(overrides), clock);
}
/** Constructs the {@link EntityManagerFactory} instance. */
@@ -102,25 +139,37 @@ public class PersistenceModule {
properties.put(Environment.URL, jdbcUrl);
properties.put(Environment.USER, username);
properties.put(Environment.PASS, password);
return create(ImmutableMap.copyOf(properties));
}
private static EntityManagerFactory create(Map<String, String> properties) {
// If there are no annotated classes, we can create the EntityManagerFactory from the generic
// method. Otherwise we have to use a more tailored approach. Note that this adds to the set
// of annotated classes defined in the configuration, it does not override them.
EntityManagerFactory emf =
Persistence.createEntityManagerFactory(PERSISTENCE_UNIT_NAME, properties);
Persistence.createEntityManagerFactory(
PERSISTENCE_UNIT_NAME, ImmutableMap.copyOf(properties));
checkState(
emf != null,
"Persistence.createEntityManagerFactory() returns a null EntityManagerFactory");
return emf;
}
/** Dagger qualifier for the {@link EntityManagerFactory} used for App Engine application. */
/** Dagger qualifier for {@link JpaTransactionManager} used for App Engine application. */
@Qualifier
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface AppEngineEmf {}
@interface AppEngineJpaTm {}
/** Dagger qualifier for {@link JpaTransactionManager} used for Nomulus tool. */
@Qualifier
@Documented
@interface NomulusToolJpaTm {}
/** Dagger qualifier for the default Hibernate configurations. */
// TODO(shicong): Change annotations in this class to none public or put them in a top level
// package
@Qualifier
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface DefaultHibernateConfigs {}
}

View File

@@ -0,0 +1,46 @@
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.persistence;
import static google.registry.model.transaction.TransactionManagerFactory.jpaTm;
import google.registry.model.UpdateAutoTimestamp;
import google.registry.util.DateTimeUtils;
import java.sql.Timestamp;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import javax.annotation.Nullable;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
/** JPA converter for storing/retrieving UpdateAutoTimestamp objects. */
@Converter(autoApply = true)
public class UpdateAutoTimestampConverter
implements AttributeConverter<UpdateAutoTimestamp, Timestamp> {
@Override
public Timestamp convertToDatabaseColumn(UpdateAutoTimestamp entity) {
return Timestamp.from(DateTimeUtils.toZonedDateTime(jpaTm().getTransactionTime()).toInstant());
}
@Override
@Nullable
public UpdateAutoTimestamp convertToEntityAttribute(@Nullable Timestamp columnValue) {
if (columnValue == null) {
return null;
}
ZonedDateTime zdt = ZonedDateTime.ofInstant(columnValue.toInstant(), ZoneOffset.UTC);
return UpdateAutoTimestamp.create(DateTimeUtils.toJodaDateTime(zdt));
}
}

View File

@@ -0,0 +1,48 @@
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.persistence;
import java.sql.Timestamp;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import javax.annotation.Nullable;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
/**
* JPA converter to for storing/retrieving {@link ZonedDateTime} objects.
*
* <p>Hibernate provides a default converter for {@link ZonedDateTime}, but it converts timestamp to
* a non-normalized format, e.g., 2019-09-01T01:01:01Z will be converted to
* 2019-09-01T01:01:01Z[UTC]. This converter solves that problem by explicitly calling {@link
* ZoneId#normalized()} to normalize the zone id.
*/
@Converter(autoApply = true)
public class ZonedDateTimeConverter implements AttributeConverter<ZonedDateTime, Timestamp> {
@Override
@Nullable
public Timestamp convertToDatabaseColumn(@Nullable ZonedDateTime attribute) {
return attribute == null ? null : Timestamp.from(attribute.toInstant());
}
@Override
@Nullable
public ZonedDateTime convertToEntityAttribute(@Nullable Timestamp dbData) {
return dbData == null
? null
: ZonedDateTime.ofInstant(dbData.toInstant(), ZoneId.of("UTC").normalized());
}
}

View File

@@ -15,13 +15,15 @@
package google.registry.schema.domain;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.util.DateTimeUtils.toJodaDateTime;
import static google.registry.util.DateTimeUtils.toZonedDateTime;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import google.registry.model.Buildable;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.ImmutableObject;
import google.registry.util.DateTimeUtils;
import java.time.ZonedDateTime;
import java.util.Optional;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
@@ -54,14 +56,17 @@ import org.joda.time.DateTime;
* Unique constraint to get around Hibernate's failure to handle auto-increment field in
* composite primary key.
*
* <p>Note: because of this index, physical columns must be declared in the {@link Column}
* annotations for {@link RegistryLock#revisionId} and {@link RegistryLock#repoId} fields.
* <p>Note: indexes use the camelCase version of the field names because the {@link
* google.registry.persistence.NomulusNamingStrategy} does not translate the field name into the
* snake_case column name until the write itself.
*/
indexes =
@Index(
name = "idx_registry_lock_repo_id_revision_id",
columnList = "repo_id, revision_id",
unique = true))
indexes = {
@Index(
name = "idx_registry_lock_repo_id_revision_id",
columnList = "repoId, revisionId",
unique = true),
@Index(name = "idx_registry_lock_verification_code", columnList = "verificationCode")
})
public final class RegistryLock extends ImmutableObject implements Buildable {
/** Describes the action taken by the user. */
@@ -72,11 +77,11 @@ public final class RegistryLock extends ImmutableObject implements Buildable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "revision_id", nullable = false)
@Column(nullable = false)
private Long revisionId;
/** EPP repo ID of the domain in question. */
@Column(name = "repo_id", nullable = false)
@Column(nullable = false)
private String repoId;
// TODO (b/140568328): remove this when everything is in Cloud SQL and we can join on "domain"
@@ -102,7 +107,7 @@ public final class RegistryLock extends ImmutableObject implements Buildable {
/** Creation timestamp is when the lock/unlock is first requested. */
@Column(nullable = false)
private ZonedDateTime creationTimestamp;
private CreateAutoTimestamp creationTimestamp = CreateAutoTimestamp.create(null);
/**
* Completion timestamp is when the user has verified the lock/unlock, when this object de facto
@@ -146,11 +151,12 @@ public final class RegistryLock extends ImmutableObject implements Buildable {
}
public DateTime getCreationTimestamp() {
return toJodaDateTime(creationTimestamp);
return creationTimestamp.getTimestamp();
}
public DateTime getCompletionTimestamp() {
return toJodaDateTime(completionTimestamp);
/** Returns the completion timestamp, or empty if this lock has not been completed yet. */
public Optional<DateTime> getCompletionTimestamp() {
return Optional.ofNullable(completionTimestamp).map(DateTimeUtils::toJodaDateTime);
}
public String getVerificationCode() {
@@ -165,9 +171,16 @@ public final class RegistryLock extends ImmutableObject implements Buildable {
return revisionId;
}
public void setCompletionTimestamp(DateTime dateTime) {
this.completionTimestamp = toZonedDateTime(dateTime);
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
RegistryLock clone = clone(this);
// Revision ID should be different for every object
clone.revisionId = null;
return new Builder(clone);
}
/** Builder for {@link google.registry.schema.domain.RegistryLock}. */
@@ -184,7 +197,6 @@ public final class RegistryLock extends ImmutableObject implements Buildable {
checkArgumentNotNull(getInstance().domainName, "Domain name cannot be null");
checkArgumentNotNull(getInstance().registrarId, "Registrar ID cannot be null");
checkArgumentNotNull(getInstance().action, "Action cannot be null");
checkArgumentNotNull(getInstance().creationTimestamp, "Creation timestamp cannot be null");
checkArgumentNotNull(getInstance().verificationCode, "Verification codecannot be null");
checkArgument(
getInstance().registrarPocId != null || getInstance().isSuperuser,
@@ -217,8 +229,8 @@ public final class RegistryLock extends ImmutableObject implements Buildable {
return this;
}
public Builder setCreationTimestamp(DateTime creationTimestamp) {
getInstance().creationTimestamp = toZonedDateTime(creationTimestamp);
public Builder setCreationTimestamp(CreateAutoTimestamp creationTimestamp) {
getInstance().creationTimestamp = creationTimestamp;
return this;
}

View File

@@ -14,10 +14,14 @@
package google.registry.schema.tld;
import static com.google.common.base.Charsets.US_ASCII;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.hash.Funnels.stringFunnel;
import com.google.common.collect.ImmutableMap;
import com.google.common.hash.BloomFilter;
import google.registry.model.CreateAutoTimestamp;
import java.math.BigDecimal;
import java.time.ZonedDateTime;
import java.util.Map;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
@@ -26,10 +30,12 @@ import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.JoinColumn;
import javax.persistence.MapKeyColumn;
import javax.persistence.Table;
import org.joda.money.CurrencyUnit;
import org.joda.time.DateTime;
/**
* A list of premium prices for domain names.
@@ -40,47 +46,55 @@ import org.joda.money.CurrencyUnit;
* This is fine though, because we only use the list with the highest revisionId.
*/
@Entity
@Table(name = "PremiumList")
@Table(indexes = {@Index(columnList = "name", name = "premiumlist_name_idx")})
public class PremiumList {
@Column(nullable = false)
private String name;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "revision_id")
@Column(nullable = false)
private Long revisionId;
@Column(name = "creation_timestamp", nullable = false)
private ZonedDateTime creationTimestamp;
@Column(nullable = false)
private CreateAutoTimestamp creationTimestamp = CreateAutoTimestamp.create(null);
@Column(name = "currency", nullable = false)
@Column(nullable = false)
private CurrencyUnit currency;
@ElementCollection
@CollectionTable(
name = "PremiumEntry",
joinColumns = @JoinColumn(name = "revision_id", referencedColumnName = "revision_id"))
@MapKeyColumn(name = "domain_label")
joinColumns = @JoinColumn(name = "revisionId", referencedColumnName = "revisionId"))
@MapKeyColumn(name = "domainLabel")
@Column(name = "price", nullable = false)
private Map<String, BigDecimal> labelsToPrices;
private PremiumList(
ZonedDateTime creationTimestamp,
CurrencyUnit currency,
Map<String, BigDecimal> labelsToPrices) {
this.creationTimestamp = creationTimestamp;
@Column(nullable = false)
private BloomFilter<String> bloomFilter;
private PremiumList(String name, CurrencyUnit currency, Map<String, BigDecimal> labelsToPrices) {
this.name = name;
this.currency = currency;
this.labelsToPrices = labelsToPrices;
// ASCII is used for the charset because all premium list domain labels are stored punycoded.
this.bloomFilter = BloomFilter.create(stringFunnel(US_ASCII), labelsToPrices.size());
labelsToPrices.keySet().forEach(this.bloomFilter::put);
}
// Hibernate requires this default constructor.
private PremiumList() {}
// TODO(mcilwain): Change creationTimestamp to Joda DateTime.
/** Constructs a {@link PremiumList} object. */
public static PremiumList create(
ZonedDateTime creationTimestamp,
CurrencyUnit currency,
Map<String, BigDecimal> labelsToPrices) {
return new PremiumList(creationTimestamp, currency, labelsToPrices);
String name, CurrencyUnit currency, Map<String, BigDecimal> labelsToPrices) {
return new PremiumList(name, currency, labelsToPrices);
}
/** Returns the name of the premium list, which is usually also a TLD string. */
public String getName() {
return name;
}
/** Returns the ID of this revision, or throws if null. */
@@ -91,12 +105,23 @@ public class PremiumList {
}
/** Returns the creation time of this revision of the premium list. */
public ZonedDateTime getCreationTimestamp() {
return creationTimestamp;
public DateTime getCreationTimestamp() {
return creationTimestamp.getTimestamp();
}
/** Returns a {@link Map} of domain labels to prices. */
public Map<String, BigDecimal> getLabelsToPrices() {
return labelsToPrices;
public ImmutableMap<String, BigDecimal> getLabelsToPrices() {
return ImmutableMap.copyOf(labelsToPrices);
}
/**
* Returns a Bloom filter to determine whether a label might be premium, or is definitely not.
*
* <p>If the domain label might be premium, then the next step is to check for the existence of a
* corresponding row in the PremiumListEntry table. Otherwise, we know for sure it's not premium,
* and no DB load is required.
*/
public BloomFilter<String> getBloomFilter() {
return bloomFilter;
}
}

View File

@@ -0,0 +1,56 @@
// Copyright 2019 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.schema.tld;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.model.transaction.TransactionManagerFactory.jpaTm;
/** Data access object class for {@link PremiumList}. */
public class PremiumListDao {
/** Persist a new premium list to Cloud SQL. */
public static void saveNew(PremiumList premiumList) {
jpaTm()
.transact(
() -> {
checkArgument(
!checkExists(premiumList.getName()),
"A premium list of this name already exists: %s.",
premiumList.getName());
jpaTm().getEntityManager().persist(premiumList);
});
}
/**
* Returns whether the premium list of the given name exists.
*
* <p>This means that at least one premium list revision must exist for the given name.
*/
public static boolean checkExists(String premiumListName) {
return jpaTm()
.transact(
() ->
jpaTm()
.getEntityManager()
.createQuery("SELECT 1 FROM PremiumList WHERE name = :name", Integer.class)
.setParameter("name", premiumListName)
.setMaxResults(1)
.getResultList()
.size()
> 0);
}
private PremiumListDao() {}
}

View File

@@ -15,7 +15,11 @@
package google.registry.schema.tmch;
import static com.google.common.base.Preconditions.checkState;
import static google.registry.util.DateTimeUtils.toJodaDateTime;
import static google.registry.util.DateTimeUtils.toZonedDateTime;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.ImmutableObject;
import java.time.ZonedDateTime;
import java.util.Map;
import java.util.Optional;
@@ -29,6 +33,7 @@ import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.MapKeyColumn;
import javax.persistence.Table;
import org.joda.time.DateTime;
/**
* A list of TMCH claims labels and their associated claims keys.
@@ -40,26 +45,29 @@ import javax.persistence.Table;
* highest {@link #revisionId}.
*/
@Entity
@Table(name = "ClaimsList")
public class ClaimsList {
@Table
public class ClaimsList extends ImmutableObject {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "revision_id")
@Column
private Long revisionId;
@Column(name = "creation_timestamp", nullable = false)
private ZonedDateTime creationTimestamp;
@Column(nullable = false)
private CreateAutoTimestamp creationTimestamp = CreateAutoTimestamp.create(null);
@Column(nullable = false)
private ZonedDateTime tmdbGenerationTime;
@ElementCollection
@CollectionTable(
name = "ClaimsEntry",
joinColumns = @JoinColumn(name = "revision_id", referencedColumnName = "revision_id"))
@MapKeyColumn(name = "domain_label", nullable = false)
@Column(name = "claim_key", nullable = false)
joinColumns = @JoinColumn(name = "revisionId", referencedColumnName = "revisionId"))
@MapKeyColumn(name = "domainLabel", nullable = false)
@Column(name = "claimKey", nullable = false)
private Map<String, String> labelsToKeys;
private ClaimsList(ZonedDateTime creationTimestamp, Map<String, String> labelsToKeys) {
this.creationTimestamp = creationTimestamp;
private ClaimsList(ZonedDateTime tmdbGenerationTime, Map<String, String> labelsToKeys) {
this.tmdbGenerationTime = tmdbGenerationTime;
this.labelsToKeys = labelsToKeys;
}
@@ -67,9 +75,8 @@ public class ClaimsList {
private ClaimsList() {}
/** Constructs a {@link ClaimsList} object. */
public static ClaimsList create(
ZonedDateTime creationTimestamp, Map<String, String> labelsToKeys) {
return new ClaimsList(creationTimestamp, labelsToKeys);
public static ClaimsList create(DateTime creationTimestamp, Map<String, String> labelsToKeys) {
return new ClaimsList(toZonedDateTime(creationTimestamp), labelsToKeys);
}
/** Returns the revision id of this claims list, or throws exception if it is null. */
@@ -79,9 +86,14 @@ public class ClaimsList {
return revisionId;
}
/** Returns the TMDB generation time of this claims list. */
public DateTime getTmdbGenerationTime() {
return toJodaDateTime(tmdbGenerationTime);
}
/** Returns the creation time of this claims list. */
public ZonedDateTime getCreationTimestamp() {
return creationTimestamp;
public DateTime getCreationTimestamp() {
return creationTimestamp.getTimestamp();
}
/** Returns an {@link Map} mapping domain label to its lookup key. */

View File

@@ -18,7 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import google.registry.model.tmch.ClaimsListShard;
import google.registry.schema.tmch.ClaimsList;
import java.util.List;
import org.joda.time.DateTime;
@@ -34,11 +34,11 @@ import org.joda.time.DateTime;
public class ClaimsListParser {
/**
* Converts the lines from the DNL CSV file into a {@link ClaimsListShard} object.
* Converts the lines from the DNL CSV file into a {@link ClaimsList} object.
*
* <p>Please note that this does <b>not</b> insert the object into Datastore.
*/
public static ClaimsListShard parse(List<String> lines) {
public static ClaimsList parse(List<String> lines) {
ImmutableMap.Builder<String, String> builder = new ImmutableMap.Builder<>();
// First line: <version>,<DNL List creation datetime>
@@ -74,6 +74,6 @@ public class ClaimsListParser {
builder.put(label, lookupKey);
}
return ClaimsListShard.create(creationTime, builder.build());
return ClaimsList.create(creationTime, builder.build());
}
}

View File

@@ -18,9 +18,11 @@ import static google.registry.request.Action.Method.POST;
import com.google.common.flogger.FluentLogger;
import google.registry.keyring.api.KeyModule.Key;
import google.registry.model.tmch.ClaimsListDao;
import google.registry.model.tmch.ClaimsListShard;
import google.registry.request.Action;
import google.registry.request.auth.Auth;
import google.registry.schema.tmch.ClaimsList;
import java.io.IOException;
import java.security.SignatureException;
import java.util.List;
@@ -54,10 +56,14 @@ public final class TmchDnlAction implements Runnable {
} catch (SignatureException | IOException | PGPException e) {
throw new RuntimeException(e);
}
ClaimsListShard claims = ClaimsListParser.parse(lines);
claims.save();
ClaimsList claims = ClaimsListParser.parse(lines);
ClaimsListShard claimsListShard =
ClaimsListShard.create(claims.getTmdbGenerationTime(), claims.getLabelsToKeys());
claimsListShard.save();
logger.atInfo().log(
"Inserted %,d claims into Datastore, created at %s",
claims.size(), claims.getCreationTime());
claimsListShard.size(), claimsListShard.getCreationTime());
ClaimsListDao.trySave(claims);
}
}

View File

@@ -0,0 +1,23 @@
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.tools;
/**
* Marker interface for commands that use Cloud Sql.
*
* <p>Just implementing this is sufficient to use Cloud Sql; {@link RegistryTool} will install it as
* needed.
*/
interface CommandWithCloudSql extends CommandWithRemoteApi {}

View File

@@ -16,6 +16,7 @@ package google.registry.tools;
import static com.google.common.base.Strings.isNullOrEmpty;
import static google.registry.security.JsonHttp.JSON_SAFETY_PREFIX;
import static google.registry.tools.server.CreateOrUpdatePremiumListAction.ALSO_CLOUD_SQL_PARAM;
import static google.registry.tools.server.CreateOrUpdatePremiumListAction.INPUT_PARAM;
import static google.registry.tools.server.CreateOrUpdatePremiumListAction.NAME_PARAM;
import static google.registry.util.ListNamingUtils.convertFilePathToName;
@@ -57,6 +58,12 @@ abstract class CreateOrUpdatePremiumListCommand extends ConfirmingCommand
required = true)
Path inputFile;
@Parameter(
names = {"--also_cloud_sql"},
description =
"Persist premium list to Cloud SQL in addition to Datastore; defaults to false.")
boolean alsoCloudSql;
protected AppEngineConnection connection;
protected int inputLineCount;
@@ -67,7 +74,7 @@ abstract class CreateOrUpdatePremiumListCommand extends ConfirmingCommand
abstract String getCommandPath();
ImmutableMap<String, ?> getParameterMap() {
ImmutableMap<String, String> getParameterMap() {
return ImmutableMap.of();
}
@@ -88,14 +95,15 @@ abstract class CreateOrUpdatePremiumListCommand extends ConfirmingCommand
@Override
public String execute() throws Exception {
ImmutableMap.Builder<String, Object> params = new ImmutableMap.Builder<>();
ImmutableMap.Builder<String, String> params = new ImmutableMap.Builder<>();
params.put(NAME_PARAM, name);
params.put(ALSO_CLOUD_SQL_PARAM, Boolean.toString(alsoCloudSql));
String inputFileContents = new String(Files.readAllBytes(inputFile), UTF_8);
String requestBody =
Joiner.on('&').withKeyValueSeparator("=").join(
ImmutableMap.of(INPUT_PARAM, URLEncoder.encode(inputFileContents, UTF_8.toString())));
ImmutableMap<String, ?> extraParams = getParameterMap();
ImmutableMap<String, String> extraParams = getParameterMap();
if (extraParams != null) {
params.putAll(extraParams);
}
@@ -110,7 +118,7 @@ abstract class CreateOrUpdatePremiumListCommand extends ConfirmingCommand
// TODO(user): refactor this behavior into a better general-purpose
// response validation that can be re-used across the new client/server commands.
String extractServerResponse(String response) {
private String extractServerResponse(String response) {
Map<String, Object> responseMap = toMap(JSONValue.parse(stripJsonPrefix(response)));
// TODO(user): consider using jart's FormField Framework.
@@ -127,7 +135,7 @@ abstract class CreateOrUpdatePremiumListCommand extends ConfirmingCommand
}
// TODO(user): figure out better place to put this method to make it re-usable
static String stripJsonPrefix(String json) {
private static String stripJsonPrefix(String json) {
Verify.verify(json.startsWith(JSON_SAFETY_PREFIX));
return json.substring(JSON_SAFETY_PREFIX.length());
}

View File

@@ -36,12 +36,11 @@ public class CreatePremiumListCommand extends CreateOrUpdatePremiumListCommand {
}
@Override
ImmutableMap<String, ?> getParameterMap() {
ImmutableMap<String, String> getParameterMap() {
if (override) {
return ImmutableMap.of("override", override);
return ImmutableMap.of("override", "true");
} else {
return ImmutableMap.of();
}
}
}

View File

@@ -19,32 +19,23 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.secdns.DelegationSignerData;
import google.registry.model.eppcommon.Trid;
import google.registry.model.transfer.BaseTransferObject;
import google.registry.model.transfer.TransferData;
import google.registry.persistence.NomulusNamingStrategy;
import google.registry.schema.domain.RegistryLock;
import google.registry.schema.tld.PremiumList;
import google.registry.schema.tmch.ClaimsList;
import google.registry.persistence.NomulusPostgreSQLDialect;
import google.registry.persistence.PersistenceModule;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.Types;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cfg.Environment;
import org.hibernate.dialect.PostgreSQL95Dialect;
import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;
import org.hibernate.jpa.boot.internal.PersistenceXmlParser;
import org.hibernate.tool.hbm2ddl.SchemaExport;
import org.hibernate.tool.schema.TargetType;
import org.joda.time.Period;
import org.testcontainers.containers.PostgreSQLContainer;
/**
@@ -57,22 +48,6 @@ import org.testcontainers.containers.PostgreSQLContainer;
@Parameters(separators = " =", commandDescription = "Generate PostgreSQL schema.")
public class GenerateSqlSchemaCommand implements Command {
// TODO(mmuller): These should be read from persistence.xml so we don't need to maintain two
// separate lists of all SQL table classes.
private static final ImmutableSet<Class> SQL_TABLE_CLASSES =
ImmutableSet.of(
BaseTransferObject.class,
ClaimsList.class,
DelegationSignerData.class,
DesignatedContact.class,
DomainBase.class,
GracePeriod.class,
Period.class,
PremiumList.class,
RegistryLock.class,
TransferData.class,
Trid.class);
@VisibleForTesting
public static final String DB_OPTIONS_CLASH =
"Database host and port may not be specified along with the option to start a "
@@ -159,7 +134,9 @@ public class GenerateSqlSchemaCommand implements Command {
MetadataSources metadata =
new MetadataSources(new StandardServiceRegistryBuilder().applySettings(settings).build());
SQL_TABLE_CLASSES.forEach(metadata::addAnnotatedClass);
addAnnotatedClasses(metadata, settings);
SchemaExport schemaExport = new SchemaExport();
schemaExport.setHaltOnError(true);
schemaExport.setFormat(true);
@@ -199,12 +176,29 @@ public class GenerateSqlSchemaCommand implements Command {
}
}
/** Nomulus mapping rules for column types in Postgresql. */
public static class NomulusPostgreSQLDialect extends PostgreSQL95Dialect {
public NomulusPostgreSQLDialect() {
super();
registerColumnType(Types.VARCHAR, "text");
registerColumnType(Types.TIMESTAMP, "timestamptz");
private void addAnnotatedClasses(MetadataSources metadata, Map<String, String> settings) {
ParsedPersistenceXmlDescriptor descriptor =
PersistenceXmlParser.locatePersistenceUnits(settings).stream()
.filter(unit -> PersistenceModule.PERSISTENCE_UNIT_NAME.equals(unit.getName()))
.findFirst()
.orElseThrow(
() ->
new IllegalArgumentException(
String.format(
"Could not find persistence unit with name %s",
PersistenceModule.PERSISTENCE_UNIT_NAME)));
List<String> classNames = descriptor.getManagedClassNames();
for (String className : classNames) {
try {
Class<?> clazz = Class.forName(className);
metadata.addAnnotatedClass(clazz);
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException(
String.format(
"Could not load class with name %s present in persistence.xml", className),
e);
}
}
}
}

View File

@@ -31,6 +31,7 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import google.registry.config.RegistryConfig;
import google.registry.model.ofy.ObjectifyService;
import google.registry.model.transaction.TransactionManagerFactory;
import google.registry.tools.AuthModule.LoginRequiredException;
import google.registry.tools.params.ParameterFactory;
import java.io.ByteArrayInputStream;
@@ -210,6 +211,8 @@ final class RegistryCli implements AutoCloseable, CommandRunner {
}
// CommandWithRemoteApis need to have the remote api installed to work.
// CommandWithCloudSql extends CommandWithRemoteApi so the command will also get the remote
// api installed. This is because the DB password is stored in Datastore.
if (command instanceof CommandWithRemoteApi) {
if (installer == null) {
installer = new RemoteApiInstaller();
@@ -233,6 +236,10 @@ final class RegistryCli implements AutoCloseable, CommandRunner {
ofy().clearSessionCache();
}
if (command instanceof CommandWithCloudSql) {
TransactionManagerFactory.initForTool();
}
command.run();
}

View File

@@ -21,7 +21,9 @@ import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.base.Joiner;
import com.google.common.io.Files;
import google.registry.model.tmch.ClaimsListDao;
import google.registry.model.tmch.ClaimsListShard;
import google.registry.schema.tmch.ClaimsList;
import google.registry.tmch.ClaimsListParser;
import java.io.File;
import java.io.IOException;
@@ -35,9 +37,14 @@ final class UploadClaimsListCommand extends ConfirmingCommand implements Command
@Parameter(description = "Claims list filename")
private List<String> mainParameters = new ArrayList<>();
@Parameter(
names = {"--also_cloud_sql"},
description = "Persist claims list to Cloud SQL in addition to Datastore; defaults to false.")
boolean alsoCloudSql;
private String claimsListFilename;
private ClaimsListShard claimsList;
private ClaimsList claimsList;
@Override
protected void init() throws IOException {
@@ -56,7 +63,10 @@ final class UploadClaimsListCommand extends ConfirmingCommand implements Command
@Override
public String execute() {
claimsList.save();
ClaimsListShard.create(claimsList.getTmdbGenerationTime(), claimsList.getLabelsToKeys()).save();
if (alsoCloudSql) {
ClaimsListDao.trySave(claimsList);
}
return String.format("Successfully uploaded claims list %s", claimsListFilename);
}
}

View File

@@ -14,17 +14,28 @@
package google.registry.tools.server;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.flogger.LazyArgs.lazy;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.flogger.FluentLogger;
import google.registry.model.registry.label.PremiumList;
import google.registry.model.registry.label.PremiumList.PremiumListEntry;
import google.registry.request.JsonResponse;
import google.registry.request.Parameter;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import org.joda.money.CurrencyUnit;
/**
* Abstract base class for actions that update premium lists.
*/
/** Abstract base class for actions that update premium lists. */
public abstract class CreateOrUpdatePremiumListAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -33,24 +44,70 @@ public abstract class CreateOrUpdatePremiumListAction implements Runnable {
public static final String NAME_PARAM = "name";
public static final String INPUT_PARAM = "inputData";
public static final String ALSO_CLOUD_SQL_PARAM = "alsoCloudSql";
@Inject JsonResponse response;
@Inject @Parameter("premiumListName") String name;
@Inject @Parameter(INPUT_PARAM) String inputData;
@Inject
@Parameter("premiumListName")
String name;
@Inject
@Parameter(INPUT_PARAM)
String inputData;
@Inject
@Parameter(ALSO_CLOUD_SQL_PARAM)
boolean alsoCloudSql;
@Override
public void run() {
try {
savePremiumList();
saveToDatastore();
} catch (IllegalArgumentException e) {
logger.atInfo().withCause(e).log(
"Usage error in attempting to save premium list from nomulus tool command");
response.setPayload(ImmutableMap.of("error", e.toString(), "status", "error"));
return;
} catch (Exception e) {
logger.atSevere().withCause(e).log(
"Unexpected error saving premium list from nomulus tool command");
"Unexpected error saving premium list to Datastore from nomulus tool command");
response.setPayload(ImmutableMap.of("error", e.toString(), "status", "error"));
return;
}
if (alsoCloudSql) {
try {
saveToCloudSql();
} catch (Throwable e) {
logger.atSevere().withCause(e).log(
"Unexpected error saving premium list to Cloud SQL from nomulus tool command");
response.setPayload(ImmutableMap.of("error", e.toString(), "status", "error"));
return;
}
}
}
google.registry.schema.tld.PremiumList parseInputToPremiumList() {
List<String> inputDataPreProcessed =
Splitter.on('\n').omitEmptyStrings().splitToList(inputData);
ImmutableMap<String, PremiumListEntry> prices =
new PremiumList.Builder().setName(name).build().parse(inputDataPreProcessed);
ImmutableSet<CurrencyUnit> currencies =
prices.values().stream()
.map(e -> e.getValue().getCurrencyUnit())
.distinct()
.collect(toImmutableSet());
checkArgument(
currencies.size() == 1,
"The Cloud SQL schema requires exactly one currency, but got: %s",
ImmutableSortedSet.copyOf(currencies));
CurrencyUnit currency = Iterables.getOnlyElement(currencies);
Map<String, BigDecimal> priceAmounts =
Maps.transformValues(prices, ple -> ple.getValue().getAmount());
return google.registry.schema.tld.PremiumList.create(name, currency, priceAmounts);
}
/** Logs the premium list data at INFO, truncated if too long. */
@@ -64,6 +121,9 @@ public abstract class CreateOrUpdatePremiumListAction implements Runnable {
: (inputData.substring(0, MAX_LOGGING_PREMIUM_LIST_LENGTH) + "<truncated>")));
}
/** Creates a new premium list or updates an existing one. */
protected abstract void savePremiumList();
/** Saves the premium list to Datastore. */
protected abstract void saveToDatastore();
/** Saves the premium list to Cloud SQL. */
protected abstract void saveToCloudSql();
}

View File

@@ -27,6 +27,7 @@ import google.registry.model.registry.label.PremiumList;
import google.registry.request.Action;
import google.registry.request.Parameter;
import google.registry.request.auth.Auth;
import google.registry.schema.tld.PremiumListDao;
import java.util.List;
import javax.inject.Inject;
@@ -50,7 +51,7 @@ public class CreatePremiumListAction extends CreateOrUpdatePremiumListAction {
@Inject CreatePremiumListAction() {}
@Override
protected void savePremiumList() {
protected void saveToDatastore() {
checkArgument(
!doesPremiumListExist(name), "A premium list of this name already exists: %s.", name);
if (!override) {
@@ -71,4 +72,22 @@ public class CreatePremiumListAction extends CreateOrUpdatePremiumListAction {
logger.atInfo().log(message);
response.setPayload(ImmutableMap.of("status", "success", "message", message));
}
@Override
protected void saveToCloudSql() {
if (!override) {
assertTldExists(name);
}
logger.atInfo().log("Saving premium list to Cloud SQL for TLD %s", name);
// TODO(mcilwain): Call logInputData() here once Datastore persistence is removed.
google.registry.schema.tld.PremiumList premiumList = parseInputToPremiumList();
PremiumListDao.saveNew(premiumList);
String message =
String.format(
"Saved premium list %s with %d entries", name, premiumList.getLabelsToPrices().size());
logger.atInfo().log(message);
// TODO(mcilwain): Call response.setPayload(...) here once Datastore persistence is removed.
}
}

View File

@@ -60,6 +60,12 @@ public class ToolsServerModule {
return extractRequiredParameter(req, CreatePremiumListAction.INPUT_PARAM);
}
@Provides
@Parameter("alsoCloudSql")
static boolean provideAlsoCloudSql(HttpServletRequest req) {
return extractBooleanParameter(req, CreatePremiumListAction.ALSO_CLOUD_SQL_PARAM);
}
@Provides
@Parameter("premiumListName")
static String provideName(HttpServletRequest req) {

View File

@@ -46,7 +46,7 @@ public class UpdatePremiumListAction extends CreateOrUpdatePremiumListAction {
@Inject UpdatePremiumListAction() {}
@Override
protected void savePremiumList() {
protected void saveToDatastore() {
Optional<PremiumList> existingPremiumList = PremiumList.getUncached(name);
checkArgument(
existingPremiumList.isPresent(),
@@ -67,4 +67,11 @@ public class UpdatePremiumListAction extends CreateOrUpdatePremiumListAction {
logger.atInfo().log(message);
response.setPayload(ImmutableMap.of("status", "success", "message", message));
}
// TODO(mcilwain): Implement this in a subsequent PR.
@Override
protected void saveToCloudSql() {
throw new UnsupportedOperationException(
"Updating of premium lists in Cloud SQL is not supported yet");
}
}

View File

@@ -32,6 +32,12 @@
<class>google.registry.model.transfer.TransferData</class>
<class>google.registry.model.eppcommon.Trid</class>
<!-- Customized type converters -->
<class>google.registry.persistence.BloomFilterConverter</class>
<class>google.registry.persistence.CreateAutoTimestampConverter</class>
<class>google.registry.persistence.UpdateAutoTimestampConverter</class>
<class>google.registry.persistence.ZonedDateTimeConverter</class>
<!-- TODO(weiminyu): check out application-layer validation. -->
<validation-mode>NONE</validation-mode>
</persistence-unit>

View File

@@ -0,0 +1,104 @@
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.registry;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.testing.JUnitBackports.assertThrows;
import google.registry.model.transaction.JpaTransactionManagerRule;
import google.registry.schema.domain.RegistryLock;
import google.registry.schema.domain.RegistryLock.Action;
import google.registry.testing.AppEngineRule;
import java.util.UUID;
import javax.persistence.PersistenceException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link RegistryLockDao}. */
@RunWith(JUnit4.class)
public final class RegistryLockDaoTest {
@Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build();
@Rule
public final JpaTransactionManagerRule jpaTmRule =
new JpaTransactionManagerRule.Builder().build();
@Test
public void testSaveAndLoad_success() {
RegistryLock lock = createLock();
RegistryLockDao.save(lock);
RegistryLock fromDatabase = RegistryLockDao.getByVerificationCode(lock.getVerificationCode());
assertThat(fromDatabase.getDomainName()).isEqualTo(lock.getDomainName());
assertThat(fromDatabase.getVerificationCode()).isEqualTo(lock.getVerificationCode());
}
@Test
public void testSaveAndLoad_failure_differentCode() {
RegistryLock lock = createLock();
RegistryLockDao.save(lock);
PersistenceException exception =
assertThrows(
PersistenceException.class,
() -> RegistryLockDao.getByVerificationCode(UUID.randomUUID().toString()));
assertThat(exception)
.hasCauseThat()
.hasMessageThat()
.isEqualTo("No registry lock with this code");
assertThat(exception).hasCauseThat().isInstanceOf(NullPointerException.class);
}
@Test
public void testSaveTwiceAndLoad_returnsLatest() {
RegistryLock lock = createLock();
jpaTm().transact(() -> RegistryLockDao.save(lock));
jpaTmRule.getTxnClock().advanceOneMilli();
jpaTm()
.transact(
() -> {
RegistryLock secondLock =
RegistryLockDao.getByVerificationCode(lock.getVerificationCode());
secondLock.setCompletionTimestamp(jpaTmRule.getTxnClock().nowUtc());
RegistryLockDao.save(secondLock);
});
jpaTm()
.transact(
() -> {
RegistryLock fromDatabase =
RegistryLockDao.getByVerificationCode(lock.getVerificationCode());
assertThat(fromDatabase.getCompletionTimestamp().get())
.isEqualTo(jpaTmRule.getTxnClock().nowUtc());
});
}
@Test
public void testFailure_saveNull() {
assertThrows(NullPointerException.class, () -> RegistryLockDao.save(null));
}
private RegistryLock createLock() {
return new RegistryLock.Builder()
.setRepoId("repoId")
.setDomainName("example.test")
.setRegistrarId("TheRegistrar")
.setAction(Action.LOCK)
.setVerificationCode(UUID.randomUUID().toString())
.isSuperuser(true)
.build();
}
}

View File

@@ -0,0 +1,95 @@
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.tmch;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.JUnitBackports.assertThrows;
import com.google.common.collect.ImmutableMap;
import google.registry.model.transaction.JpaTransactionManagerRule;
import google.registry.schema.tmch.ClaimsList;
import google.registry.testing.FakeClock;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link ClaimsListDao}. */
@RunWith(JUnit4.class)
public class ClaimsListDaoTest {
private FakeClock fakeClock = new FakeClock();
@Rule
public final JpaTransactionManagerRule jpaTmRule =
new JpaTransactionManagerRule.Builder().build();
@Test
public void trySave_insertsClaimsListSuccessfully() {
ClaimsList claimsList =
ClaimsList.create(fakeClock.nowUtc(), ImmutableMap.of("label1", "key1", "label2", "key2"));
ClaimsListDao.trySave(claimsList);
ClaimsList insertedClaimsList = ClaimsListDao.getCurrent();
assertClaimsListEquals(claimsList, insertedClaimsList);
assertThat(insertedClaimsList.getCreationTimestamp())
.isEqualTo(jpaTmRule.getTxnClock().nowUtc());
}
@Test
public void trySave_noExceptionThrownWhenSaveFail() {
ClaimsList claimsList =
ClaimsList.create(fakeClock.nowUtc(), ImmutableMap.of("label1", "key1", "label2", "key2"));
ClaimsListDao.trySave(claimsList);
ClaimsList insertedClaimsList = ClaimsListDao.getCurrent();
assertClaimsListEquals(claimsList, insertedClaimsList);
// Save ClaimsList with existing revisionId should fail because revisionId is the primary key.
ClaimsListDao.trySave(insertedClaimsList);
}
@Test
public void trySave_claimsListWithNoEntries() {
ClaimsList claimsList = ClaimsList.create(fakeClock.nowUtc(), ImmutableMap.of());
ClaimsListDao.trySave(claimsList);
ClaimsList insertedClaimsList = ClaimsListDao.getCurrent();
assertClaimsListEquals(claimsList, insertedClaimsList);
assertThat(insertedClaimsList.getLabelsToKeys()).isEmpty();
}
@Test
public void getCurrent_throwsNoResultExceptionIfTableIsEmpty() {
PersistenceException thrown =
assertThrows(PersistenceException.class, () -> ClaimsListDao.getCurrent());
assertThat(thrown).hasCauseThat().isInstanceOf(NoResultException.class);
}
@Test
public void getCurrent_returnsLatestClaims() {
ClaimsList oldClaimsList =
ClaimsList.create(fakeClock.nowUtc(), ImmutableMap.of("label1", "key1", "label2", "key2"));
ClaimsList newClaimsList =
ClaimsList.create(fakeClock.nowUtc(), ImmutableMap.of("label3", "key3", "label4", "key4"));
ClaimsListDao.trySave(oldClaimsList);
ClaimsListDao.trySave(newClaimsList);
assertClaimsListEquals(newClaimsList, ClaimsListDao.getCurrent());
}
private void assertClaimsListEquals(ClaimsList left, ClaimsList right) {
assertThat(left.getRevisionId()).isEqualTo(right.getRevisionId());
assertThat(left.getTmdbGenerationTime()).isEqualTo(right.getTmdbGenerationTime());
assertThat(left.getLabelsToKeys()).isEqualTo(right.getLabelsToKeys());
}
}

View File

@@ -16,9 +16,21 @@ package google.registry.model.transaction;
import static org.joda.time.DateTimeZone.UTC;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import google.registry.persistence.PersistenceModule;
import google.registry.testing.FakeClock;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.persistence.EntityManagerFactory;
import org.hibernate.cfg.Environment;
import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;
import org.hibernate.jpa.boot.internal.PersistenceXmlParser;
import org.hibernate.jpa.boot.spi.Bootstrap;
import org.joda.time.DateTime;
import org.junit.rules.ExternalResource;
import org.junit.rules.RuleChain;
@@ -41,12 +53,20 @@ public class JpaTransactionManagerRule extends ExternalResource {
private final DateTime now = DateTime.now(UTC);
private final FakeClock clock = new FakeClock(now);
private final String initScript;
private final ImmutableList<Class> extraEntityClasses;
private final ImmutableMap userProperties;
private JdbcDatabaseContainer database;
private EntityManagerFactory emf;
private JpaTransactionManager cachedTm;
private JpaTransactionManagerRule(String initScript) {
private JpaTransactionManagerRule(
String initScript,
ImmutableList<Class> extraEntityClasses,
ImmutableMap<String, String> userProperties) {
this.initScript = initScript;
this.extraEntityClasses = extraEntityClasses;
this.userProperties = userProperties;
}
/** Wraps {@link JpaTransactionManagerRule} in a {@link PostgreSQLContainer}. */
@@ -60,12 +80,21 @@ public class JpaTransactionManagerRule extends ExternalResource {
@Override
public void before() {
ImmutableMap properties = PersistenceModule.providesDefaultDatabaseConfigs();
if (!userProperties.isEmpty()) {
// If there are user properties, create a new properties object with these added.
ImmutableMap.Builder builder = properties.builder();
builder.putAll(userProperties);
properties = builder.build();
}
emf =
PersistenceModule.create(
createEntityManagerFactory(
database.getJdbcUrl(),
database.getUsername(),
database.getPassword(),
PersistenceModule.providesDefaultDatabaseConfigs());
properties,
extraEntityClasses);
JpaTransactionManagerImpl txnManager = new JpaTransactionManagerImpl(emf, clock);
cachedTm = TransactionManagerFactory.jpaTm;
TransactionManagerFactory.jpaTm = txnManager;
@@ -80,6 +109,33 @@ public class JpaTransactionManagerRule extends ExternalResource {
cachedTm = null;
}
/** Constructs the {@link EntityManagerFactory} instance. */
private static EntityManagerFactory createEntityManagerFactory(
String jdbcUrl,
String username,
String password,
ImmutableMap<String, String> configs,
ImmutableList<Class> extraEntityClasses) {
HashMap<String, String> properties = Maps.newHashMap(configs);
properties.put(Environment.URL, jdbcUrl);
properties.put(Environment.USER, username);
properties.put(Environment.PASS, password);
ParsedPersistenceXmlDescriptor descriptor =
PersistenceXmlParser.locatePersistenceUnits(properties).stream()
.filter(unit -> PersistenceModule.PERSISTENCE_UNIT_NAME.equals(unit.getName()))
.findFirst()
.orElseThrow(
() ->
new IllegalArgumentException(
String.format(
"Could not find persistence unit with name %s",
PersistenceModule.PERSISTENCE_UNIT_NAME)));
extraEntityClasses.stream().map(Class::getName).forEach(descriptor::addClasses);
return Bootstrap.getEntityManagerFactoryBuilder(descriptor, properties).build();
}
/** Returns the {@link FakeClock} used by the underlying {@link JpaTransactionManagerImpl}. */
public FakeClock getTxnClock() {
return clock;
@@ -88,6 +144,8 @@ public class JpaTransactionManagerRule extends ExternalResource {
/** Builder for {@link JpaTransactionManagerRule}. */
public static class Builder {
private String initScript;
private List<Class> extraEntityClasses = new ArrayList<Class>();
private Map<String, String> userProperties = new HashMap<String, String>();
/**
* Sets the SQL script to be used to initialize the database. If not set,
@@ -98,12 +156,27 @@ public class JpaTransactionManagerRule extends ExternalResource {
return this;
}
/** Adds annotated class(es) to the known entities for the database. */
public Builder withEntityClass(Class... classes) {
this.extraEntityClasses.addAll(ImmutableSet.copyOf(classes));
return this;
}
/** Adds the specified property to those used to initialize the transaction manager. */
public Builder withProperty(String name, String value) {
this.userProperties.put(name, value);
return this;
}
/** Builds a {@link JpaTransactionManagerRule} instance. */
public JpaTransactionManagerRule build() {
if (initScript == null) {
initScript = SCHEMA_GOLDEN_SQL;
}
return new JpaTransactionManagerRule(initScript);
return new JpaTransactionManagerRule(
initScript,
ImmutableList.copyOf(extraEntityClasses),
ImmutableMap.copyOf(userProperties));
}
}
}

View File

@@ -18,8 +18,12 @@ import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.testing.JUnitBackports.assertThrows;
import google.registry.model.ImmutableObject;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.PersistenceException;
import org.hibernate.cfg.Environment;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -31,7 +35,10 @@ public class JpaTransactionManagerRuleTest {
@Rule
public final JpaTransactionManagerRule jpaTmRule =
new JpaTransactionManagerRule.Builder().build();
new JpaTransactionManagerRule.Builder()
.withEntityClass(TestEntity.class)
.withProperty(Environment.HBM2DDL_AUTO, "update")
.build();
@Test
public void verifiesRuleWorks() {
@@ -56,4 +63,28 @@ public class JpaTransactionManagerRuleTest {
assertThat(results).isEmpty();
});
}
@Test
public void testExtraParameters() {
// This test verifies that 1) withEntityClass() has registered TestEntity and 2) The table
// has been created, implying withProperty(HBM2DDL_AUTO, "update") worked.
TestEntity original = new TestEntity("key", "value");
jpaTm().transact(() -> jpaTm().getEntityManager().persist(original));
TestEntity retrieved =
jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "key"));
assertThat(retrieved).isEqualTo(original);
}
@Entity(name = "TestEntity") // Specify name to avoid nested class naming issues.
static class TestEntity extends ImmutableObject {
@Id String key;
String value;
TestEntity(String key, String value) {
this.key = key;
this.value = value;
}
TestEntity() {}
}
}

View File

@@ -0,0 +1,67 @@
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.persistence;
import static com.google.common.base.Charsets.US_ASCII;
import static com.google.common.hash.Funnels.stringFunnel;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.transaction.TransactionManagerFactory.jpaTm;
import com.google.common.collect.ImmutableSet;
import com.google.common.hash.BloomFilter;
import google.registry.model.ImmutableObject;
import google.registry.model.transaction.JpaTransactionManagerRule;
import javax.persistence.Entity;
import javax.persistence.Id;
import org.hibernate.cfg.Environment;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public class BloomFilterConverterTest {
@Rule
public final JpaTransactionManagerRule jpaTmRule =
new JpaTransactionManagerRule.Builder()
.withEntityClass(TestEntity.class)
.withProperty(Environment.HBM2DDL_AUTO, "update")
.build();
@Test
public void roundTripConversion_returnsSameBloomFilter() {
BloomFilter<String> bloomFilter = BloomFilter.create(stringFunnel(US_ASCII), 3);
ImmutableSet.of("foo", "bar", "baz").forEach(bloomFilter::put);
TestEntity entity = new TestEntity(bloomFilter);
jpaTm().transact(() -> jpaTm().getEntityManager().persist(entity));
TestEntity persisted =
jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "id"));
assertThat(persisted.bloomFilter).isEqualTo(bloomFilter);
}
@Entity(name = "TestEntity") // Override entity name to avoid the nested class reference.
public static class TestEntity extends ImmutableObject {
@Id String name = "id";
BloomFilter<String> bloomFilter;
public TestEntity() {}
public TestEntity(BloomFilter<String> bloomFilter) {
this.bloomFilter = bloomFilter;
}
}
}

View File

@@ -0,0 +1,78 @@
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.persistence;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.transaction.TransactionManagerFactory.jpaTm;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.ImmutableObject;
import google.registry.model.transaction.JpaTransactionManagerRule;
import javax.persistence.Entity;
import javax.persistence.Id;
import org.hibernate.cfg.Environment;
import org.joda.time.DateTime;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public class CreateAutoTimestampConverterTest {
@Rule
public final JpaTransactionManagerRule jpaTmRule =
new JpaTransactionManagerRule.Builder()
.withEntityClass(TestEntity.class)
.withProperty(Environment.HBM2DDL_AUTO, "update")
.build();
@Test
public void testTypeConversion() {
CreateAutoTimestamp ts = CreateAutoTimestamp.create(DateTime.parse("2019-09-9T11:39:00Z"));
TestEntity ent = new TestEntity("myinst", ts);
jpaTm().transact(() -> jpaTm().getEntityManager().persist(ent));
TestEntity result =
jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "myinst"));
assertThat(result).isEqualTo(new TestEntity("myinst", ts));
}
@Test
public void testAutoInitialization() {
CreateAutoTimestamp ts = CreateAutoTimestamp.create(null);
TestEntity ent = new TestEntity("autoinit", ts);
jpaTm().transact(() -> jpaTm().getEntityManager().persist(ent));
TestEntity result =
jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "autoinit"));
assertThat(result.cat.getTimestamp()).isEqualTo(jpaTmRule.getTxnClock().nowUtc());
}
@Entity(name = "TestEntity") // Override entity name to avoid the nested class reference.
public static class TestEntity extends ImmutableObject {
@Id String name;
CreateAutoTimestamp cat;
public TestEntity() {}
public TestEntity(String name, CreateAutoTimestamp cat) {
this.name = name;
this.cat = cat;
}
}
}

View File

@@ -0,0 +1,89 @@
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.persistence;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.transaction.TransactionManagerFactory.jpaTm;
import google.registry.model.ImmutableObject;
import google.registry.model.UpdateAutoTimestamp;
import google.registry.model.transaction.JpaTransactionManagerRule;
import javax.persistence.Entity;
import javax.persistence.Id;
import org.hibernate.cfg.Environment;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public class UpdateAutoTimestampConverterTest {
@Rule
public final JpaTransactionManagerRule jpaTmRule =
new JpaTransactionManagerRule.Builder()
.withEntityClass(TestEntity.class)
.withProperty(Environment.HBM2DDL_AUTO, "update")
.build();
@Test
public void testTypeConversion() {
TestEntity ent = new TestEntity("myinst", null);
jpaTm().transact(() -> jpaTm().getEntityManager().persist(ent));
TestEntity result =
jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "myinst"));
assertThat(result.name).isEqualTo("myinst");
assertThat(result.uat.getTimestamp()).isEqualTo(jpaTmRule.getTxnClock().nowUtc());
}
@Test
public void testTimeChangesOnSubsequentTransactions() {
TestEntity ent1 = new TestEntity("myinst1", null);
jpaTm().transact(() -> jpaTm().getEntityManager().persist(ent1));
TestEntity result1 =
jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "myinst1"));
jpaTmRule.getTxnClock().advanceOneMilli();
TestEntity ent2 = new TestEntity("myinst2", result1.uat);
jpaTm().transact(() -> jpaTm().getEntityManager().persist(ent2));
TestEntity result2 =
jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "myinst2"));
assertThat(result1.uat.getTimestamp()).isNotEqualTo(result2.uat.getTimestamp());
assertThat(result2.uat.getTimestamp()).isEqualTo(jpaTmRule.getTxnClock().nowUtc());
}
@Entity(name = "TestEntity") // Override entity name to avoid the nested class reference.
public static class TestEntity extends ImmutableObject {
@Id String name;
UpdateAutoTimestamp uat;
public TestEntity() {}
public TestEntity(String name, UpdateAutoTimestamp uat) {
this.name = name;
this.uat = uat;
}
}
}

View File

@@ -0,0 +1,121 @@
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.persistence;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.transaction.TransactionManagerFactory.jpaTm;
import google.registry.model.ImmutableObject;
import google.registry.model.transaction.JpaTransactionManagerRule;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.ZonedDateTime;
import javax.persistence.Entity;
import javax.persistence.Id;
import org.hibernate.cfg.Environment;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link ZonedDateTimeConverter}. */
@RunWith(JUnit4.class)
public class ZonedDateTimeConverterTest {
@Rule
public final JpaTransactionManagerRule jpaTmRule =
new JpaTransactionManagerRule.Builder()
.withEntityClass(TestEntity.class)
.withProperty(Environment.HBM2DDL_AUTO, "update")
.build();
private final ZonedDateTimeConverter converter = new ZonedDateTimeConverter();
@Test
public void convertToDatabaseColumn_returnsNullIfInputIsNull() {
assertThat(converter.convertToDatabaseColumn(null)).isNull();
}
@Test
public void convertToDatabaseColumn_convertsCorrectly() {
ZonedDateTime zonedDateTime = ZonedDateTime.parse("2019-09-01T01:01:01Z");
assertThat(converter.convertToDatabaseColumn(zonedDateTime).toInstant())
.isEqualTo(zonedDateTime.toInstant());
}
@Test
public void convertToEntityAttribute_returnsNullIfInputIsNull() {
assertThat(converter.convertToEntityAttribute(null)).isNull();
}
@Test
public void convertToEntityAttribute_convertsCorrectly() {
ZonedDateTime zonedDateTime = ZonedDateTime.parse("2019-09-01T01:01:01Z");
Instant instant = zonedDateTime.toInstant();
assertThat(converter.convertToEntityAttribute(Timestamp.from(instant)))
.isEqualTo(zonedDateTime);
}
@Test
public void converter_generatesTimestampWithNormalizedZone() {
ZonedDateTime zdt = ZonedDateTime.parse("2019-09-01T01:01:01Z");
TestEntity entity = new TestEntity("normalized_utc_time", zdt);
jpaTm().transact(() -> jpaTm().getEntityManager().persist(entity));
TestEntity retrievedEntity =
jpaTm()
.transact(
() -> jpaTm().getEntityManager().find(TestEntity.class, "normalized_utc_time"));
assertThat(retrievedEntity.zdt.toString()).isEqualTo("2019-09-01T01:01:01Z");
}
@Test
public void converter_convertsNonNormalizedZoneCorrectly() {
ZonedDateTime zdt = ZonedDateTime.parse("2019-09-01T01:01:01Z[UTC]");
TestEntity entity = new TestEntity("non_normalized_utc_time", zdt);
jpaTm().transact(() -> jpaTm().getEntityManager().persist(entity));
TestEntity retrievedEntity =
jpaTm()
.transact(
() -> jpaTm().getEntityManager().find(TestEntity.class, "non_normalized_utc_time"));
assertThat(retrievedEntity.zdt.toString()).isEqualTo("2019-09-01T01:01:01Z");
}
@Test
public void converter_convertsNonUtcZoneCorrectly() {
ZonedDateTime zdt = ZonedDateTime.parse("2019-09-01T01:01:01+05:00");
TestEntity entity = new TestEntity("new_york_time", zdt);
jpaTm().transact(() -> jpaTm().getEntityManager().persist(entity));
TestEntity retrievedEntity =
jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "new_york_time"));
assertThat(retrievedEntity.zdt.toString()).isEqualTo("2019-08-31T20:01:01Z");
}
@Entity(name = "TestEntity") // Override entity name to avoid the nested class reference.
private static class TestEntity extends ImmutableObject {
@Id String name;
ZonedDateTime zdt;
public TestEntity() {}
public TestEntity(String name, ZonedDateTime zdt) {
this.name = name;
this.zdt = zdt;
}
}
}

View File

@@ -35,6 +35,7 @@ import google.registry.testing.FakeClock;
import google.registry.testing.FakeLockHandler;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -54,9 +55,11 @@ public class EscrowTaskRunnerTest {
private final EscrowTask task = mock(EscrowTask.class);
private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01TZ"));
private DateTimeZone previousDateTimeZone;
private EscrowTaskRunner runner;
private Registry registry;
@Before
public void before() {
createTld("lol");
@@ -64,9 +67,15 @@ public class EscrowTaskRunnerTest {
runner = new EscrowTaskRunner();
runner.clock = clock;
runner.lockHandler = new FakeLockHandler(true);
previousDateTimeZone = DateTimeZone.getDefault();
DateTimeZone.setDefault(DateTimeZone.forID("America/New_York")); // Make sure UTC stuff works.
}
@After
public void after() {
DateTimeZone.setDefault(previousDateTimeZone);
}
@Test
public void testRun_cursorIsToday_advancesCursorToTomorrow() throws Exception {
clock.setTo(DateTime.parse("2006-06-06T00:30:00Z"));

View File

@@ -0,0 +1,89 @@
// Copyright 2019 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.schema.tld;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.testing.JUnitBackports.assertThrows;
import com.google.common.collect.ImmutableMap;
import google.registry.model.transaction.JpaTransactionManagerRule;
import java.math.BigDecimal;
import javax.persistence.PersistenceException;
import org.joda.money.CurrencyUnit;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link PremiumListDao}. */
@RunWith(JUnit4.class)
public class PremiumListDaoTest {
@Rule
public final JpaTransactionManagerRule jpaTmRule =
new JpaTransactionManagerRule.Builder().build();
private static final ImmutableMap<String, BigDecimal> TEST_PRICES =
ImmutableMap.of(
"silver",
BigDecimal.valueOf(10.23),
"gold",
BigDecimal.valueOf(1305.47),
"palladium",
BigDecimal.valueOf(1552.78));
@Test
public void saveNew_worksSuccessfully() {
PremiumList premiumList = PremiumList.create("testname", CurrencyUnit.USD, TEST_PRICES);
PremiumListDao.saveNew(premiumList);
jpaTm()
.transact(
() -> {
PremiumList persistedList =
jpaTm()
.getEntityManager()
.createQuery(
"SELECT pl FROM PremiumList pl WHERE pl.name = :name", PremiumList.class)
.setParameter("name", "testname")
.getSingleResult();
assertThat(persistedList.getLabelsToPrices()).containsExactlyEntriesIn(TEST_PRICES);
assertThat(persistedList.getCreationTimestamp())
.isEqualTo(jpaTmRule.getTxnClock().nowUtc());
});
}
@Test
public void saveNew_throwsWhenPremiumListAlreadyExists() {
PremiumListDao.saveNew(PremiumList.create("testlist", CurrencyUnit.USD, TEST_PRICES));
PersistenceException thrown =
assertThrows(
PersistenceException.class,
() ->
PremiumListDao.saveNew(
PremiumList.create("testlist", CurrencyUnit.USD, TEST_PRICES)));
assertThat(thrown)
.hasCauseThat()
.hasMessageThat()
.contains("A premium list of this name already exists");
}
@Test
public void checkExists_worksSuccessfully() {
assertThat(PremiumListDao.checkExists("testlist")).isFalse();
PremiumListDao.saveNew(PremiumList.create("testlist", CurrencyUnit.USD, TEST_PRICES));
assertThat(PremiumListDao.checkExists("testlist")).isTrue();
}
}

View File

@@ -0,0 +1,50 @@
// Copyright 2019 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.schema.tld;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.hash.BloomFilter;
import java.math.BigDecimal;
import org.joda.money.CurrencyUnit;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link PremiumList}. */
@RunWith(JUnit4.class)
public class PremiumListTest {
private static final ImmutableMap<String, BigDecimal> TEST_PRICES =
ImmutableMap.of(
"silver",
BigDecimal.valueOf(10.23),
"gold",
BigDecimal.valueOf(1305.47),
"palladium",
BigDecimal.valueOf(1552.78));
@Test
public void bloomFilter_worksCorrectly() {
BloomFilter<String> bloomFilter =
PremiumList.create("testname", CurrencyUnit.USD, TEST_PRICES).getBloomFilter();
ImmutableSet.of("silver", "gold", "palladium")
.forEach(l -> assertThat(bloomFilter.mightContain(l)).isTrue());
ImmutableSet.of("dirt", "pyrite", "zirconia")
.forEach(l -> assertThat(bloomFilter.mightContain(l)).isFalse());
}
}

View File

@@ -67,7 +67,13 @@ public class CreatePremiumListCommandTest<C extends CreatePremiumListCommand>
verifySentParams(
connection,
servletPath,
ImmutableMap.of("name", "foo", "inputData", generateInputData(premiumTermsPath)));
ImmutableMap.of(
"name",
"foo",
"inputData",
generateInputData(premiumTermsPath),
"alsoCloudSql",
"false"));
}
@Test
@@ -78,7 +84,28 @@ public class CreatePremiumListCommandTest<C extends CreatePremiumListCommand>
connection,
servletPath,
ImmutableMap.of(
"name", "example_premium_terms", "inputData", generateInputData(premiumTermsPath)));
"name",
"example_premium_terms",
"inputData",
generateInputData(premiumTermsPath),
"alsoCloudSql",
"false"));
}
@Test
public void testRun_alsoCloudSql() throws Exception {
runCommandForced("-i=" + premiumTermsPath, "-n=foo", "--also_cloud_sql");
assertInStdout("Successfully");
verifySentParams(
connection,
servletPath,
ImmutableMap.of(
"name",
"foo",
"inputData",
generateInputData(premiumTermsPath),
"alsoCloudSql",
"true"));
}
@Test

View File

@@ -57,7 +57,13 @@ public class UpdatePremiumListCommandTest<C extends UpdatePremiumListCommand>
verifySentParams(
connection,
servletPath,
ImmutableMap.of("name", "foo", "inputData", generateInputData(premiumTermsPath)));
ImmutableMap.of(
"name",
"foo",
"inputData",
generateInputData(premiumTermsPath),
"alsoCloudSql",
"false"));
}
@Test
@@ -68,6 +74,11 @@ public class UpdatePremiumListCommandTest<C extends UpdatePremiumListCommand>
connection,
servletPath,
ImmutableMap.of(
"name", "example_premium_terms", "inputData", generateInputData(premiumTermsPath)));
"name",
"example_premium_terms",
"inputData",
generateInputData(premiumTermsPath),
"alsoCloudSql",
"false"));
}
}

View File

@@ -0,0 +1,70 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.tools.server;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static google.registry.testing.JUnitBackports.assertThrows;
import google.registry.schema.tld.PremiumList;
import google.registry.testing.AppEngineRule;
import google.registry.testing.FakeJsonResponse;
import java.math.BigDecimal;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link CreateOrUpdatePremiumListAction}. */
@RunWith(JUnit4.class)
public class CreateOrUpdatePremiumListActionTest {
@Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build();
private CreatePremiumListAction action;
private FakeJsonResponse response;
@Before
public void init() {
action = new CreatePremiumListAction();
response = new FakeJsonResponse();
action.response = response;
action.name = "testlist";
}
@Test
public void parseInputToPremiumList_works() {
action.inputData = "foo,USD 99.50\n" + "bar,USD 30\n" + "baz,USD 10\n";
PremiumList premiumList = action.parseInputToPremiumList();
assertThat(premiumList.getName()).isEqualTo("testlist");
assertThat(premiumList.getLabelsToPrices())
.containsExactly("foo", twoDigits(99.50), "bar", twoDigits(30), "baz", twoDigits(10));
}
@Test
public void parseInputToPremiumList_throwsOnInconsistentCurrencies() {
action.inputData = "foo,USD 99.50\n" + "bar,USD 30\n" + "baz,JPY 990\n";
IllegalArgumentException thrown =
assertThrows(IllegalArgumentException.class, () -> action.parseInputToPremiumList());
assertThat(thrown)
.hasMessageThat()
.isEqualTo("The Cloud SQL schema requires exactly one currency, but got: [JPY, USD]");
}
private static BigDecimal twoDigits(double num) {
return BigDecimal.valueOf((long) (num * 100.0), 2);
}
}

View File

@@ -3,6 +3,23 @@
This project contains Nomulus's Cloud SQL schema and schema-deployment
utilities.
### Database Roles and Privileges
Nomulus uses the 'postgres' database in the 'public' schema. The following
users/roles are defined:
* postgres: the initial user is used for admin and schema deployment.
* In Cloud SQL, we do not control superusers. The initial 'postgres' user
is a regular user with create-role/create-db privileges. Therefore,
it is not possible to separate admin user and schema-deployment user.
* readwrite is a role with read-write privileges on all data tables and
sequences. However, it does not have write access to admin tables. Nor
can it create new tables.
* The Registry server user is granted this role.
* readonly is a role with SELECT privileges on all tables.
* Reporting job user and individual human readers may be granted
this role.
### Schema DDL Scripts
Currently we use Flyway for schema deployment. Versioned incremental update
@@ -12,18 +29,18 @@ changes not yet deployed are pushed.
Below are the steps to submit a schema change:
* Define the incremental DDL script that would update the existing schema to
the new one.
* Add the script to the src/main/resource/flyway folder. Its name should
follow the V{id}__{description text}.sql, where {id} is a number that is
higher than all existing scripts in that folder. Also note that it is a
1. Write the incremental DDL script that makes your changes to the existing
schema. It should be stored in a new file in the
`db/src/main/resources/sql/flyway` folder using the naming pattern
`V{id}__{description text}.sql`, where `{id}` is the next highest number
following the existing scripts in that folder. Also note that it is a
**double** underscore in the naming pattern.
* Run the `:db:test` task from the Gradle root project. The SchemaTest will
2. Run the `:db:test` task from the Gradle root project. The SchemaTest will
fail because the new schema does not match the golden file.
* Copy db/build/resources/test/testcontainer/mount/dump.txt to the golden file
3. Copy db/build/resources/test/testcontainer/mount/dump.txt to the golden file
(db/src/main/resources/sql/schema/nomulus.golden.sql). Diff it against the
old version and verify that all changes are expected.
* Rerun the `:db:test` task. This time all tests should pass.
4. Rerun the `:db:test` task. This time all tests should pass.
Relevant files (under db/src/main/resources/sql/schema/):

View File

@@ -12,19 +12,45 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import com.google.common.collect.ImmutableList
import org.gradle.api.internal.tasks.userinput.UserInputHandler
plugins {
id "org.flywaydb.flyway" version "6.0.1"
id 'maven-publish'
}
ext {
Set restrictedDbEnv =
[ 'sandbox', 'production' ].asUnmodifiable()
Set allDbEnv =
[ 'alpha', 'crash' ].plus(restrictedDbEnv).asUnmodifiable()
def dbServerProperty = 'dbServer'
def dbNameProperty = 'dbName'
def dbServer = findProperty(dbServerProperty)
def dbServer = findProperty(dbServerProperty).toString().toLowerCase()
def dbName = findProperty(dbNameProperty)
reconfirmRestrictedDbEnv = {
if (!restrictedDbEnv.contains(dbServer)) {
return
}
// For restricted environments, ask the user to type again to confirm.
// The following statement uses Gradle internal API to get around the
// missing console bug when Gradle Daemon is in use. Another option is
// to use the ant.input task. For details please refer to
// https://github.com/gradle/gradle/issues/1251.
def dbServerAgain = services.get(UserInputHandler.class).askQuestion(
"""\
Are you sure? Operating on ${dbServer} from desktop is unsafe.
Please type '${dbServer}' again to proceed: """.stripIndent(),
'').trim()
if (dbServer != dbServerAgain) {
throw new RuntimeException(
"Failed to confirm for restricted database environment. Operation aborted.")
}
}
getAccessInfoByHostPort = { hostAndPort ->
return [
url: "jdbc:postgresql://${hostAndPort}/${dbName}",
@@ -32,8 +58,8 @@ ext {
password: findProperty('dbPassword')]
}
getSocketFactoryAccessInfo = {
def cred = getCloudSqlCredential('alpha', 'superuser').split(' ')
getSocketFactoryAccessInfo = { env ->
def cred = getCloudSqlCredential(env, 'admin').split(' ')
def sqlInstance = cred[0]
return [
url: """\
@@ -47,11 +73,10 @@ ext {
}
getJdbcAccessInfo = {
switch (dbServer.toString().toLowerCase()) {
case 'alpha':
return getSocketFactoryAccessInfo()
default:
return getAccessInfoByHostPort(dbServer)
if (allDbEnv.contains(dbServer)) {
return getSocketFactoryAccessInfo(dbServer)
} else {
return getAccessInfoByHostPort(dbServer)
}
}
@@ -63,18 +88,50 @@ ext {
// later).
getCloudSqlCredential = { env, role ->
env = env == 'production' ? '' : "-${env}"
def keyProject = env == '-crash'
? 'domain-registry-crash-kms-keys'
: "domain-registry${env}-keys"
def command =
"""gsutil cp \
gs://domain-registry${env}-cloudsql-credentials/${role}.enc - | \
gs://domain-registry${env}-cloudsql-credentials/${role}_credential.enc - | \
gcloud kms decrypt --location global --keyring nomulus \
--key sql-credentials-on-gcs-key --plaintext-file=- \
--ciphertext-file=- \
--project=domain-registry${env}-keys"""
--project=${keyProject}"""
return execInBash(command, '/tmp')
}
}
task schemaJar(type: Jar) {
archiveBaseName = 'schema'
from(sourceSets.main.resources) {
include 'sql/flyway/**'
include 'sql/schema/nomulus.golden.sql'
}
}
artifacts {
archives schemaJar
}
publishing {
repositories {
maven {
url project.schema_jar_repo
}
}
publications {
schemaOrmPublication(MavenPublication) {
groupId 'google.registry'
artifactId 'schema'
version project.schema_version
artifact schemaJar
}
}
}
flyway {
def accessInfo = project.ext.getJdbcAccessInfo()
@@ -86,6 +143,13 @@ flyway {
locations = [ "classpath:sql/flyway" ]
}
tasks.flywayMigrate.dependsOn(
tasks.create('confirmMigrateOnRestrictedDb') {
doLast {
project.ext.reconfirmRestrictedDbEnv()
}
})
dependencies {
def deps = rootProject.dependencyMap

View File

@@ -0,0 +1,16 @@
-- Copyright 2019 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.
create index if not exists idx_registry_lock_verification_code ON "RegistryLock"
using btree (verification_code);

View File

@@ -12,19 +12,6 @@
-- See the License for the specific language governing permissions and
-- limitations under the License.
CREATE TABLE "RegistryLock" (
revision_id BIGSERIAL NOT NULL,
action TEXT NOT NULL,
completion_timestamp TIMESTAMPTZ,
creation_timestamp TIMESTAMPTZ NOT NULL,
domain_name TEXT NOT NULL,
is_superuser BOOLEAN NOT NULL,
registrar_id TEXT NOT NULL,
registrar_poc_id TEXT,
repo_id TEXT NOT NULL,
verification_code TEXT NOT NULL,
PRIMARY KEY (revision_id)
);
alter table "PremiumList" add column if not exists name text not null;
ALTER TABLE IF EXISTS "RegistryLock"
ADD CONSTRAINT idx_registry_lock_repo_id_revision_id UNIQUE (repo_id, revision_id);
create index if not exists premiumlist_name_idx ON "PremiumList" (name);

View File

@@ -0,0 +1,15 @@
-- Copyright 2019 The Nomulus Authors. All Rights Reserved.
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
alter table "PremiumList" add column if not exists bloom_filter bytea not null;

View File

@@ -0,0 +1,15 @@
-- Copyright 2019 The Nomulus Authors. All Rights Reserved.
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
alter table "ClaimsList" add column if not exists tmdb_generation_time timestamp with time zone not null;

View File

@@ -130,8 +130,10 @@
create table "PremiumList" (
revision_id bigserial not null,
bloom_filter bytea not null,
creation_timestamp timestamptz not null,
currency bytea not null,
name text not null,
primary key (revision_id)
);
@@ -157,6 +159,7 @@
alter table if exists "Domain_GracePeriod"
add constraint UK_4ps2u4y8i5r91wu2n1x2xea28 unique (grace_periods_id);
create index premiumlist_name_idx on "PremiumList" (name);
alter table if exists "RegistryLock"
add constraint idx_registry_lock_repo_id_revision_id unique (repo_id, revision_id);

View File

@@ -50,7 +50,8 @@ CREATE TABLE public."ClaimsEntry" (
CREATE TABLE public."ClaimsList" (
revision_id bigint NOT NULL,
creation_timestamp timestamp with time zone NOT NULL
creation_timestamp timestamp with time zone NOT NULL,
tmdb_generation_time timestamp with time zone NOT NULL
);
@@ -91,7 +92,9 @@ CREATE TABLE public."PremiumEntry" (
CREATE TABLE public."PremiumList" (
revision_id bigint NOT NULL,
creation_timestamp timestamp with time zone NOT NULL,
currency bytea NOT NULL
currency bytea NOT NULL,
name text NOT NULL,
bloom_filter bytea NOT NULL
);
@@ -220,6 +223,20 @@ ALTER TABLE ONLY public."RegistryLock"
ADD CONSTRAINT idx_registry_lock_repo_id_revision_id UNIQUE (repo_id, revision_id);
--
-- Name: idx_registry_lock_verification_code; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX idx_registry_lock_verification_code ON public."RegistryLock" USING btree (verification_code);
--
-- Name: premiumlist_name_idx; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX premiumlist_name_idx ON public."PremiumList" USING btree (name);
--
-- Name: ClaimsEntry fk6sc6at5hedffc0nhdcab6ivuq; Type: FK CONSTRAINT; Schema: public; Owner: -
--

View File

@@ -12,10 +12,12 @@
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Script to create a user with read-only permission to all tables.
-- Script to create a user with read-only permission to all tables. The
-- initialize_roles.sql script creates the readonly role used here.
-- Comment out line below if user already exists:
CREATE USER :username ENCRYPTED PASSWORD :'password';
GRANT CONNECT ON DATABASE postgres TO :username;
GRANT USAGE ON SCHEMA public TO :username;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO :username;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO :username;
-- Comment out line above and uncomment line below if user has been created
-- from Cloud Dashboard:
-- ALTER USER :username NOCREATEDB NOCREATEROLE;
GRANT readonly TO :username;

View File

@@ -12,10 +12,13 @@
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Script to create a user with read-write permission to all tables.
-- Script to create a user with read-write permission to all tables (except for
-- WRITE permissions to flyway_schema_history). The initialize_roles.sql script
-- creates the readwrite role used here.
-- Comment out line below if user already exists:
CREATE USER :username ENCRYPTED PASSWORD :'password';
GRANT CONNECT ON DATABASE postgres TO :username;
GRANT USAGE ON SCHEMA public TO :username;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO :username;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO :username;
-- Comment out line above and uncomment line below if user has been created
-- from Cloud Dashboard:
-- ALTER USER :username NOCREATEDB NOCREATEROLE;
GRANT readwrite TO :username;

View File

@@ -14,9 +14,7 @@
--
-- Script to delete a user from the database.
REVOKE ALL PRIVILEGES ON DATABASE postgres FROM :username;
REVOKE ALL PRIVILEGES ON SCHEMA public FROM :username;
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM :username;
REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM :username;
REVOKE ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public FROM :username;
-- Ignore warnings like :username is not a member of role readonly/write.
REVOKE readonly FROM :username;
REVOKE readwrite FROM :username;
DROP USER :username;

View File

@@ -0,0 +1,48 @@
-- Copyright 2019 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.
--
-- Initializes roles and their privileges in the postgres database in Cloud SQL.
-- This script should run once under the **'postgres'** user before any other
-- roles or users are created.
-- Prevent backdoor grants through the implicit 'public' role.
REVOKE ALL PRIVILEGES ON SCHEMA public from public;
CREATE ROLE readonly;
GRANT CONNECT ON DATABASE postgres TO readonly;
GRANT USAGE ON SCHEMA public TO readonly;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO readonly;
ALTER DEFAULT PRIVILEGES
IN SCHEMA public
FOR USER postgres
GRANT USAGE, SELECT ON SEQUENCES TO readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly;
ALTER DEFAULT PRIVILEGES
IN SCHEMA public
FOR USER postgres
GRANT SELECT ON TABLES TO readonly;
CREATE ROLE readwrite;
GRANT CONNECT ON DATABASE postgres TO readwrite;
GRANT USAGE ON SCHEMA public TO readwrite;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO readwrite;
ALTER DEFAULT PRIVILEGES
IN SCHEMA public
FOR USER postgres
GRANT USAGE, SELECT ON SEQUENCES TO readwrite;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO readwrite;
ALTER DEFAULT PRIVILEGES
IN SCHEMA public
FOR USER postgres
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO readwrite;

View File

@@ -12,11 +12,9 @@
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Script to create a user with read-write permission to schema 'public' and
-- all tables.
-- Removes write privileges to Flyway admin table from roles.
-- This script is run once under 'postgres' after initialize_roles.sql
-- has been run AND the initial schema deployment by Flyway is done.
CREATE USER :username ENCRYPTED PASSWORD :'password';
GRANT CONNECT ON DATABASE postgres TO :username;
GRANT ALL PRIVILEGES ON SCHEMA public TO :username;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO :username;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO :username;
REVOKE INSERT, UPDATE, DELETE ON TABLE public.flyway_schema_history FROM readonly;
REVOKE INSERT, UPDATE, DELETE ON TABLE public.flyway_schema_history FROM readwrite;

View File

@@ -8,6 +8,12 @@ verboseTestOutput=false
flowDocsFile=
enableDependencyLocking=true
# Comma separated list of test patterns, if specified run only these.
testFilter=
# GAE Environment for deployment and staging.
environment=
# Cloud SQL properties
# A registry environment name (e.g., 'alpha') or a host[:port] string
@@ -15,3 +21,8 @@ dbServer=
dbName=postgres
dbUser=
dbPassword=
# Maven repository of the Cloud SQL schema jar, which contains the
# SQL DDL scripts.
schema_jar_repo=
schema_version=

View File

@@ -12,26 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
def environments = ['production', 'sandbox', 'alpha', 'crash']
// Mapping from environment names to GCP projects.
// Replace the values with the names of your deployment environments.
def projects = ['production': 'domain-registry',
'sandbox' : 'domain-registry-sandbox',
'alpha' : 'domain-registry-alpha',
'crash' : 'domain-registry-crash']
def environment = rootProject.findProperty("environment")
if (environment == null) {
environment = 'production'
}
def gcpProject = projects[environment]
if (gcpProject == null) {
throw new GradleException("-Penvironment must be one of ${environments}.")
}
rootProject.ext.environment = environment
rootProject.ext.gcpProject = gcpProject
rootProject.ext.prodOrSandboxEnv = ['production', 'sandbox'].contains(environment)
rootProject.ext.projects = ['production': 'your-production-project',
'sandbox' : 'your-sandbox-project',
'alpha' : 'your-alpha-project',
'crash' : 'your-crash-project']