mirror of
https://github.com/google/nomulus
synced 2026-05-21 15:21:48 +00:00
Compare commits
11 Commits
nomulus-20
...
nomulus-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e647d4e215 | ||
|
|
08471242df | ||
|
|
cd23fea698 | ||
|
|
ba54208dad | ||
|
|
b5e131ecba | ||
|
|
87e99f59bc | ||
|
|
30accea383 | ||
|
|
72e0101746 | ||
|
|
3090df9a78 | ||
|
|
7332b1fa38 | ||
|
|
9330e3a50d |
@@ -1,161 +0,0 @@
|
||||
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.batch;
|
||||
|
||||
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.persistence.PersistenceModule.SchemaManagerConnection;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.util.Retrier;
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.function.Supplier;
|
||||
import javax.inject.Inject;
|
||||
|
||||
/**
|
||||
* Wipes out all Cloud SQL data in a Nomulus GCP environment.
|
||||
*
|
||||
* <p>This class is created for the QA environment, where migration testing with production data
|
||||
* will happen. A regularly scheduled wipeout is a prerequisite to using production data there.
|
||||
*/
|
||||
@Action(
|
||||
service = Action.Service.BACKEND,
|
||||
path = "/_dr/task/wipeOutCloudSql",
|
||||
auth = Auth.AUTH_API_ADMIN)
|
||||
public class WipeOutCloudSqlAction implements Runnable {
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
private static final ImmutableSet<RegistryEnvironment> FORBIDDEN_ENVIRONMENTS =
|
||||
ImmutableSet.of(RegistryEnvironment.PRODUCTION, RegistryEnvironment.SANDBOX);
|
||||
|
||||
private final Supplier<Connection> connectionSupplier;
|
||||
private final Response response;
|
||||
private final Retrier retrier;
|
||||
|
||||
@Inject
|
||||
WipeOutCloudSqlAction(
|
||||
@SchemaManagerConnection Supplier<Connection> connectionSupplier,
|
||||
Response response,
|
||||
Retrier retrier) {
|
||||
this.connectionSupplier = connectionSupplier;
|
||||
this.response = response;
|
||||
this.retrier = retrier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
response.setContentType(PLAIN_TEXT_UTF_8);
|
||||
|
||||
if (FORBIDDEN_ENVIRONMENTS.contains(RegistryEnvironment.get())) {
|
||||
response.setStatus(SC_FORBIDDEN);
|
||||
response.setPayload("Wipeout is not allowed in " + RegistryEnvironment.get());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
retrier.callWithRetry(
|
||||
() -> {
|
||||
try (Connection conn = connectionSupplier.get()) {
|
||||
dropAllTables(conn, listTables(conn));
|
||||
dropAllSequences(conn, listSequences(conn));
|
||||
}
|
||||
return null;
|
||||
},
|
||||
e -> !(e instanceof SQLException));
|
||||
response.setStatus(SC_OK);
|
||||
response.setPayload("Wiped out Cloud SQL in " + RegistryEnvironment.get());
|
||||
} catch (RuntimeException e) {
|
||||
logger.atSevere().withCause(e).log("Failed to wipe out Cloud SQL data.");
|
||||
response.setStatus(SC_INTERNAL_SERVER_ERROR);
|
||||
response.setPayload("Failed to wipe out Cloud SQL in " + RegistryEnvironment.get());
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns a list of all tables in the public schema of a Postgresql database. */
|
||||
static ImmutableList<String> listTables(Connection connection) throws SQLException {
|
||||
try (ResultSet resultSet =
|
||||
connection.getMetaData().getTables(null, null, null, new String[] {"TABLE"})) {
|
||||
ImmutableList.Builder<String> tables = new ImmutableList.Builder<>();
|
||||
while (resultSet.next()) {
|
||||
String schema = resultSet.getString("TABLE_SCHEM");
|
||||
if (schema == null || !schema.equalsIgnoreCase("public")) {
|
||||
continue;
|
||||
}
|
||||
String tableName = resultSet.getString("TABLE_NAME");
|
||||
tables.add("public.\"" + tableName + "\"");
|
||||
}
|
||||
return tables.build();
|
||||
}
|
||||
}
|
||||
|
||||
static void dropAllTables(Connection conn, ImmutableList<String> tables) throws SQLException {
|
||||
if (tables.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (Statement statement = conn.createStatement()) {
|
||||
for (String table : tables) {
|
||||
statement.addBatch(String.format("DROP TABLE IF EXISTS %s CASCADE;", table));
|
||||
}
|
||||
for (int code : statement.executeBatch()) {
|
||||
if (code == Statement.EXECUTE_FAILED) {
|
||||
throw new RuntimeException("Failed to drop some tables. Please check.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns a list of all sequences in a Postgresql database. */
|
||||
static ImmutableList<String> listSequences(Connection conn) throws SQLException {
|
||||
try (Statement statement = conn.createStatement();
|
||||
ResultSet resultSet =
|
||||
statement.executeQuery("SELECT c.relname FROM pg_class c WHERE c.relkind = 'S';")) {
|
||||
ImmutableList.Builder<String> sequences = new ImmutableList.Builder<>();
|
||||
while (resultSet.next()) {
|
||||
sequences.add('\"' + resultSet.getString(1) + '\"');
|
||||
}
|
||||
return sequences.build();
|
||||
}
|
||||
}
|
||||
|
||||
static void dropAllSequences(Connection conn, ImmutableList<String> sequences)
|
||||
throws SQLException {
|
||||
if (sequences.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (Statement statement = conn.createStatement()) {
|
||||
for (String sequence : sequences) {
|
||||
statement.addBatch(String.format("DROP SEQUENCE IF EXISTS %s CASCADE;", sequence));
|
||||
}
|
||||
for (int code : statement.executeBatch()) {
|
||||
if (code == Statement.EXECUTE_FAILED) {
|
||||
throw new RuntimeException("Failed to drop some sequences. Please check.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,7 +209,7 @@ public final class RegistryJpaIO {
|
||||
|
||||
@ProcessElement
|
||||
public void processElement(OutputReceiver<T> outputReceiver) {
|
||||
tm().transactNoRetry(
|
||||
tm().transact(
|
||||
() -> {
|
||||
query.stream().map(resultMapper::apply).forEach(outputReceiver::output);
|
||||
});
|
||||
|
||||
@@ -1548,9 +1548,9 @@ public final class RegistryConfig {
|
||||
return CONFIG_SETTINGS.get().hibernate.connectionIsolation;
|
||||
}
|
||||
|
||||
/** Returns true if per-transaction isolation level is enabled. */
|
||||
public static boolean getHibernatePerTransactionIsolationEnabled() {
|
||||
return CONFIG_SETTINGS.get().hibernate.perTransactionIsolation;
|
||||
/** Returns true if nested calls to {@code tm().transact()} are allowed. */
|
||||
public static boolean getHibernateAllowNestedTransactions() {
|
||||
return CONFIG_SETTINGS.get().hibernate.allowNestedTransactions;
|
||||
}
|
||||
|
||||
/** Returns true if hibernate.show_sql is enabled. */
|
||||
|
||||
@@ -113,7 +113,7 @@ public class RegistryConfigSettings {
|
||||
|
||||
/** Configuration for Hibernate. */
|
||||
public static class Hibernate {
|
||||
public boolean perTransactionIsolation;
|
||||
public boolean allowNestedTransactions;
|
||||
public String connectionIsolation;
|
||||
public String logSqlQueries;
|
||||
public String hikariConnectionTimeout;
|
||||
|
||||
@@ -189,11 +189,13 @@ registryPolicy:
|
||||
sunriseDomainCreateDiscount: 0.15
|
||||
|
||||
hibernate:
|
||||
# Make it possible to specify the isolation level for each transaction. If set
|
||||
# to true, nested transactions will throw an exception. If set to false, a
|
||||
# transaction with the isolation override specified will still execute at the
|
||||
# default level (specified below).
|
||||
perTransactionIsolation: true
|
||||
# If set to false, calls to tm().transact() cannot be nested. If set to true,
|
||||
# nested calls to tm().transact() are allowed, as long as they do not specify
|
||||
# a transaction isolation level override. These nested transactions should
|
||||
# either be refactored to non-nested transactions, or changed to
|
||||
# tm().reTransact(), which explicitly allows nested transactions, but does not
|
||||
# allow setting an isolation level override.
|
||||
allowNestedTransactions: true
|
||||
|
||||
# Make 'SERIALIZABLE' the default isolation level to ensure correctness.
|
||||
#
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>backend</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>default</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>pubapi</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>tools</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>backend</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>default</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4_1G</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>pubapi</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>tools</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>backend</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>default</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4_1G</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>pubapi</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>tools</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>backend</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4_1G</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>default</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4_1G</instance-class>
|
||||
<manual-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>pubapi</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4_1G</instance-class>
|
||||
<manual-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>tools</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4_1G</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>backend</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>default</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>F4_1G</instance-class>
|
||||
<automatic-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>pubapi</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>tools</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>backend</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>default</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4_1G</instance-class>
|
||||
<manual-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>pubapi</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4_1G</instance-class>
|
||||
<manual-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>tools</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -66,7 +66,6 @@ import google.registry.model.domain.DomainCommand.Check;
|
||||
import google.registry.model.domain.fee.FeeCheckCommandExtension;
|
||||
import google.registry.model.domain.fee.FeeCheckCommandExtensionItem;
|
||||
import google.registry.model.domain.fee.FeeCheckResponseExtensionItem;
|
||||
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName;
|
||||
import google.registry.model.domain.fee06.FeeCheckCommandExtensionV06;
|
||||
import google.registry.model.domain.launch.LaunchCheckExtension;
|
||||
import google.registry.model.domain.token.AllocationToken;
|
||||
@@ -272,7 +271,7 @@ public final class DomainCheckFlow implements TransactionalFlow {
|
||||
ImmutableList.Builder<FeeCheckResponseExtensionItem> responseItems =
|
||||
new ImmutableList.Builder<>();
|
||||
ImmutableMap<String, Domain> domainObjs =
|
||||
loadDomainsForRestoreChecks(feeCheck, domainNames, existingDomains);
|
||||
loadDomainsForChecks(feeCheck, domainNames, existingDomains);
|
||||
ImmutableMap<String, BillingRecurrence> recurrences = loadRecurrencesForDomains(domainObjs);
|
||||
|
||||
for (FeeCheckCommandExtensionItem feeCheckItem : feeCheck.getItems()) {
|
||||
@@ -335,17 +334,20 @@ public final class DomainCheckFlow implements TransactionalFlow {
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and returns all existing domains that are having restore fees checked.
|
||||
* Loads and returns all existing domains that are having restore/renew/transfer fees checked.
|
||||
*
|
||||
* <p>This is necessary so that we can check their expiration dates to determine if a one-year
|
||||
* renewal is part of the cost of a restore.
|
||||
* <p>These need to be loaded for renews and transfers because there could be a relevant {@link
|
||||
* google.registry.model.billing.BillingBase.RenewalPriceBehavior} on the {@link
|
||||
* BillingRecurrence} affecting the price. They also need to be loaded for restores so that we can
|
||||
* check their expiration dates to determine if a one-year renewal is part of the cost of a
|
||||
* restore.
|
||||
*
|
||||
* <p>This may be resource-intensive for large checks of many restore fees, but those are
|
||||
* comparatively rare, and we are at least using an in-memory cache. Also, this will get a lot
|
||||
* nicer in Cloud SQL when we can SELECT just the fields we want rather than having to load the
|
||||
* entire entity.
|
||||
*/
|
||||
private ImmutableMap<String, Domain> loadDomainsForRestoreChecks(
|
||||
private ImmutableMap<String, Domain> loadDomainsForChecks(
|
||||
FeeCheckCommandExtension<?, ?> feeCheck,
|
||||
ImmutableMap<String, InternetDomainName> domainNames,
|
||||
ImmutableMap<String, VKey<Domain>> existingDomains) {
|
||||
@@ -354,18 +356,18 @@ public final class DomainCheckFlow implements TransactionalFlow {
|
||||
// The V06 fee extension supports specifying the command fees to check on a per-domain basis.
|
||||
restoreCheckDomains =
|
||||
feeCheck.getItems().stream()
|
||||
.filter(fc -> fc.getCommandName() == CommandName.RESTORE)
|
||||
.filter(fc -> fc.getCommandName().shouldLoadDomainForCheck())
|
||||
.map(FeeCheckCommandExtensionItem::getDomainName)
|
||||
.distinct()
|
||||
.collect(toImmutableList());
|
||||
} else if (feeCheck.getItems().stream()
|
||||
.anyMatch(fc -> fc.getCommandName() == CommandName.RESTORE)) {
|
||||
.anyMatch(fc -> fc.getCommandName().shouldLoadDomainForCheck())) {
|
||||
// The more recent fee extension versions support specifying the command fees to check only on
|
||||
// the overall domain check, not per-domain.
|
||||
restoreCheckDomains = ImmutableList.copyOf(domainNames.keySet());
|
||||
} else {
|
||||
// Fall-through case for more recent fee extension versions when the restore fee isn't being
|
||||
// checked.
|
||||
// Fall-through case for more recent fee extension versions when the restore/renew/transfer
|
||||
// fees aren't being checked.
|
||||
restoreCheckDomains = ImmutableList.of();
|
||||
}
|
||||
|
||||
|
||||
@@ -28,11 +28,17 @@ import google.registry.flows.EppException.CommandUseErrorException;
|
||||
import google.registry.flows.EppException.ParameterValuePolicyErrorException;
|
||||
import google.registry.flows.EppException.UnimplementedExtensionException;
|
||||
import google.registry.flows.EppException.UnimplementedObjectServiceException;
|
||||
import google.registry.flows.EppException.UnimplementedProtocolVersionException;
|
||||
import google.registry.flows.ExtensionManager;
|
||||
import google.registry.flows.FlowModule.RegistrarId;
|
||||
import google.registry.flows.FlowUtils.GenericXmlSyntaxErrorException;
|
||||
import google.registry.flows.MutatingFlow;
|
||||
import google.registry.flows.SessionMetadata;
|
||||
import google.registry.flows.TlsCredentials.BadRegistrarCertificateException;
|
||||
import google.registry.flows.TlsCredentials.BadRegistrarIpAddressException;
|
||||
import google.registry.flows.TlsCredentials.MissingRegistrarCertificateException;
|
||||
import google.registry.flows.TransportCredentials;
|
||||
import google.registry.flows.TransportCredentials.BadRegistrarPasswordException;
|
||||
import google.registry.model.eppcommon.ProtocolDefinition;
|
||||
import google.registry.model.eppcommon.ProtocolDefinition.ServiceExtension;
|
||||
import google.registry.model.eppinput.EppInput;
|
||||
@@ -41,6 +47,7 @@ import google.registry.model.eppinput.EppInput.Options;
|
||||
import google.registry.model.eppinput.EppInput.Services;
|
||||
import google.registry.model.eppoutput.EppResponse;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.util.PasswordUtils.HashAlgorithm;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import javax.inject.Inject;
|
||||
@@ -48,14 +55,14 @@ import javax.inject.Inject;
|
||||
/**
|
||||
* An EPP flow for login.
|
||||
*
|
||||
* @error {@link google.registry.flows.EppException.UnimplementedExtensionException}
|
||||
* @error {@link google.registry.flows.EppException.UnimplementedObjectServiceException}
|
||||
* @error {@link google.registry.flows.EppException.UnimplementedProtocolVersionException}
|
||||
* @error {@link google.registry.flows.FlowUtils.GenericXmlSyntaxErrorException}
|
||||
* @error {@link google.registry.flows.TlsCredentials.BadRegistrarCertificateException}
|
||||
* @error {@link google.registry.flows.TlsCredentials.BadRegistrarIpAddressException}
|
||||
* @error {@link google.registry.flows.TlsCredentials.MissingRegistrarCertificateException}
|
||||
* @error {@link google.registry.flows.TransportCredentials.BadRegistrarPasswordException}
|
||||
* @error {@link UnimplementedExtensionException}
|
||||
* @error {@link UnimplementedObjectServiceException}
|
||||
* @error {@link UnimplementedProtocolVersionException}
|
||||
* @error {@link GenericXmlSyntaxErrorException}
|
||||
* @error {@link BadRegistrarCertificateException}
|
||||
* @error {@link BadRegistrarIpAddressException}
|
||||
* @error {@link MissingRegistrarCertificateException}
|
||||
* @error {@link BadRegistrarPasswordException}
|
||||
* @error {@link LoginFlow.AlreadyLoggedInException}
|
||||
* @error {@link BadRegistrarIdException}
|
||||
* @error {@link LoginFlow.TooManyFailedLoginsException}
|
||||
@@ -134,13 +141,24 @@ public class LoginFlow implements MutatingFlow {
|
||||
if (!registrar.get().isLive()) {
|
||||
throw new RegistrarAccountNotActiveException();
|
||||
}
|
||||
if (login.getNewPassword().isPresent()) {
|
||||
|
||||
if (login.getNewPassword().isPresent()
|
||||
|| registrar.get().getCurrentHashAlgorithm(login.getPassword()).orElse(null)
|
||||
!= HashAlgorithm.SCRYPT) {
|
||||
String newPassword =
|
||||
login
|
||||
.getNewPassword()
|
||||
.orElseGet(
|
||||
() -> {
|
||||
logger.atInfo().log("Rehashing existing registrar password with Scrypt");
|
||||
return login.getPassword();
|
||||
});
|
||||
// Load fresh from database (bypassing the cache) to ensure we don't save stale data.
|
||||
Optional<Registrar> freshRegistrar = Registrar.loadByRegistrarId(login.getClientId());
|
||||
if (!freshRegistrar.isPresent()) {
|
||||
throw new BadRegistrarIdException(login.getClientId());
|
||||
}
|
||||
tm().put(freshRegistrar.get().asBuilder().setPassword(login.getNewPassword().get()).build());
|
||||
tm().put(freshRegistrar.get().asBuilder().setPassword(newPassword).build());
|
||||
}
|
||||
|
||||
// We are in!
|
||||
@@ -152,35 +170,35 @@ public class LoginFlow implements MutatingFlow {
|
||||
|
||||
/** Registrar with this ID could not be found. */
|
||||
static class BadRegistrarIdException extends AuthenticationErrorException {
|
||||
public BadRegistrarIdException(String registrarId) {
|
||||
BadRegistrarIdException(String registrarId) {
|
||||
super("Registrar with this ID could not be found: " + registrarId);
|
||||
}
|
||||
}
|
||||
|
||||
/** Registrar login failed too many times. */
|
||||
static class TooManyFailedLoginsException extends AuthenticationErrorClosingConnectionException {
|
||||
public TooManyFailedLoginsException() {
|
||||
TooManyFailedLoginsException() {
|
||||
super("Registrar login failed too many times");
|
||||
}
|
||||
}
|
||||
|
||||
/** Registrar account is not active. */
|
||||
static class RegistrarAccountNotActiveException extends AuthorizationErrorException {
|
||||
public RegistrarAccountNotActiveException() {
|
||||
RegistrarAccountNotActiveException() {
|
||||
super("Registrar account is not active");
|
||||
}
|
||||
}
|
||||
|
||||
/** Registrar is already logged in. */
|
||||
static class AlreadyLoggedInException extends CommandUseErrorException {
|
||||
public AlreadyLoggedInException() {
|
||||
AlreadyLoggedInException() {
|
||||
super("Registrar is already logged in");
|
||||
}
|
||||
}
|
||||
|
||||
/** Specified language is not supported. */
|
||||
static class UnsupportedLanguageException extends ParameterValuePolicyErrorException {
|
||||
public UnsupportedLanguageException() {
|
||||
UnsupportedLanguageException() {
|
||||
super("Specified language is not supported");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ public final class InMemoryKeyring implements Keyring {
|
||||
private final String marksdbDnlLoginAndPassword;
|
||||
private final String marksdbLordnPassword;
|
||||
private final String marksdbSmdrlLoginAndPassword;
|
||||
private final String jsonCredential;
|
||||
private final String bsaApiKey;
|
||||
|
||||
public InMemoryKeyring(
|
||||
PGPKeyPair rdeStagingKey,
|
||||
@@ -53,9 +53,9 @@ public final class InMemoryKeyring implements Keyring {
|
||||
String marksdbDnlLoginAndPassword,
|
||||
String marksdbLordnPassword,
|
||||
String marksdbSmdrlLoginAndPassword,
|
||||
String jsonCredential,
|
||||
String cloudSqlPassword,
|
||||
String toolsCloudSqlPassword) {
|
||||
String toolsCloudSqlPassword,
|
||||
String bsaApiKey) {
|
||||
checkArgument(PgpHelper.isSigningKey(rdeSigningKey.getPublicKey()),
|
||||
"RDE signing key must support signing: %s", rdeSigningKey.getKeyID());
|
||||
checkArgument(rdeStagingKey.getPublicKey().isEncryptionKey(),
|
||||
@@ -80,7 +80,7 @@ public final class InMemoryKeyring implements Keyring {
|
||||
this.marksdbLordnPassword = checkNotNull(marksdbLordnPassword, "marksdbLordnPassword");
|
||||
this.marksdbSmdrlLoginAndPassword =
|
||||
checkNotNull(marksdbSmdrlLoginAndPassword, "marksdbSmdrlLoginAndPassword");
|
||||
this.jsonCredential = checkNotNull(jsonCredential, "jsonCredential");
|
||||
this.bsaApiKey = checkNotNull(bsaApiKey, "bsaApiKey");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -149,8 +149,8 @@ public final class InMemoryKeyring implements Keyring {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getJsonCredential() {
|
||||
return jsonCredential;
|
||||
public String getBsaApiKey() {
|
||||
return bsaApiKey;
|
||||
}
|
||||
|
||||
/** Does nothing. */
|
||||
|
||||
@@ -145,11 +145,8 @@ public interface Keyring extends AutoCloseable {
|
||||
*/
|
||||
String getMarksdbSmdrlLoginAndPassword();
|
||||
|
||||
/**
|
||||
* Returns the credentials for a service account on the Google AppEngine project downloaded from
|
||||
* the Cloud Console dashboard in JSON format.
|
||||
*/
|
||||
String getJsonCredential();
|
||||
/** Returns the API_KEY for authentication with the BSA portal. */
|
||||
String getBsaApiKey();
|
||||
|
||||
// Don't throw so try-with-resources works better.
|
||||
@Override
|
||||
|
||||
@@ -58,8 +58,8 @@ public class SecretManagerKeyring implements Keyring {
|
||||
/** Key labels for string secrets. */
|
||||
enum StringKeyLabel {
|
||||
SAFE_BROWSING_API_KEY,
|
||||
BSA_API_KEY_STRING,
|
||||
ICANN_REPORTING_PASSWORD_STRING,
|
||||
JSON_CREDENTIAL_STRING,
|
||||
MARKSDB_DNL_LOGIN_STRING,
|
||||
MARKSDB_LORDN_PASSWORD_STRING,
|
||||
MARKSDB_SMDRL_LOGIN_STRING,
|
||||
@@ -143,10 +143,9 @@ public class SecretManagerKeyring implements Keyring {
|
||||
return getString(StringKeyLabel.MARKSDB_SMDRL_LOGIN_STRING);
|
||||
}
|
||||
|
||||
// TODO(b/237305940): remove this method and all supports, including entry in secretmanager
|
||||
@Override
|
||||
public String getJsonCredential() {
|
||||
return getString(StringKeyLabel.JSON_CREDENTIAL_STRING);
|
||||
public String getBsaApiKey() {
|
||||
return getString(StringKeyLabel.BSA_API_KEY_STRING);
|
||||
}
|
||||
|
||||
/** No persistent resources are maintained for this Keyring implementation. */
|
||||
|
||||
@@ -24,8 +24,8 @@ import static google.registry.keyring.secretmanager.SecretManagerKeyring.PublicK
|
||||
import static google.registry.keyring.secretmanager.SecretManagerKeyring.PublicKeyLabel.RDE_RECEIVER_PUBLIC;
|
||||
import static google.registry.keyring.secretmanager.SecretManagerKeyring.PublicKeyLabel.RDE_SIGNING_PUBLIC;
|
||||
import static google.registry.keyring.secretmanager.SecretManagerKeyring.PublicKeyLabel.RDE_STAGING_PUBLIC;
|
||||
import static google.registry.keyring.secretmanager.SecretManagerKeyring.StringKeyLabel.BSA_API_KEY_STRING;
|
||||
import static google.registry.keyring.secretmanager.SecretManagerKeyring.StringKeyLabel.ICANN_REPORTING_PASSWORD_STRING;
|
||||
import static google.registry.keyring.secretmanager.SecretManagerKeyring.StringKeyLabel.JSON_CREDENTIAL_STRING;
|
||||
import static google.registry.keyring.secretmanager.SecretManagerKeyring.StringKeyLabel.MARKSDB_DNL_LOGIN_STRING;
|
||||
import static google.registry.keyring.secretmanager.SecretManagerKeyring.StringKeyLabel.MARKSDB_LORDN_PASSWORD_STRING;
|
||||
import static google.registry.keyring.secretmanager.SecretManagerKeyring.StringKeyLabel.MARKSDB_SMDRL_LOGIN_STRING;
|
||||
@@ -120,8 +120,8 @@ public final class SecretManagerKeyringUpdater {
|
||||
return setString(login, MARKSDB_SMDRL_LOGIN_STRING);
|
||||
}
|
||||
|
||||
public SecretManagerKeyringUpdater setJsonCredential(String credential) {
|
||||
return setString(credential, JSON_CREDENTIAL_STRING);
|
||||
public SecretManagerKeyringUpdater setBsaApiKey(String credential) {
|
||||
return setString(credential, BSA_API_KEY_STRING);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright 2023 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.adapters;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.TypeAdapterFactory;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
/**
|
||||
* Adapter factory that allows for (de)serialization of Class objects in GSON.
|
||||
*
|
||||
* <p>GSON's built-in adapter for Class objects throws an exception, but there are situations where
|
||||
* we want to (de)serialize these, such as in VKeys. This instructs GSON to look for our custom
|
||||
* {@link ClassTypeAdapter} rather than the default.
|
||||
*/
|
||||
public class ClassProcessingTypeAdapterFactory implements TypeAdapterFactory {
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
|
||||
if (Class.class.isAssignableFrom(typeToken.getRawType())) {
|
||||
// in this case, T is a class object
|
||||
return (TypeAdapter<T>) new ClassTypeAdapter();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright 2023 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.adapters;
|
||||
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* TypeAdapter for {@link Class} objects.
|
||||
*
|
||||
* <p>GSON's default adapter doesn't allow this, but we want to allow for (de)serialization of Class
|
||||
* objects for containers like VKeys using the full name of the class.
|
||||
*/
|
||||
public class ClassTypeAdapter extends TypeAdapter<Class<?>> {
|
||||
|
||||
@Override
|
||||
public void write(JsonWriter out, Class value) throws IOException {
|
||||
out.value(value.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> read(JsonReader reader) throws IOException {
|
||||
String stringValue = reader.nextString();
|
||||
if (stringValue.equals("null")) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Class.forName(stringValue);
|
||||
} catch (ClassNotFoundException e) {
|
||||
// this should not happen...
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright 2023 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.adapters;
|
||||
|
||||
import google.registry.util.StringBaseTypeAdapter;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* TypeAdapter for {@link Serializable} objects.
|
||||
*
|
||||
* <p>VKey keys (primary keys in SQL) are usually represented by either a long or a String. There
|
||||
* are a couple situations (CursorId, HistoryEntryId) where the Serializable in question is a
|
||||
* complex object, but we do not need to worry about (de)serializing those objects to/from JSON.
|
||||
*/
|
||||
public class SerializableJsonTypeAdapter extends StringBaseTypeAdapter<Serializable> {
|
||||
|
||||
@Override
|
||||
protected Serializable fromString(String stringValue) throws IOException {
|
||||
try {
|
||||
return Long.parseLong(stringValue);
|
||||
} catch (NumberFormatException e) {
|
||||
return stringValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
import google.registry.model.Buildable;
|
||||
import google.registry.model.UpdateAutoTimestampEntity;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.util.PasswordUtils;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
@@ -84,8 +85,9 @@ public class User extends UpdateAutoTimestampEntity implements Buildable {
|
||||
|| isNullOrEmpty(registryLockPasswordHash)) {
|
||||
return false;
|
||||
}
|
||||
return hashPassword(registryLockPassword, registryLockPasswordSalt)
|
||||
.equals(registryLockPasswordHash);
|
||||
return PasswordUtils.verifyPassword(
|
||||
registryLockPassword, registryLockPasswordHash, registryLockPasswordSalt)
|
||||
.isPresent();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,9 +156,9 @@ public class User extends UpdateAutoTimestampEntity implements Buildable {
|
||||
!getInstance().hasRegistryLockPassword(), "User already has a password, remove it first");
|
||||
checkArgument(
|
||||
!isNullOrEmpty(registryLockPassword), "Registry lock password was null or empty");
|
||||
getInstance().registryLockPasswordSalt = base64().encode(SALT_SUPPLIER.get());
|
||||
getInstance().registryLockPasswordHash =
|
||||
hashPassword(registryLockPassword, getInstance().registryLockPasswordSalt);
|
||||
byte[] salt = SALT_SUPPLIER.get();
|
||||
getInstance().registryLockPasswordSalt = base64().encode(salt);
|
||||
getInstance().registryLockPasswordHash = hashPassword(registryLockPassword, salt);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,14 +128,14 @@ public class DomainBase extends EppResource
|
||||
String tld;
|
||||
|
||||
/** References to hosts that are the nameservers for the domain. */
|
||||
@Transient Set<VKey<Host>> nsHosts;
|
||||
@Expose @Transient Set<VKey<Host>> nsHosts;
|
||||
|
||||
/** Contacts. */
|
||||
VKey<Contact> adminContact;
|
||||
@Expose VKey<Contact> adminContact;
|
||||
|
||||
VKey<Contact> billingContact;
|
||||
VKey<Contact> techContact;
|
||||
VKey<Contact> registrantContact;
|
||||
@Expose VKey<Contact> billingContact;
|
||||
@Expose VKey<Contact> techContact;
|
||||
@Expose VKey<Contact> registrantContact;
|
||||
|
||||
/** Authorization info (aka transfer secret) of the domain. */
|
||||
@Embedded
|
||||
|
||||
@@ -34,12 +34,14 @@ public abstract class FeeQueryCommandExtensionItem extends ImmutableObject {
|
||||
|
||||
/** The name of a command that might have an associated fee. */
|
||||
public enum CommandName {
|
||||
UNKNOWN,
|
||||
CREATE,
|
||||
RENEW,
|
||||
TRANSFER,
|
||||
RESTORE,
|
||||
UPDATE;
|
||||
UNKNOWN(false),
|
||||
CREATE(false),
|
||||
RENEW(true),
|
||||
TRANSFER(true),
|
||||
RESTORE(true),
|
||||
UPDATE(false);
|
||||
|
||||
private final boolean loadDomainForCheck;
|
||||
|
||||
public static CommandName parseKnownCommand(String string) {
|
||||
try {
|
||||
@@ -52,6 +54,14 @@ public abstract class FeeQueryCommandExtensionItem extends ImmutableObject {
|
||||
+ " UPDATE");
|
||||
}
|
||||
}
|
||||
|
||||
CommandName(boolean loadDomainForCheck) {
|
||||
this.loadDomainForCheck = loadDomainForCheck;
|
||||
}
|
||||
|
||||
public boolean shouldLoadDomainForCheck() {
|
||||
return this.loadDomainForCheck;
|
||||
}
|
||||
}
|
||||
|
||||
/** The default validity period (if not specified) is 1 year for all operations. */
|
||||
|
||||
@@ -60,6 +60,8 @@ import google.registry.model.tld.Tld;
|
||||
import google.registry.model.tld.Tld.TldType;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.util.CidrAddressBlock;
|
||||
import google.registry.util.PasswordUtils;
|
||||
import google.registry.util.PasswordUtils.HashAlgorithm;
|
||||
import java.security.cert.CertificateParsingException;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
@@ -97,7 +99,7 @@ import org.joda.time.DateTime;
|
||||
column = @Column(nullable = false, name = "lastUpdateTime"))
|
||||
public class Registrar extends UpdateAutoTimestampEntity implements Buildable, Jsonifiable {
|
||||
|
||||
/** Represents the type of a registrar entity. */
|
||||
/** Represents the type of registrar entity. */
|
||||
public enum Type {
|
||||
/** A real-world, third-party registrar. Should have non-null IANA and billing account IDs. */
|
||||
REAL(Objects::nonNull),
|
||||
@@ -376,7 +378,7 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
|
||||
*/
|
||||
@Expose String icannReferralEmail;
|
||||
|
||||
/** Id of the folder in drive used to publish information for this registrar. */
|
||||
/** ID of the folder in drive used to publish information for this registrar. */
|
||||
@Expose String driveFolderId;
|
||||
|
||||
// Metadata.
|
||||
@@ -639,7 +641,11 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
|
||||
}
|
||||
|
||||
public boolean verifyPassword(String password) {
|
||||
return hashPassword(password, salt).equals(passwordHash);
|
||||
return getCurrentHashAlgorithm(password).isPresent();
|
||||
}
|
||||
|
||||
public Optional<HashAlgorithm> getCurrentHashAlgorithm(String password) {
|
||||
return PasswordUtils.verifyPassword(password, passwordHash, salt);
|
||||
}
|
||||
|
||||
public String getPhonePasscode() {
|
||||
@@ -861,8 +867,9 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
|
||||
checkArgument(
|
||||
Range.closed(6, 16).contains(nullToEmpty(password).length()),
|
||||
"Password must be 6-16 characters long.");
|
||||
getInstance().salt = base64().encode(SALT_SUPPLIER.get());
|
||||
getInstance().passwordHash = hashPassword(password, getInstance().salt);
|
||||
byte[] salt = SALT_SUPPLIER.get();
|
||||
getInstance().salt = base64().encode(salt);
|
||||
getInstance().passwordHash = hashPassword(password, salt);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,8 @@ import google.registry.model.Jsonifiable;
|
||||
import google.registry.model.UnsafeSerializable;
|
||||
import google.registry.model.registrar.RegistrarPoc.RegistrarPocId;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.util.PasswordUtils;
|
||||
import google.registry.util.PasswordUtils.HashAlgorithm;
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -240,8 +242,12 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
|
||||
|| isNullOrEmpty(registryLockPasswordHash)) {
|
||||
return false;
|
||||
}
|
||||
return hashPassword(registryLockPassword, registryLockPasswordSalt)
|
||||
.equals(registryLockPasswordHash);
|
||||
return getCurrentHashAlgorithm(registryLockPassword).isPresent();
|
||||
}
|
||||
|
||||
public Optional<HashAlgorithm> getCurrentHashAlgorithm(String registryLockPassword) {
|
||||
return PasswordUtils.verifyPassword(
|
||||
registryLockPassword, registryLockPasswordHash, registryLockPasswordSalt);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -436,9 +442,9 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
|
||||
"Not allowed to set registry lock password for this contact");
|
||||
checkArgument(
|
||||
!isNullOrEmpty(registryLockPassword), "Registry lock password was null or empty");
|
||||
getInstance().registryLockPasswordSalt = base64().encode(SALT_SUPPLIER.get());
|
||||
getInstance().registryLockPasswordHash =
|
||||
hashPassword(registryLockPassword, getInstance().registryLockPasswordSalt);
|
||||
byte[] salt = SALT_SUPPLIER.get();
|
||||
getInstance().registryLockPasswordSalt = base64().encode(salt);
|
||||
getInstance().registryLockPasswordHash = hashPassword(registryLockPassword, salt);
|
||||
getInstance().allowedToSetRegistryLockPassword = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -25,9 +25,10 @@ import com.google.common.base.Strings;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.persistence.transaction.TransactionManager.ThrowingRunnable;
|
||||
import java.io.Serializable;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.concurrent.Callable;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
@@ -184,7 +185,7 @@ public class Lock extends ImmutableObject implements Serializable {
|
||||
public static Optional<Lock> acquire(
|
||||
String resourceName, @Nullable String tld, Duration leaseLength) {
|
||||
String scope = tld != null ? tld : GLOBAL;
|
||||
Supplier<AcquireResult> lockAcquirer =
|
||||
Callable<AcquireResult> lockAcquirer =
|
||||
() -> {
|
||||
DateTime now = tm().getTransactionTime();
|
||||
|
||||
@@ -221,7 +222,7 @@ public class Lock extends ImmutableObject implements Serializable {
|
||||
/** Release the lock. */
|
||||
public void release() {
|
||||
// Just use the default clock because we aren't actually doing anything that will use the clock.
|
||||
Supplier<Void> lockReleaser =
|
||||
ThrowingRunnable lockReleaser =
|
||||
() -> {
|
||||
// To release a lock, check that no one else has already obtained it and if not
|
||||
// delete it. If the lock in the database was different, then this lock is gone already;
|
||||
@@ -246,7 +247,6 @@ public class Lock extends ImmutableObject implements Serializable {
|
||||
logger.atInfo().log(
|
||||
"Not deleting lock: %s - someone else has it: %s", lockId, loadedLock);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
tm().transact(lockReleaser);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ import google.registry.batch.RelockDomainAction;
|
||||
import google.registry.batch.ResaveAllEppResourcesPipelineAction;
|
||||
import google.registry.batch.ResaveEntityAction;
|
||||
import google.registry.batch.SendExpiringCertificateNotificationEmailAction;
|
||||
import google.registry.batch.WipeOutCloudSqlAction;
|
||||
import google.registry.batch.WipeOutContactHistoryPiiAction;
|
||||
import google.registry.cron.CronModule;
|
||||
import google.registry.cron.TldFanoutAction;
|
||||
@@ -178,8 +177,6 @@ interface BackendRequestComponent {
|
||||
|
||||
UpdateRegistrarRdapBaseUrlsAction updateRegistrarRdapBaseUrlsAction();
|
||||
|
||||
WipeOutCloudSqlAction wipeOutCloudSqlAction();
|
||||
|
||||
WipeOutContactHistoryPiiAction wipeOutContactHistoryPiiAction();
|
||||
|
||||
@Subcomponent.Builder
|
||||
|
||||
@@ -57,7 +57,7 @@ public class VKey<T> extends ImmutableObject implements Serializable {
|
||||
// The primary key for the referenced entity.
|
||||
@Expose Serializable key;
|
||||
|
||||
Class<? extends T> kind;
|
||||
@Expose Class<? extends T> kind;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
VKey() {}
|
||||
|
||||
@@ -62,7 +62,7 @@ class DatabaseException extends PersistenceException {
|
||||
* <p>If the {@code original Throwable} has at least one {@link SQLException} in its chain of
|
||||
* causes, a {@link DatabaseException} is thrown; otherwise this does nothing.
|
||||
*/
|
||||
static void tryWrapAndThrow(Throwable original) {
|
||||
static void throwIfSqlException(Throwable original) {
|
||||
Throwable t = original;
|
||||
do {
|
||||
if (t instanceof SQLException) {
|
||||
|
||||
@@ -16,7 +16,6 @@ package google.registry.persistence.transaction;
|
||||
|
||||
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
|
||||
import google.registry.persistence.VKey;
|
||||
import java.util.function.Supplier;
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.Query;
|
||||
import javax.persistence.TypedQuery;
|
||||
@@ -62,24 +61,6 @@ public interface JpaTransactionManager extends TransactionManager {
|
||||
*/
|
||||
Query query(String sqlString);
|
||||
|
||||
/** Executes the work in a transaction with no retries and returns the result. */
|
||||
<T> T transactNoRetry(Supplier<T> work);
|
||||
|
||||
/**
|
||||
* Executes the work in a transaction at the given {@link TransactionIsolationLevel} with no
|
||||
* retries and returns the result.
|
||||
*/
|
||||
<T> T transactNoRetry(Supplier<T> work, TransactionIsolationLevel isolationLevel);
|
||||
|
||||
/** Executes the work in a transaction with no retries. */
|
||||
void transactNoRetry(Runnable work);
|
||||
|
||||
/**
|
||||
* Executes the work in a transaction at the given {@link TransactionIsolationLevel} with no
|
||||
* retries.
|
||||
*/
|
||||
void transactNoRetry(Runnable work, TransactionIsolationLevel isolationLevel);
|
||||
|
||||
/** Deletes the entity by its id, throws exception if the entity is not deleted. */
|
||||
<T> void assertDelete(VKey<T> key);
|
||||
|
||||
@@ -103,7 +84,4 @@ public interface JpaTransactionManager extends TransactionManager {
|
||||
|
||||
/** Return the {@link TransactionIsolationLevel} used in the current transaction. */
|
||||
TransactionIsolationLevel getCurrentTransactionIsolationLevel();
|
||||
|
||||
/** Asserts that the current transaction runs at the given level. */
|
||||
void assertTransactionIsolationLevel(TransactionIsolationLevel expectedLevel);
|
||||
}
|
||||
|
||||
@@ -15,11 +15,12 @@
|
||||
package google.registry.persistence.transaction;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Throwables.throwIfUnchecked;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.collect.ImmutableMap.toImmutableMap;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static google.registry.config.RegistryConfig.getHibernatePerTransactionIsolationEnabled;
|
||||
import static google.registry.persistence.transaction.DatabaseException.tryWrapAndThrow;
|
||||
import static google.registry.config.RegistryConfig.getHibernateAllowNestedTransactions;
|
||||
import static google.registry.persistence.transaction.DatabaseException.throwIfSqlException;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
import static java.util.AbstractMap.SimpleEntry;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
@@ -31,6 +32,7 @@ import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Streams;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.flogger.StackSize;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.persistence.JpaRetries;
|
||||
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
|
||||
@@ -52,7 +54,7 @@ import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
import javax.annotation.Nullable;
|
||||
@@ -76,6 +78,9 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
private static final Retrier retrier = new Retrier(new SystemSleeper(), 3);
|
||||
private static final String NESTED_TRANSACTION_MESSAGE =
|
||||
"Nested transaction detected. Try refactoring to avoid nested transactions. If unachievable,"
|
||||
+ " use reTransact() in nested transactions";
|
||||
|
||||
// EntityManagerFactory is thread safe.
|
||||
private final EntityManagerFactory emf;
|
||||
@@ -138,21 +143,23 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assertTransactionIsolationLevel(TransactionIsolationLevel expectedLevel) {
|
||||
assertInTransaction();
|
||||
TransactionIsolationLevel currentLevel = getCurrentTransactionIsolationLevel();
|
||||
if (currentLevel != expectedLevel) {
|
||||
throw new IllegalStateException(
|
||||
String.format(
|
||||
"Current transaction isolation level (%s) is not as expected (%s)",
|
||||
currentLevel, expectedLevel));
|
||||
public <T> T reTransact(Callable<T> work) {
|
||||
// This prevents inner transaction from retrying, thus avoiding a cascade retry effect.
|
||||
if (inTransaction()) {
|
||||
return transactNoRetry(work, null);
|
||||
}
|
||||
return retrier.callWithRetry(
|
||||
() -> transactNoRetry(work, null), JpaRetries::isFailedTxnRetriable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T transact(Supplier<T> work, TransactionIsolationLevel isolationLevel) {
|
||||
// This prevents inner transaction from retrying, thus avoiding a cascade retry effect.
|
||||
public <T> T transact(Callable<T> work, TransactionIsolationLevel isolationLevel) {
|
||||
if (inTransaction()) {
|
||||
if (!getHibernateAllowNestedTransactions()) {
|
||||
throw new IllegalStateException(NESTED_TRANSACTION_MESSAGE);
|
||||
}
|
||||
logger.atWarning().withStackTrace(StackSize.MEDIUM).log(NESTED_TRANSACTION_MESSAGE);
|
||||
// This prevents inner transaction from retrying, thus avoiding a cascade retry effect.
|
||||
return transactNoRetry(work, isolationLevel);
|
||||
}
|
||||
return retrier.callWithRetry(
|
||||
@@ -160,30 +167,32 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T reTransact(Supplier<T> work) {
|
||||
return transact(work);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T transact(Supplier<T> work) {
|
||||
public <T> T transact(Callable<T> work) {
|
||||
return transact(work, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T transactNoRetry(
|
||||
Supplier<T> work, @Nullable TransactionIsolationLevel isolationLevel) {
|
||||
Callable<T> work, @Nullable TransactionIsolationLevel isolationLevel) {
|
||||
if (inTransaction()) {
|
||||
if (isolationLevel != null && getHibernatePerTransactionIsolationEnabled()) {
|
||||
TransactionIsolationLevel enclosingLevel = getCurrentTransactionIsolationLevel();
|
||||
if (isolationLevel != enclosingLevel) {
|
||||
throw new IllegalStateException(
|
||||
String.format(
|
||||
"Isolation level conflict detected in nested transactions.\n"
|
||||
+ "Enclosing transaction: %s\nCurrent transaction: %s",
|
||||
enclosingLevel, isolationLevel));
|
||||
}
|
||||
// This check will no longer be necessary when the transact() method always throws
|
||||
// inside a nested transaction, as the only way to pass a non-null isolation level
|
||||
// is by calling the transact() method (and its variants), which would have already
|
||||
// thrown before calling transactNoRetry() when inside a nested transaction.
|
||||
//
|
||||
// For now, we still need it, so we don't accidentally call a nested transact() with an
|
||||
// isolation level override. This buys us time to detect nested transact() calls and either
|
||||
// remove them or change the call site to reTransact().
|
||||
if (isolationLevel != null) {
|
||||
throw new IllegalStateException(
|
||||
"Transaction isolation level cannot be specified for nested transactions");
|
||||
}
|
||||
try {
|
||||
return work.call();
|
||||
} catch (Exception e) {
|
||||
throwIfSqlException(e);
|
||||
throwIfUnchecked(e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return work.get();
|
||||
}
|
||||
TransactionInfo txnInfo = transactionInfo.get();
|
||||
txnInfo.entityManager = emf.createEntityManager();
|
||||
@@ -191,43 +200,36 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
try {
|
||||
txn.begin();
|
||||
txnInfo.start(clock);
|
||||
if (isolationLevel != null) {
|
||||
if (getHibernatePerTransactionIsolationEnabled()) {
|
||||
getEntityManager()
|
||||
.createNativeQuery(
|
||||
String.format("SET TRANSACTION ISOLATION LEVEL %s", isolationLevel.getMode()))
|
||||
.executeUpdate();
|
||||
logger.atInfo().log("Running transaction at %s", isolationLevel);
|
||||
} else {
|
||||
logger.atWarning().log(
|
||||
"Per-transaction isolation level disabled, but %s was requested", isolationLevel);
|
||||
}
|
||||
if (isolationLevel != null && isolationLevel != getDefaultTransactionIsolationLevel()) {
|
||||
getEntityManager()
|
||||
.createNativeQuery(
|
||||
String.format("SET TRANSACTION ISOLATION LEVEL %s", isolationLevel.getMode()))
|
||||
.executeUpdate();
|
||||
logger.atInfo().log(
|
||||
"Overriding transaction isolation level from %s to %s",
|
||||
getDefaultTransactionIsolationLevel(), isolationLevel);
|
||||
}
|
||||
T result = work.get();
|
||||
T result = work.call();
|
||||
txn.commit();
|
||||
return result;
|
||||
} catch (RuntimeException | Error e) {
|
||||
// Error is unchecked!
|
||||
} catch (Throwable e) {
|
||||
// Catch a Throwable here so even Errors would lead to a rollback.
|
||||
try {
|
||||
txn.rollback();
|
||||
logger.atWarning().log("Error during transaction; transaction rolled back.");
|
||||
} catch (Throwable rollbackException) {
|
||||
} catch (Exception rollbackException) {
|
||||
logger.atSevere().withCause(rollbackException).log("Rollback failed; suppressing error.");
|
||||
}
|
||||
tryWrapAndThrow(e);
|
||||
throw e;
|
||||
throwIfSqlException(e);
|
||||
throwIfUnchecked(e);
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
txnInfo.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T transactNoRetry(Supplier<T> work) {
|
||||
return transactNoRetry(work, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transact(Runnable work, TransactionIsolationLevel isolationLevel) {
|
||||
public void transact(ThrowingRunnable work, TransactionIsolationLevel isolationLevel) {
|
||||
transact(
|
||||
() -> {
|
||||
work.run();
|
||||
@@ -237,28 +239,17 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reTransact(Runnable work) {
|
||||
transact(work);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transact(Runnable work) {
|
||||
public void transact(ThrowingRunnable work) {
|
||||
transact(work, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transactNoRetry(Runnable work, TransactionIsolationLevel isolationLevel) {
|
||||
transactNoRetry(
|
||||
public void reTransact(ThrowingRunnable work) {
|
||||
reTransact(
|
||||
() -> {
|
||||
work.run();
|
||||
return null;
|
||||
},
|
||||
isolationLevel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transactNoRetry(Runnable work) {
|
||||
transactNoRetry(work, null);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -22,7 +22,7 @@ import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
|
||||
import google.registry.persistence.VKey;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.stream.Stream;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
@@ -48,51 +48,51 @@ public interface TransactionManager {
|
||||
void assertInTransaction();
|
||||
|
||||
/** Executes the work in a transaction and returns the result. */
|
||||
<T> T transact(Supplier<T> work);
|
||||
<T> T transact(Callable<T> work);
|
||||
|
||||
/**
|
||||
* Executes the work in a transaction at the given {@link TransactionIsolationLevel} and returns
|
||||
* the result.
|
||||
*/
|
||||
<T> T transact(Supplier<T> work, TransactionIsolationLevel isolationLevel);
|
||||
<T> T transact(Callable<T> work, TransactionIsolationLevel isolationLevel);
|
||||
|
||||
/**
|
||||
* Executes the work in a (potentially wrapped) transaction and returns the result.
|
||||
*
|
||||
* <p>Calls to this method are typically going to be in inner functions, that are called either as
|
||||
* top-level transactions themselves or are nested inside of larger transactions (e.g. a
|
||||
* top-level transactions themselves or are nested inside larger transactions (e.g. a
|
||||
* transactional flow). Invocations of reTransact must be vetted to occur in both situations and
|
||||
* with such complexity that it is not trivial to refactor out the nested transaction calls. New
|
||||
* code should be written in such a way as to avoid requiring reTransact in the first place.
|
||||
*
|
||||
* <p>In the future we will be enforcing that {@link #transact(Supplier)} calls be top-level only,
|
||||
* <p>In the future we will be enforcing that {@link #transact(Callable)} calls be top-level only,
|
||||
* with reTransact calls being the only ones that can potentially be an inner nested transaction
|
||||
* (which is a noop). Note that, as this can be a nested inner exception, there is no overload
|
||||
* provided to specify a (potentially conflicting) transaction isolation level.
|
||||
*/
|
||||
<T> T reTransact(Supplier<T> work);
|
||||
<T> T reTransact(Callable<T> work);
|
||||
|
||||
/** Executes the work in a transaction. */
|
||||
void transact(Runnable work);
|
||||
void transact(ThrowingRunnable work);
|
||||
|
||||
/** Executes the work in a transaction at the given {@link TransactionIsolationLevel}. */
|
||||
void transact(Runnable work, TransactionIsolationLevel isolationLevel);
|
||||
void transact(ThrowingRunnable work, TransactionIsolationLevel isolationLevel);
|
||||
|
||||
/**
|
||||
* Executes the work in a (potentially wrapped) transaction and returns the result.
|
||||
*
|
||||
* <p>Calls to this method are typically going to be in inner functions, that are called either as
|
||||
* top-level transactions themselves or are nested inside of larger transactions (e.g. a
|
||||
* top-level transactions themselves or are nested inside larger transactions (e.g. a
|
||||
* transactional flow). Invocations of reTransact must be vetted to occur in both situations and
|
||||
* with such complexity that it is not trivial to refactor out the nested transaction calls. New
|
||||
* code should be written in such a way as to avoid requiring reTransact in the first place.
|
||||
*
|
||||
* <p>In the future we will be enforcing that {@link #transact(Runnable)} calls be top-level only,
|
||||
* with reTransact calls being the only ones that can potentially be an inner nested transaction
|
||||
* (which is a noop). Note that, as this can be a nested inner exception, there is no overload *
|
||||
* provided to specify a (potentially conflicting) transaction isolation level.
|
||||
* <p>In the future we will be enforcing that {@link #transact(ThrowingRunnable)} calls be
|
||||
* top-level only, with reTransact calls being the only ones that can potentially be an inner
|
||||
* nested transaction (which is a noop). Note that, as this can be a nested inner exception, there
|
||||
* is no overload provided to specify a (potentially conflicting) transaction isolation level.
|
||||
*/
|
||||
void reTransact(Runnable work);
|
||||
void reTransact(ThrowingRunnable work);
|
||||
|
||||
/** Returns the time associated with the start of this particular transaction attempt. */
|
||||
DateTime getTransactionTime();
|
||||
@@ -216,4 +216,15 @@ public interface TransactionManager {
|
||||
|
||||
/** Returns a QueryComposer which can be used to perform queries against the current database. */
|
||||
<T> QueryComposer<T> createQueryComposer(Class<T> entity);
|
||||
|
||||
/**
|
||||
* A runnable that allows for checked exceptions to be thrown.
|
||||
*
|
||||
* <p>This makes it easier to write lambdas without having to worry about wrapping and re-throwing
|
||||
* checked excpetions as unchecked ones.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
interface ThrowingRunnable {
|
||||
void run() throws Exception;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,16 +30,14 @@ public enum Auth {
|
||||
* <p>If a user is logged in, will authenticate (and return) them. Otherwise, access is still
|
||||
* granted, but NOT_AUTHENTICATED is returned.
|
||||
*
|
||||
* <p>This is used for public HTML endpoints like RDAP, the check API, and web WHOIS.
|
||||
*
|
||||
* <p>User-facing legacy console endpoints (those that extend {@link HtmlAction}) also use it.
|
||||
* They need to allow requests from signed-out users so that they can redirect users to the login
|
||||
* page. After a user is logged in, they check if the user actually has access to the specific
|
||||
* console using {@link AuthenticatedRegistrarAccessor}.
|
||||
* <p>User-facing legacy console endpoints (those that extend {@link HtmlAction}) use it. They
|
||||
* need to allow requests from signed-out users so that they can redirect users to the login page.
|
||||
* After a user is logged in, they check if the user actually has access to the specific console
|
||||
* using {@link AuthenticatedRegistrarAccessor}.
|
||||
*
|
||||
* @see HtmlAction
|
||||
*/
|
||||
AUTH_PUBLIC(
|
||||
AUTH_PUBLIC_LEGACY(
|
||||
ImmutableList.of(AuthMethod.API, AuthMethod.LEGACY), AuthLevel.NONE, UserPolicy.PUBLIC),
|
||||
|
||||
/**
|
||||
@@ -52,6 +50,13 @@ public enum Auth {
|
||||
AUTH_PUBLIC_LOGGED_IN(
|
||||
ImmutableList.of(AuthMethod.API, AuthMethod.LEGACY), AuthLevel.USER, UserPolicy.PUBLIC),
|
||||
|
||||
/**
|
||||
* Allows anyone to access.
|
||||
*
|
||||
* <p>This is used for public HTML endpoints like RDAP, the check API, and web WHOIS.
|
||||
*/
|
||||
AUTH_PUBLIC(ImmutableList.of(AuthMethod.API), AuthLevel.NONE, UserPolicy.PUBLIC),
|
||||
|
||||
/**
|
||||
* Allows only the app itself (via service accounts) or admins to access.
|
||||
*
|
||||
|
||||
@@ -64,15 +64,15 @@ final class GetKeyringSecretCommand implements Command {
|
||||
case BRDA_SIGNING_PUBLIC_KEY:
|
||||
out.write(KeySerializer.serializePublicKey(keyring.getBrdaSigningKey().getPublicKey()));
|
||||
break;
|
||||
case BSA_API_KEY:
|
||||
out.write(KeySerializer.serializeString(keyring.getBsaApiKey()));
|
||||
break;
|
||||
case ICANN_REPORTING_PASSWORD:
|
||||
out.write(KeySerializer.serializeString(keyring.getIcannReportingPassword()));
|
||||
break;
|
||||
case SAFE_BROWSING_API_KEY:
|
||||
out.write(KeySerializer.serializeString(keyring.getSafeBrowsingAPIKey()));
|
||||
break;
|
||||
case JSON_CREDENTIAL:
|
||||
out.write(KeySerializer.serializeString(keyring.getJsonCredential()));
|
||||
break;
|
||||
case MARKSDB_DNL_LOGIN_AND_PASSWORD:
|
||||
out.write(KeySerializer.serializeString(keyring.getMarksdbDnlLoginAndPassword()));
|
||||
break;
|
||||
|
||||
@@ -21,11 +21,14 @@ import com.google.gson.TypeAdapterFactory;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import google.registry.model.adapters.ClassProcessingTypeAdapterFactory;
|
||||
import google.registry.model.adapters.CurrencyJsonAdapter;
|
||||
import google.registry.model.adapters.SerializableJsonTypeAdapter;
|
||||
import google.registry.util.CidrAddressBlock;
|
||||
import google.registry.util.CidrAddressBlock.CidrAddressBlockAdapter;
|
||||
import google.registry.util.DateTimeTypeAdapter;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import org.joda.money.CurrencyUnit;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
@@ -69,9 +72,11 @@ public class GsonUtils {
|
||||
|
||||
public static Gson provideGson() {
|
||||
return new GsonBuilder()
|
||||
.registerTypeAdapter(DateTime.class, new DateTimeTypeAdapter())
|
||||
.registerTypeAdapter(CidrAddressBlock.class, new CidrAddressBlockAdapter())
|
||||
.registerTypeAdapter(CurrencyUnit.class, new CurrencyJsonAdapter())
|
||||
.registerTypeAdapter(DateTime.class, new DateTimeTypeAdapter())
|
||||
.registerTypeAdapter(Serializable.class, new SerializableJsonTypeAdapter())
|
||||
.registerTypeAdapterFactory(new ClassProcessingTypeAdapterFactory())
|
||||
.registerTypeAdapterFactory(new GsonPostProcessableTypeAdapterFactory())
|
||||
.excludeFieldsWithoutExposeAnnotation()
|
||||
.create();
|
||||
|
||||
@@ -64,12 +64,12 @@ final class UpdateKeyringSecretCommand implements Command {
|
||||
throw new IllegalArgumentException(
|
||||
"Can't update BRDA_SIGNING_PUBLIC_KEY directly."
|
||||
+ " Must update public and private keys together using BRDA_SIGNING_KEY_PAIR.");
|
||||
case BSA_API_KEY:
|
||||
secretManagerKeyringUpdater.setBsaApiKey(deserializeString(input));
|
||||
break;
|
||||
case ICANN_REPORTING_PASSWORD:
|
||||
secretManagerKeyringUpdater.setIcannReportingPassword(deserializeString(input));
|
||||
break;
|
||||
case JSON_CREDENTIAL:
|
||||
secretManagerKeyringUpdater.setJsonCredential(deserializeString(input));
|
||||
break;
|
||||
case MARKSDB_DNL_LOGIN_AND_PASSWORD:
|
||||
secretManagerKeyringUpdater.setMarksdbDnlLoginAndPassword(deserializeString(input));
|
||||
break;
|
||||
|
||||
@@ -24,8 +24,8 @@ public enum KeyringKeyName {
|
||||
BRDA_RECEIVER_PUBLIC_KEY,
|
||||
BRDA_SIGNING_KEY_PAIR,
|
||||
BRDA_SIGNING_PUBLIC_KEY,
|
||||
BSA_API_KEY,
|
||||
ICANN_REPORTING_PASSWORD,
|
||||
JSON_CREDENTIAL,
|
||||
MARKSDB_DNL_LOGIN_AND_PASSWORD,
|
||||
MARKSDB_LORDN_PASSWORD,
|
||||
MARKSDB_SMDRL_LOGIN_AND_PASSWORD,
|
||||
|
||||
@@ -53,7 +53,7 @@ import javax.inject.Named;
|
||||
service = Action.Service.DEFAULT,
|
||||
path = ConsoleOteSetupAction.PATH,
|
||||
method = {Method.POST, Method.GET},
|
||||
auth = Auth.AUTH_PUBLIC)
|
||||
auth = Auth.AUTH_PUBLIC_LEGACY)
|
||||
public final class ConsoleOteSetupAction extends HtmlAction {
|
||||
|
||||
public static final String PATH = "/registrar-ote-setup";
|
||||
|
||||
@@ -63,7 +63,7 @@ import org.joda.money.CurrencyUnit;
|
||||
service = Service.DEFAULT,
|
||||
path = ConsoleRegistrarCreatorAction.PATH,
|
||||
method = {Method.POST, Method.GET},
|
||||
auth = Auth.AUTH_PUBLIC)
|
||||
auth = Auth.AUTH_PUBLIC_LEGACY)
|
||||
public final class ConsoleRegistrarCreatorAction extends HtmlAction {
|
||||
|
||||
private static final int PASSWORD_LENGTH = 16;
|
||||
|
||||
@@ -41,7 +41,10 @@ import java.util.Optional;
|
||||
import javax.inject.Inject;
|
||||
|
||||
/** Action that serves Registrar Console single HTML page (SPA). */
|
||||
@Action(service = Action.Service.DEFAULT, path = ConsoleUiAction.PATH, auth = Auth.AUTH_PUBLIC)
|
||||
@Action(
|
||||
service = Action.Service.DEFAULT,
|
||||
path = ConsoleUiAction.PATH,
|
||||
auth = Auth.AUTH_PUBLIC_LEGACY)
|
||||
public final class ConsoleUiAction extends HtmlAction {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
@@ -47,6 +47,7 @@ import google.registry.request.auth.UserAuthInfo;
|
||||
import google.registry.security.JsonResponseHelper;
|
||||
import google.registry.tools.DomainLockUtils;
|
||||
import google.registry.util.EmailMessage;
|
||||
import google.registry.util.PasswordUtils.HashAlgorithm;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -126,6 +127,7 @@ public class RegistryLockPostAction implements Runnable, JsonActionRunner.JsonAc
|
||||
.userAuthInfo()
|
||||
.orElseThrow(() -> new ForbiddenException("User is not logged in"));
|
||||
|
||||
// TODO: Move this line to the transaction below during nested transaction refactoring.
|
||||
String userEmail = verifyPasswordAndGetEmail(userAuthInfo, postInput);
|
||||
tm().transact(
|
||||
() -> {
|
||||
@@ -208,6 +210,7 @@ public class RegistryLockPostAction implements Runnable, JsonActionRunner.JsonAc
|
||||
throws RegistrarAccessDeniedException {
|
||||
// Verify that the user can access the registrar, that the user has
|
||||
// registry lock enabled, and that the user provided a correct password
|
||||
|
||||
Registrar registrar =
|
||||
getRegistrarAndVerifyLockAccess(registrarAccessor, postInput.registrarId, false);
|
||||
RegistrarPoc registrarPoc =
|
||||
@@ -220,6 +223,19 @@ public class RegistryLockPostAction implements Runnable, JsonActionRunner.JsonAc
|
||||
checkArgument(
|
||||
registrarPoc.verifyRegistryLockPassword(postInput.password),
|
||||
"Incorrect registry lock password for contact");
|
||||
if (registrarPoc.getCurrentHashAlgorithm(postInput.password).orElse(null)
|
||||
!= HashAlgorithm.SCRYPT) {
|
||||
logger.atInfo().log("Rehashing existing registry lock password with Scrypt.");
|
||||
tm().transact(
|
||||
() -> {
|
||||
tm().update(
|
||||
tm().loadByEntity(registrarPoc)
|
||||
.asBuilder()
|
||||
.setAllowedToSetRegistryLockPassword(true)
|
||||
.setRegistryLockPassword(postInput.password)
|
||||
.build());
|
||||
});
|
||||
}
|
||||
return registrarPoc
|
||||
.getRegistryLockEmailAddress()
|
||||
.orElseThrow(
|
||||
|
||||
@@ -34,7 +34,7 @@ import javax.inject.Inject;
|
||||
@Action(
|
||||
service = Action.Service.DEFAULT,
|
||||
path = RegistryLockVerifyAction.PATH,
|
||||
auth = Auth.AUTH_PUBLIC)
|
||||
auth = Auth.AUTH_PUBLIC_LEGACY)
|
||||
public final class RegistryLockVerifyAction extends HtmlAction {
|
||||
|
||||
public static final String PATH = "/registry-lock-verify";
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||
<command>
|
||||
<delete>
|
||||
<domain:delete
|
||||
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
|
||||
<domain:name>%DOMAIN%</domain:name>
|
||||
</domain:delete>
|
||||
</delete>
|
||||
<extension>
|
||||
<metadata:metadata xmlns:metadata="urn:google:params:xml:ns:metadata-1.0">
|
||||
<metadata:reason>Non-renewing domain has reached expiration date.</metadata:reason>
|
||||
<metadata:requestedByRegistrar>false</metadata:requestedByRegistrar>
|
||||
</metadata:metadata>
|
||||
</extension>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
</command>
|
||||
</epp>
|
||||
@@ -1,117 +0,0 @@
|
||||
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.batch;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.nullable;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.FakeResponse;
|
||||
import google.registry.testing.FakeSleeper;
|
||||
import google.registry.util.Retrier;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DatabaseMetaData;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
/** Unit tests for {@link WipeOutCloudSqlAction}. */
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class WipeOutCloudSqlActionTest {
|
||||
|
||||
@Mock private Statement stmt;
|
||||
@Mock private Connection conn;
|
||||
@Mock private DatabaseMetaData metaData;
|
||||
@Mock private ResultSet resultSet;
|
||||
|
||||
private FakeResponse response = new FakeResponse();
|
||||
private Retrier retrier = new Retrier(new FakeSleeper(new FakeClock()), 2);
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() throws Exception {
|
||||
lenient().when(conn.createStatement()).thenReturn(stmt);
|
||||
lenient().when(conn.getMetaData()).thenReturn(metaData);
|
||||
lenient()
|
||||
.when(
|
||||
metaData.getTables(
|
||||
nullable(String.class),
|
||||
nullable(String.class),
|
||||
nullable(String.class),
|
||||
nullable(String[].class)))
|
||||
.thenReturn(resultSet);
|
||||
lenient().when(stmt.executeQuery(anyString())).thenReturn(resultSet);
|
||||
lenient().when(resultSet.next()).thenReturn(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void run_projectAllowed() throws Exception {
|
||||
WipeOutCloudSqlAction action = new WipeOutCloudSqlAction(() -> conn, response, retrier);
|
||||
action.run();
|
||||
assertThat(response.getStatus()).isEqualTo(SC_OK);
|
||||
verify(stmt, times(1)).executeQuery(anyString());
|
||||
verify(stmt, times(1)).close();
|
||||
verifyNoMoreInteractions(stmt);
|
||||
}
|
||||
|
||||
@Test
|
||||
void run_projectNotAllowed() {
|
||||
try {
|
||||
RegistryEnvironment.SANDBOX.setup();
|
||||
WipeOutCloudSqlAction action = new WipeOutCloudSqlAction(() -> conn, response, retrier);
|
||||
action.run();
|
||||
assertThat(response.getStatus()).isEqualTo(SC_FORBIDDEN);
|
||||
verifyNoInteractions(stmt);
|
||||
} finally {
|
||||
RegistryEnvironment.UNITTEST.setup();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void run_nonRetrieableFailure() throws Exception {
|
||||
doThrow(new SQLException()).when(conn).getMetaData();
|
||||
WipeOutCloudSqlAction action = new WipeOutCloudSqlAction(() -> conn, response, retrier);
|
||||
action.run();
|
||||
assertThat(response.getStatus()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
|
||||
verifyNoInteractions(stmt);
|
||||
}
|
||||
|
||||
@Test
|
||||
void run_retrieableFailure() throws Exception {
|
||||
when(conn.getMetaData()).thenThrow(new RuntimeException()).thenReturn(metaData);
|
||||
WipeOutCloudSqlAction action = new WipeOutCloudSqlAction(() -> conn, response, retrier);
|
||||
action.run();
|
||||
assertThat(response.getStatus()).isEqualTo(SC_OK);
|
||||
verify(stmt, times(1)).executeQuery(anyString());
|
||||
verify(stmt, times(1)).close();
|
||||
verifyNoMoreInteractions(stmt);
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.batch;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import google.registry.persistence.NomulusPostgreSql;
|
||||
import java.sql.Connection;
|
||||
import java.sql.Statement;
|
||||
import java.util.Properties;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.containers.PostgreSQLContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
|
||||
/** Tests the database wipeout mechanism used by {@link WipeOutCloudSqlAction}. */
|
||||
@Testcontainers
|
||||
public class WipeOutCloudSqlIntegrationTest {
|
||||
|
||||
@Container
|
||||
PostgreSQLContainer container = new PostgreSQLContainer(NomulusPostgreSql.getDockerTag());
|
||||
|
||||
private Connection getJdbcConnection() throws Exception {
|
||||
Properties properties = new Properties();
|
||||
properties.setProperty("user", container.getUsername());
|
||||
properties.setProperty("password", container.getPassword());
|
||||
return container.getJdbcDriverInstance().connect(container.getJdbcUrl(), properties);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() throws Exception {
|
||||
try (Connection conn = getJdbcConnection();
|
||||
Statement statement = conn.createStatement()) {
|
||||
statement.addBatch("CREATE TABLE public.\"Domain\" (value int);");
|
||||
statement.addBatch("CREATE SEQUENCE public.\"Domain_seq\"");
|
||||
statement.executeBatch();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void listTables() throws Exception {
|
||||
try (Connection conn = getJdbcConnection()) {
|
||||
ImmutableList<String> tables = WipeOutCloudSqlAction.listTables(conn);
|
||||
assertThat(tables).containsExactly("public.\"Domain\"");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void dropAllTables() throws Exception {
|
||||
try (Connection conn = getJdbcConnection()) {
|
||||
ImmutableList<String> tables = WipeOutCloudSqlAction.listTables(conn);
|
||||
assertThat(tables).isNotEmpty();
|
||||
WipeOutCloudSqlAction.dropAllTables(conn, tables);
|
||||
assertThat(WipeOutCloudSqlAction.listTables(conn)).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void listAllSequences() throws Exception {
|
||||
try (Connection conn = getJdbcConnection()) {
|
||||
ImmutableList<String> sequences = WipeOutCloudSqlAction.listSequences(conn);
|
||||
assertThat(sequences).containsExactly("\"Domain_seq\"");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void dropAllSequences() throws Exception {
|
||||
try (Connection conn = getJdbcConnection()) {
|
||||
ImmutableList<String> sequences = WipeOutCloudSqlAction.listSequences(conn);
|
||||
assertThat(sequences).isNotEmpty();
|
||||
WipeOutCloudSqlAction.dropAllSequences(conn, sequences);
|
||||
assertThat(WipeOutCloudSqlAction.listSequences(conn)).isEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,6 @@ import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import com.google.common.testing.TestLogHandler;
|
||||
import google.registry.config.RegistryConfig;
|
||||
import google.registry.flows.certs.CertificateChecker;
|
||||
import google.registry.model.eppcommon.Trid;
|
||||
import google.registry.model.eppoutput.EppOutput.ResponseOrGreeting;
|
||||
@@ -89,8 +88,8 @@ class FlowRunnerTest {
|
||||
|
||||
@Override
|
||||
public ResponseOrGreeting run() {
|
||||
tm().assertTransactionIsolationLevel(
|
||||
isolationLevel.orElse(tm().getDefaultTransactionIsolationLevel()));
|
||||
assertThat(tm().getCurrentTransactionIsolationLevel())
|
||||
.isEqualTo(isolationLevel.orElse(tm().getDefaultTransactionIsolationLevel()));
|
||||
return mock(EppResponse.class);
|
||||
}
|
||||
}
|
||||
@@ -136,10 +135,8 @@ class FlowRunnerTest {
|
||||
Optional.of(TransactionIsolationLevel.TRANSACTION_READ_UNCOMMITTED);
|
||||
flowRunner.flowClass = TestTransactionalFlow.class;
|
||||
flowRunner.flowProvider = () -> new TestTransactionalFlow(flowRunner.isolationLevelOverride);
|
||||
if (RegistryConfig.getHibernatePerTransactionIsolationEnabled()) {
|
||||
flowRunner.run(eppMetricBuilder);
|
||||
assertThat(eppMetricBuilder.build().getCommandName()).hasValue("TestTransactional");
|
||||
}
|
||||
flowRunner.run(eppMetricBuilder);
|
||||
assertThat(eppMetricBuilder.build().getCommandName()).hasValue("TestTransactional");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1072,6 +1072,30 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
|
||||
ImmutableMap.of("RENEWPRICE", "11.00")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFeeExtension_existingPremiumDomain_withNonPremiumRenewalBehavior_renewPriceOnly()
|
||||
throws Exception {
|
||||
createTld("example");
|
||||
persistBillingRecurrenceForDomain(persistActiveDomain("rich.example"), NONPREMIUM, null);
|
||||
setEppInput("domain_check_fee_premium_v06_renew_only.xml");
|
||||
runFlowAssertResponse(
|
||||
loadFile(
|
||||
"domain_check_fee_response_domain_exists_v06_renew_only.xml",
|
||||
ImmutableMap.of("RENEWPRICE", "11.00")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFeeExtension_existingPremiumDomain_withNonPremiumRenewalBehavior_transferPriceOnly()
|
||||
throws Exception {
|
||||
createTld("example");
|
||||
persistBillingRecurrenceForDomain(persistActiveDomain("rich.example"), NONPREMIUM, null);
|
||||
setEppInput("domain_check_fee_premium_v06_transfer_only.xml");
|
||||
runFlowAssertResponse(
|
||||
loadFile(
|
||||
"domain_check_fee_response_domain_exists_v06_transfer_only.xml",
|
||||
ImmutableMap.of("RENEWPRICE", "11.00")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFeeExtension_existingPremiumDomain_withSpecifiedRenewalBehavior() throws Exception {
|
||||
createTld("example");
|
||||
@@ -1205,6 +1229,18 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
|
||||
runFlowAssertResponse(loadFile("domain_check_fee_premium_response_v12.xml"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFeeExtension_premiumLabels_v12_specifiedPriceRenewal_renewPriceOnly() throws Exception {
|
||||
createTld("example");
|
||||
persistBillingRecurrenceForDomain(
|
||||
persistActiveDomain("rich.example"), SPECIFIED, Money.of(USD, new BigDecimal("27.74")));
|
||||
setEppInput("domain_check_fee_premium_v12_renew_only.xml");
|
||||
runFlowAssertResponse(
|
||||
loadFile(
|
||||
"domain_check_fee_premium_response_v12_renew_only.xml",
|
||||
ImmutableMap.of("RENEWPRICE", "27.74")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFeeExtension_premiumLabels_doesNotApplyDefaultToken_v12() throws Exception {
|
||||
createTld("example");
|
||||
|
||||
@@ -14,11 +14,15 @@
|
||||
|
||||
package google.registry.flows.session;
|
||||
|
||||
import static com.google.common.io.BaseEncoding.base64;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.testing.DatabaseHelper.deleteResource;
|
||||
import static google.registry.testing.DatabaseHelper.loadRegistrar;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
|
||||
import static google.registry.util.PasswordUtils.HashAlgorithm.SCRYPT;
|
||||
import static google.registry.util.PasswordUtils.HashAlgorithm.SHA256;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
@@ -38,6 +42,7 @@ import google.registry.model.eppoutput.EppOutput;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.model.registrar.Registrar.State;
|
||||
import google.registry.testing.DatabaseHelper;
|
||||
import google.registry.util.PasswordUtils;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -186,6 +191,33 @@ public abstract class LoginFlowTestCase extends FlowTestCase<LoginFlow> {
|
||||
doFailingTest("login_valid.xml", RegistrarAccountNotActiveException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_sha256Password() throws Exception {
|
||||
String password = "foo-BAR2";
|
||||
tm().transact(
|
||||
() -> {
|
||||
// The salt is not exposed by Registrar (nor should it be), so we query it
|
||||
// directly.
|
||||
String encodedSalt =
|
||||
tm().query("SELECT salt FROM Registrar WHERE registrarId = :id", String.class)
|
||||
.setParameter("id", registrar.getRegistrarId())
|
||||
.getSingleResult();
|
||||
byte[] salt = base64().decode(encodedSalt);
|
||||
String newHash = PasswordUtils.hashPassword(password, salt, SHA256);
|
||||
// Set password directly, as the Java method would have used Scrypt.
|
||||
tm().query("UPDATE Registrar SET passwordHash = :hash WHERE registrarId = :id")
|
||||
.setParameter("id", registrar.getRegistrarId())
|
||||
.setParameter("hash", newHash)
|
||||
.executeUpdate();
|
||||
});
|
||||
assertThat(loadRegistrar("NewRegistrar").getCurrentHashAlgorithm(password).get())
|
||||
.isEqualTo(SHA256);
|
||||
doSuccessfulTest("login_valid.xml");
|
||||
// Verifies that after successfully login, the password is re-hased with Scrypt.
|
||||
assertThat(loadRegistrar("NewRegistrar").getCurrentHashAlgorithm(password).get())
|
||||
.isEqualTo(SCRYPT);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_incorrectPassword() {
|
||||
persistResource(getRegistrarBuilder().setPassword("diff password").build());
|
||||
|
||||
@@ -51,16 +51,16 @@ public class SecretManagerKeyringUpdaterTest {
|
||||
updater
|
||||
.setMarksdbDnlLoginAndPassword(secretPrefix + "marksdb")
|
||||
.setIcannReportingPassword(secretPrefix + "icann")
|
||||
.setJsonCredential(secretPrefix + "json")
|
||||
.setBsaApiKey(secretPrefix + "bsa")
|
||||
.update();
|
||||
|
||||
assertThat(keyring.getMarksdbDnlLoginAndPassword()).isEqualTo(secretPrefix + "marksdb");
|
||||
assertThat(keyring.getIcannReportingPassword()).isEqualTo(secretPrefix + "icann");
|
||||
assertThat(keyring.getJsonCredential()).isEqualTo(secretPrefix + "json");
|
||||
assertThat(keyring.getBsaApiKey()).isEqualTo(secretPrefix + "bsa");
|
||||
|
||||
verifyPersistedSecret("marksdb-dnl-login-string", secretPrefix + "marksdb");
|
||||
verifyPersistedSecret("icann-reporting-password-string", secretPrefix + "icann");
|
||||
verifyPersistedSecret("json-credential-string", secretPrefix + "json");
|
||||
verifyPersistedSecret("bsa-api-key-string", secretPrefix + "bsa");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -94,12 +94,12 @@ public class SecretManagerKeyringUpdaterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void jsonCredential() {
|
||||
String secret = "jsonCredential";
|
||||
updater.setJsonCredential(secret).update();
|
||||
void bsaApiKey() {
|
||||
String secret = "bsaApiKey";
|
||||
updater.setBsaApiKey(secret).update();
|
||||
|
||||
assertThat(keyring.getJsonCredential()).isEqualTo(secret);
|
||||
verifyPersistedSecret("json-credential-string", secret);
|
||||
assertThat(keyring.getBsaApiKey()).isEqualTo(secret);
|
||||
verifyPersistedSecret("bsa-api-key-string", secret);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright 2023 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.adapters;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.tools.GsonUtils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Tests for {@link ClassTypeAdapter} and {@link SerializableJsonTypeAdapter}. */
|
||||
public class VKeyAdapterTest {
|
||||
|
||||
private static final Gson GSON = GsonUtils.provideGson();
|
||||
|
||||
@Test
|
||||
void testVKeyConversion_string() {
|
||||
VKey<Domain> vkey = VKey.create(Domain.class, "someRepoId");
|
||||
String vkeyJson = GSON.toJson(vkey);
|
||||
assertThat(vkeyJson)
|
||||
.isEqualTo(
|
||||
"{\"key\":\"someRepoId\",\"kind\":" + "\"google.registry.model.domain.Domain\"}");
|
||||
assertThat(GSON.fromJson(vkeyJson, VKey.class)).isEqualTo(vkey);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVKeyConversion_number() {
|
||||
VKey<BillingEvent> vkey = VKey.create(BillingEvent.class, 203L);
|
||||
String vkeyJson = GSON.toJson(vkey);
|
||||
assertThat(vkeyJson)
|
||||
.isEqualTo("{\"key\":203,\"kind\":" + "\"google.registry.model.billing.BillingEvent\"}");
|
||||
assertThat(GSON.fromJson(vkeyJson, VKey.class)).isEqualTo(vkey);
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth8.assertThat;
|
||||
import static google.registry.persistence.transaction.DatabaseException.getSqlError;
|
||||
import static google.registry.persistence.transaction.DatabaseException.getSqlExceptionDetails;
|
||||
import static google.registry.persistence.transaction.DatabaseException.tryWrapAndThrow;
|
||||
import static google.registry.persistence.transaction.DatabaseException.throwIfSqlException;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
@@ -99,13 +99,13 @@ public class DatabaseExceptionTest {
|
||||
@Test
|
||||
void tryWrapAndThrow_notSQLException() {
|
||||
RuntimeException orig = new RuntimeException(new Exception());
|
||||
tryWrapAndThrow(orig);
|
||||
throwIfSqlException(orig);
|
||||
}
|
||||
|
||||
@Test
|
||||
void tryWrapAndThrow_hasSQLException() {
|
||||
Throwable orig = new Throwable(new SQLException());
|
||||
assertThrows(DatabaseException.class, () -> tryWrapAndThrow(orig));
|
||||
assertThrows(DatabaseException.class, () -> throwIfSqlException(orig));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
package google.registry.persistence.transaction;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.persistence.PersistenceModule.TransactionIsolationLevel.TRANSACTION_READ_COMMITTED;
|
||||
@@ -28,6 +29,7 @@ import static google.registry.testing.TestDataHelper.fileClassPath;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -36,6 +38,7 @@ import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import google.registry.config.RegistryConfig;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions.JpaUnitTestExtension;
|
||||
import google.registry.testing.DatabaseHelper;
|
||||
@@ -43,7 +46,6 @@ import google.registry.testing.FakeClock;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.function.Supplier;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.Id;
|
||||
@@ -53,6 +55,8 @@ import javax.persistence.PersistenceException;
|
||||
import javax.persistence.RollbackException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.junit.jupiter.api.function.Executable;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
/**
|
||||
* Unit tests for SQL only APIs defined in {@link JpaTransactionManagerImpl}. Note that the tests
|
||||
@@ -94,7 +98,7 @@ class JpaTransactionManagerImplTest {
|
||||
insertPerson(10);
|
||||
insertCompany("Foo");
|
||||
insertCompany("Bar");
|
||||
tm().assertTransactionIsolationLevel(tm().getDefaultTransactionIsolationLevel());
|
||||
assertTransactionIsolationLevel(tm().getDefaultTransactionIsolationLevel());
|
||||
});
|
||||
assertPersonCount(1);
|
||||
assertPersonExist(10);
|
||||
@@ -105,145 +109,98 @@ class JpaTransactionManagerImplTest {
|
||||
|
||||
@Test
|
||||
void transact_setIsolationLevel() {
|
||||
// If not specified, run at the default isolation level.
|
||||
tm().transact(
|
||||
() -> {
|
||||
tm().assertTransactionIsolationLevel(
|
||||
RegistryConfig.getHibernatePerTransactionIsolationEnabled()
|
||||
? TRANSACTION_READ_UNCOMMITTED
|
||||
: tm().getDefaultTransactionIsolationLevel());
|
||||
return null;
|
||||
},
|
||||
() -> assertTransactionIsolationLevel(tm().getDefaultTransactionIsolationLevel()),
|
||||
null);
|
||||
tm().transact(
|
||||
() -> assertTransactionIsolationLevel(TRANSACTION_READ_UNCOMMITTED),
|
||||
TRANSACTION_READ_UNCOMMITTED);
|
||||
// Make sure that we can start a new transaction on the same thread with a different isolation
|
||||
// level.
|
||||
// Make sure that we can start a new transaction on the same thread at a different level.
|
||||
tm().transact(
|
||||
() -> {
|
||||
tm().assertTransactionIsolationLevel(
|
||||
RegistryConfig.getHibernatePerTransactionIsolationEnabled()
|
||||
? TRANSACTION_REPEATABLE_READ
|
||||
: tm().getDefaultTransactionIsolationLevel());
|
||||
return null;
|
||||
},
|
||||
() -> assertTransactionIsolationLevel(TRANSACTION_REPEATABLE_READ),
|
||||
TRANSACTION_REPEATABLE_READ);
|
||||
}
|
||||
|
||||
@Test
|
||||
void transact_nestedTransactions_perTransactionIsolationLevelEnabled() {
|
||||
if (!RegistryConfig.getHibernatePerTransactionIsolationEnabled()) {
|
||||
return;
|
||||
}
|
||||
// Nested transactions allowed (both at the default isolation level).
|
||||
tm().transact(
|
||||
() -> {
|
||||
tm().assertTransactionIsolationLevel(tm().getDefaultTransactionIsolationLevel());
|
||||
tm().transact(
|
||||
() -> {
|
||||
tm().assertTransactionIsolationLevel(
|
||||
void transact_nestedTransactions_disabled() {
|
||||
try (MockedStatic<RegistryConfig> config = mockStatic(RegistryConfig.class)) {
|
||||
config.when(RegistryConfig::getHibernateAllowNestedTransactions).thenReturn(false);
|
||||
// transact() not allowed in nested transactions.
|
||||
IllegalStateException thrown =
|
||||
assertThrows(
|
||||
IllegalStateException.class,
|
||||
() ->
|
||||
tm().transact(
|
||||
() -> {
|
||||
assertTransactionIsolationLevel(
|
||||
tm().getDefaultTransactionIsolationLevel());
|
||||
});
|
||||
});
|
||||
// Nested transactions allowed (enclosed transaction does not have an override, using the
|
||||
// enclosing transaction's level).
|
||||
tm().transact(
|
||||
() -> {
|
||||
tm().assertTransactionIsolationLevel(TRANSACTION_READ_UNCOMMITTED);
|
||||
tm().transact(
|
||||
() -> {
|
||||
tm().assertTransactionIsolationLevel(TRANSACTION_READ_UNCOMMITTED);
|
||||
});
|
||||
},
|
||||
TRANSACTION_READ_UNCOMMITTED);
|
||||
// Nested transactions allowed (Both have the same override).
|
||||
tm().transact(
|
||||
() -> {
|
||||
tm().assertTransactionIsolationLevel(TRANSACTION_REPEATABLE_READ);
|
||||
tm().transact(
|
||||
() -> {
|
||||
tm().assertTransactionIsolationLevel(TRANSACTION_REPEATABLE_READ);
|
||||
},
|
||||
TRANSACTION_REPEATABLE_READ);
|
||||
},
|
||||
TRANSACTION_REPEATABLE_READ);
|
||||
// Nested transactions disallowed (enclosed transaction has an override that conflicts from the
|
||||
// default).
|
||||
IllegalStateException e =
|
||||
assertThrows(
|
||||
IllegalStateException.class,
|
||||
() ->
|
||||
tm().transact(
|
||||
() -> {
|
||||
tm().transact(() -> {}, TRANSACTION_READ_COMMITTED);
|
||||
}));
|
||||
assertThat(e).hasMessageThat().contains("conflict detected");
|
||||
// Nested transactions disallowed (conflicting overrides).
|
||||
e =
|
||||
assertThrows(
|
||||
IllegalStateException.class,
|
||||
() ->
|
||||
tm().transact(
|
||||
() -> {
|
||||
tm().transact(() -> {}, TRANSACTION_READ_COMMITTED);
|
||||
},
|
||||
TRANSACTION_REPEATABLE_READ));
|
||||
assertThat(e).hasMessageThat().contains("conflict detected");
|
||||
tm().transact(() -> null);
|
||||
}));
|
||||
assertThat(thrown).hasMessageThat().contains("Nested transaction detected");
|
||||
// reTransact() allowed in nested transactions.
|
||||
tm().transact(
|
||||
() -> {
|
||||
assertTransactionIsolationLevel(tm().getDefaultTransactionIsolationLevel());
|
||||
tm().reTransact(
|
||||
() ->
|
||||
assertTransactionIsolationLevel(
|
||||
tm().getDefaultTransactionIsolationLevel()));
|
||||
});
|
||||
// reTransact() respects enclosing transaction's isolation level.
|
||||
tm().transact(
|
||||
() -> {
|
||||
assertTransactionIsolationLevel(TRANSACTION_READ_UNCOMMITTED);
|
||||
tm().reTransact(
|
||||
() -> assertTransactionIsolationLevel(TRANSACTION_READ_UNCOMMITTED));
|
||||
},
|
||||
TRANSACTION_READ_UNCOMMITTED);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void transact_nestedTransactions_perTransactionIsolationLevelDisabled() {
|
||||
if (RegistryConfig.getHibernatePerTransactionIsolationEnabled()) {
|
||||
return;
|
||||
void transact_nestedTransactions_enabled() {
|
||||
try (MockedStatic<RegistryConfig> config = mockStatic(RegistryConfig.class)) {
|
||||
config.when(RegistryConfig::getHibernateAllowNestedTransactions).thenReturn(true);
|
||||
// transact() allowed in nested transactions.
|
||||
tm().transact(
|
||||
() -> {
|
||||
assertTransactionIsolationLevel(tm().getDefaultTransactionIsolationLevel());
|
||||
tm().reTransact(
|
||||
() ->
|
||||
assertTransactionIsolationLevel(
|
||||
tm().getDefaultTransactionIsolationLevel()));
|
||||
});
|
||||
// transact() not allowed in nested transactions if isolation level is specified.
|
||||
IllegalStateException thrown =
|
||||
assertThrows(
|
||||
IllegalStateException.class,
|
||||
() ->
|
||||
tm().transact(
|
||||
() -> {
|
||||
assertTransactionIsolationLevel(
|
||||
tm().getDefaultTransactionIsolationLevel());
|
||||
tm().transact(() -> null, TRANSACTION_READ_COMMITTED);
|
||||
}));
|
||||
assertThat(thrown).hasMessageThat().contains("cannot be specified");
|
||||
// reTransact() allowed in nested transactions.
|
||||
tm().transact(
|
||||
() -> {
|
||||
assertTransactionIsolationLevel(tm().getDefaultTransactionIsolationLevel());
|
||||
tm().reTransact(
|
||||
() ->
|
||||
assertTransactionIsolationLevel(
|
||||
tm().getDefaultTransactionIsolationLevel()));
|
||||
});
|
||||
// reTransact() respects enclosing transaction's isolation level.
|
||||
tm().transact(
|
||||
() -> {
|
||||
assertTransactionIsolationLevel(TRANSACTION_READ_UNCOMMITTED);
|
||||
tm().reTransact(
|
||||
() -> assertTransactionIsolationLevel(TRANSACTION_READ_UNCOMMITTED));
|
||||
},
|
||||
TRANSACTION_READ_UNCOMMITTED);
|
||||
}
|
||||
tm().transact(
|
||||
() -> {
|
||||
tm().assertTransactionIsolationLevel(tm().getDefaultTransactionIsolationLevel());
|
||||
tm().transact(
|
||||
() -> {
|
||||
tm().assertTransactionIsolationLevel(
|
||||
tm().getDefaultTransactionIsolationLevel());
|
||||
});
|
||||
});
|
||||
tm().transact(
|
||||
() -> {
|
||||
tm().assertTransactionIsolationLevel(tm().getDefaultTransactionIsolationLevel());
|
||||
tm().transact(
|
||||
() -> {
|
||||
tm().assertTransactionIsolationLevel(
|
||||
tm().getDefaultTransactionIsolationLevel());
|
||||
});
|
||||
},
|
||||
TRANSACTION_READ_UNCOMMITTED);
|
||||
tm().transact(
|
||||
() -> {
|
||||
tm().assertTransactionIsolationLevel(tm().getDefaultTransactionIsolationLevel());
|
||||
tm().transact(
|
||||
() -> {
|
||||
tm().assertTransactionIsolationLevel(
|
||||
tm().getDefaultTransactionIsolationLevel());
|
||||
},
|
||||
TRANSACTION_READ_UNCOMMITTED);
|
||||
});
|
||||
tm().transact(
|
||||
() -> {
|
||||
tm().assertTransactionIsolationLevel(tm().getDefaultTransactionIsolationLevel());
|
||||
tm().transact(
|
||||
() -> {
|
||||
tm().assertTransactionIsolationLevel(
|
||||
tm().getDefaultTransactionIsolationLevel());
|
||||
},
|
||||
TRANSACTION_READ_UNCOMMITTED);
|
||||
},
|
||||
TRANSACTION_READ_UNCOMMITTED);
|
||||
tm().transact(
|
||||
() -> {
|
||||
tm().assertTransactionIsolationLevel(tm().getDefaultTransactionIsolationLevel());
|
||||
tm().transact(
|
||||
() -> {
|
||||
tm().assertTransactionIsolationLevel(
|
||||
tm().getDefaultTransactionIsolationLevel());
|
||||
},
|
||||
TRANSACTION_READ_COMMITTED);
|
||||
},
|
||||
TRANSACTION_READ_UNCOMMITTED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -299,32 +256,55 @@ class JpaTransactionManagerImplTest {
|
||||
OptimisticLockException.class,
|
||||
() -> spyJpaTm.transact(() -> spyJpaTm.delete(theEntityKey)));
|
||||
verify(spyJpaTm, times(3)).delete(theEntityKey);
|
||||
Supplier<Runnable> supplier =
|
||||
() -> {
|
||||
Runnable work = () -> spyJpaTm.delete(theEntityKey);
|
||||
work.run();
|
||||
return null;
|
||||
};
|
||||
assertThrows(OptimisticLockException.class, () -> spyJpaTm.transact(supplier));
|
||||
assertThrows(
|
||||
OptimisticLockException.class,
|
||||
() -> spyJpaTm.transact(() -> spyJpaTm.delete(theEntityKey)));
|
||||
verify(spyJpaTm, times(6)).delete(theEntityKey);
|
||||
}
|
||||
|
||||
@Test
|
||||
void transactNoRetry_doesNotRetryOptimisticLockException() {
|
||||
JpaTransactionManager spyJpaTm = spy(tm());
|
||||
doThrow(OptimisticLockException.class).when(spyJpaTm).delete(any(VKey.class));
|
||||
spyJpaTm.transactNoRetry(() -> spyJpaTm.insert(theEntity));
|
||||
assertThrows(
|
||||
OptimisticLockException.class,
|
||||
() -> spyJpaTm.transactNoRetry(() -> spyJpaTm.delete(theEntityKey)));
|
||||
verify(spyJpaTm, times(1)).delete(theEntityKey);
|
||||
Supplier<Runnable> supplier =
|
||||
void transactNoRetry_nested() {
|
||||
JpaTransactionManagerImpl tm = (JpaTransactionManagerImpl) tm();
|
||||
// Calling transactNoRetry() without an isolation level override inside a transaction is fine.
|
||||
tm.transact(
|
||||
() -> {
|
||||
Runnable work = () -> spyJpaTm.delete(theEntityKey);
|
||||
work.run();
|
||||
tm.transactNoRetry(
|
||||
() -> {
|
||||
assertTransactionIsolationLevel(tm.getDefaultTransactionIsolationLevel());
|
||||
return null;
|
||||
},
|
||||
null);
|
||||
});
|
||||
// Calling transactNoRetry() with an isolation level override inside a transaction is not
|
||||
// allowed.
|
||||
IllegalStateException thrown =
|
||||
assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> tm.transact(() -> tm.transactNoRetry(() -> null, TRANSACTION_READ_UNCOMMITTED)));
|
||||
assertThat(thrown).hasMessageThat().contains("cannot be specified");
|
||||
}
|
||||
|
||||
@Test
|
||||
void transactNoRetry_doesNotRetryOptimisticLockException() {
|
||||
JpaTransactionManagerImpl spyJpaTm = spy((JpaTransactionManagerImpl) tm());
|
||||
doThrow(OptimisticLockException.class).when(spyJpaTm).delete(any(VKey.class));
|
||||
spyJpaTm.transactNoRetry(
|
||||
() -> {
|
||||
spyJpaTm.insert(theEntity);
|
||||
return null;
|
||||
};
|
||||
assertThrows(OptimisticLockException.class, () -> spyJpaTm.transactNoRetry(supplier));
|
||||
},
|
||||
null);
|
||||
Executable transaction =
|
||||
() ->
|
||||
spyJpaTm.transactNoRetry(
|
||||
() -> {
|
||||
spyJpaTm.delete(theEntityKey);
|
||||
return null;
|
||||
},
|
||||
null);
|
||||
assertThrows(OptimisticLockException.class, transaction);
|
||||
verify(spyJpaTm, times(1)).delete(theEntityKey);
|
||||
assertThrows(OptimisticLockException.class, transaction);
|
||||
verify(spyJpaTm, times(2)).delete(theEntityKey);
|
||||
}
|
||||
|
||||
@@ -338,13 +318,8 @@ class JpaTransactionManagerImplTest {
|
||||
assertThrows(
|
||||
RuntimeException.class, () -> spyJpaTm.transact(() -> spyJpaTm.delete(theEntityKey)));
|
||||
verify(spyJpaTm, times(3)).delete(theEntityKey);
|
||||
Supplier<Runnable> supplier =
|
||||
() -> {
|
||||
Runnable work = () -> spyJpaTm.delete(theEntityKey);
|
||||
work.run();
|
||||
return null;
|
||||
};
|
||||
assertThrows(RuntimeException.class, () -> spyJpaTm.transact(supplier));
|
||||
assertThrows(
|
||||
RuntimeException.class, () -> spyJpaTm.transact(() -> spyJpaTm.delete(theEntityKey)));
|
||||
verify(spyJpaTm, times(6)).delete(theEntityKey);
|
||||
}
|
||||
|
||||
@@ -740,20 +715,13 @@ class JpaTransactionManagerImplTest {
|
||||
doThrow(OptimisticLockException.class).when(spyJpaTm).delete(any(VKey.class));
|
||||
spyJpaTm.transact(() -> spyJpaTm.insert(theEntity));
|
||||
|
||||
Supplier<Runnable> supplier =
|
||||
() -> {
|
||||
Runnable work = () -> spyJpaTm.delete(theEntityKey);
|
||||
work.run();
|
||||
return null;
|
||||
};
|
||||
|
||||
assertThrows(
|
||||
OptimisticLockException.class,
|
||||
() ->
|
||||
spyJpaTm.transact(
|
||||
() -> {
|
||||
spyJpaTm.exists(theEntity);
|
||||
spyJpaTm.transact(supplier);
|
||||
spyJpaTm.transact(() -> spyJpaTm.delete(theEntityKey));
|
||||
}));
|
||||
|
||||
verify(spyJpaTm, times(3)).exists(theEntity);
|
||||
@@ -814,6 +782,16 @@ class JpaTransactionManagerImplTest {
|
||||
assertCompanyCount(0);
|
||||
}
|
||||
|
||||
private static void assertTransactionIsolationLevel(TransactionIsolationLevel expectedLevel) {
|
||||
tm().assertInTransaction();
|
||||
TransactionIsolationLevel currentLevel = tm().getCurrentTransactionIsolationLevel();
|
||||
checkState(
|
||||
currentLevel == expectedLevel,
|
||||
"Current transaction isolation level (%s) is not as expected (%s)",
|
||||
currentLevel,
|
||||
expectedLevel);
|
||||
}
|
||||
|
||||
private static int countTable(String tableName) {
|
||||
return tm().transact(
|
||||
() -> {
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
|
||||
package google.registry.persistence.transaction;
|
||||
|
||||
import static com.google.common.base.Throwables.throwIfUnchecked;
|
||||
import static google.registry.persistence.transaction.DatabaseException.throwIfSqlException;
|
||||
|
||||
import com.google.common.collect.ImmutableCollection;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
@@ -21,7 +24,7 @@ import google.registry.model.ImmutableObject;
|
||||
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
|
||||
import google.registry.persistence.VKey;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.stream.Stream;
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.Query;
|
||||
@@ -60,11 +63,6 @@ public class ReplicaSimulatingJpaTransactionManager implements JpaTransactionMan
|
||||
return delegate.getCurrentTransactionIsolationLevel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assertTransactionIsolationLevel(TransactionIsolationLevel expectedLevel) {
|
||||
delegate.assertTransactionIsolationLevel(expectedLevel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityManager getStandaloneEntityManager() {
|
||||
return delegate.getStandaloneEntityManager();
|
||||
@@ -101,9 +99,15 @@ public class ReplicaSimulatingJpaTransactionManager implements JpaTransactionMan
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T transact(Supplier<T> work, TransactionIsolationLevel isolationLevel) {
|
||||
if (delegate.inTransaction()) {
|
||||
return work.get();
|
||||
public <T> T transact(Callable<T> work, TransactionIsolationLevel isolationLevel) {
|
||||
if (inTransaction()) {
|
||||
try {
|
||||
return work.call();
|
||||
} catch (Exception e) {
|
||||
throwIfSqlException(e);
|
||||
throwIfUnchecked(e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
return delegate.transact(
|
||||
() -> {
|
||||
@@ -111,33 +115,23 @@ public class ReplicaSimulatingJpaTransactionManager implements JpaTransactionMan
|
||||
.getEntityManager()
|
||||
.createNativeQuery("SET TRANSACTION READ ONLY")
|
||||
.executeUpdate();
|
||||
return work.get();
|
||||
return work.call();
|
||||
},
|
||||
isolationLevel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T reTransact(Supplier<T> work) {
|
||||
public <T> T reTransact(Callable<T> work) {
|
||||
return transact(work);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T transact(Supplier<T> work) {
|
||||
public <T> T transact(Callable<T> work) {
|
||||
return transact(work, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T transactNoRetry(Supplier<T> work, TransactionIsolationLevel isolationLevel) {
|
||||
return transact(work, isolationLevel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T transactNoRetry(Supplier<T> work) {
|
||||
return transactNoRetry(work, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transact(Runnable work, TransactionIsolationLevel isolationLevel) {
|
||||
public void transact(ThrowingRunnable work, TransactionIsolationLevel isolationLevel) {
|
||||
transact(
|
||||
() -> {
|
||||
work.run();
|
||||
@@ -147,25 +141,15 @@ public class ReplicaSimulatingJpaTransactionManager implements JpaTransactionMan
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reTransact(Runnable work) {
|
||||
public void reTransact(ThrowingRunnable work) {
|
||||
transact(work);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transact(Runnable work) {
|
||||
public void transact(ThrowingRunnable work) {
|
||||
transact(work, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transactNoRetry(Runnable work, TransactionIsolationLevel isolationLevel) {
|
||||
transact(work, isolationLevel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transactNoRetry(Runnable work) {
|
||||
transactNoRetry(work, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DateTime getTransactionTime() {
|
||||
return delegate.getTransactionTime();
|
||||
|
||||
@@ -55,7 +55,7 @@ public final class FakeKeyringModule {
|
||||
private static final String MARKSDB_DNL_LOGIN_AND_PASSWORD = "dnl:yolo";
|
||||
private static final String MARKSDB_LORDN_PASSWORD = "yolo";
|
||||
private static final String MARKSDB_SMDRL_LOGIN_AND_PASSWORD = "smdrl:yolo";
|
||||
private static final String JSON_CREDENTIAL = "json123";
|
||||
private static final String BSA_API_KEY = "bsaapikey";
|
||||
|
||||
@Provides
|
||||
public Keyring get() {
|
||||
@@ -127,8 +127,8 @@ public final class FakeKeyringModule {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getJsonCredential() {
|
||||
return JSON_CREDENTIAL;
|
||||
public String getBsaApiKey() {
|
||||
return BSA_API_KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -66,10 +66,14 @@ public class ConsoleDomainGetActionTest {
|
||||
assertThat(RESPONSE.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_OK);
|
||||
assertThat(RESPONSE.getPayload())
|
||||
.isEqualTo(
|
||||
"{\"domainName\":\"exists.tld\",\"registrationExpirationTime\":"
|
||||
+ "\"294247-01-10T04:00:54.775Z\",\"lastTransferTime\":\"null\",\"repoId\":"
|
||||
+ "\"2-TLD\",\"currentSponsorRegistrarId\":\"TheRegistrar\",\"creationRegistrarId\""
|
||||
+ ":\"TheRegistrar\",\"creationTime\":{\"creationTime\":"
|
||||
"{\"domainName\":\"exists.tld\",\"adminContact\":{\"key\":\"3-ROID\",\"kind\":"
|
||||
+ "\"google.registry.model.contact.Contact\"},\"techContact\":{\"key\":\"3-ROID\","
|
||||
+ "\"kind\":\"google.registry.model.contact.Contact\"},\"registrantContact\":"
|
||||
+ "{\"key\":\"3-ROID\",\"kind\":\"google.registry.model.contact.Contact\"},"
|
||||
+ "\"registrationExpirationTime\":\"294247-01-10T04:00:54.775Z\","
|
||||
+ "\"lastTransferTime\":\"null\",\"repoId\":\"2-TLD\","
|
||||
+ "\"currentSponsorRegistrarId\":\"TheRegistrar\",\"creationRegistrarId\":"
|
||||
+ "\"TheRegistrar\",\"creationTime\":{\"creationTime\":"
|
||||
+ "\"1970-01-01T00:00:00.000Z\"},\"lastEppUpdateTime\":\"null\",\"statuses\":"
|
||||
+ "[\"INACTIVE\"]}");
|
||||
}
|
||||
|
||||
@@ -15,8 +15,10 @@
|
||||
package google.registry.ui.server.registrar;
|
||||
|
||||
import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap;
|
||||
import static com.google.common.io.BaseEncoding.base64;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.EppResourceUtils.loadByForeignKey;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.testing.DatabaseHelper.createTld;
|
||||
import static google.registry.testing.DatabaseHelper.loadRegistrar;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
@@ -25,6 +27,8 @@ import static google.registry.testing.SqlHelper.getRegistryLockByVerificationCod
|
||||
import static google.registry.testing.SqlHelper.saveRegistryLock;
|
||||
import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STATUSES;
|
||||
import static google.registry.ui.server.registrar.RegistryLockGetActionTest.userFromRegistrarPoc;
|
||||
import static google.registry.util.PasswordUtils.HashAlgorithm.SCRYPT;
|
||||
import static google.registry.util.PasswordUtils.HashAlgorithm.SHA256;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -38,6 +42,9 @@ import google.registry.model.console.RegistrarRole;
|
||||
import google.registry.model.console.UserRoles;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.RegistryLock;
|
||||
import google.registry.model.registrar.RegistrarPoc;
|
||||
import google.registry.model.registrar.RegistrarPoc.RegistrarPocId;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
|
||||
import google.registry.persistence.transaction.JpaTransactionManagerExtension;
|
||||
@@ -54,6 +61,7 @@ import google.registry.testing.DeterministicStringGenerator;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.tools.DomainLockUtils;
|
||||
import google.registry.util.EmailMessage;
|
||||
import google.registry.util.PasswordUtils;
|
||||
import google.registry.util.StringGenerator.Alphabets;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -123,6 +131,48 @@ final class RegistryLockPostActionTest {
|
||||
assertSuccess(response, "lock", "Marla.Singer.RegistryLock@crr.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_lock_sha256Password() throws Exception {
|
||||
tm().transact(
|
||||
() -> {
|
||||
// The salt is not exposed by RegistrarPoc (nor should it be), so we query
|
||||
// it directly.
|
||||
String encodedSalt =
|
||||
tm().query(
|
||||
"SELECT registryLockPasswordSalt FROM RegistrarPoc "
|
||||
+ "WHERE emailAddress = :email "
|
||||
+ "AND registrarId = :registrarId",
|
||||
String.class)
|
||||
.setParameter("email", "Marla.Singer@crr.com")
|
||||
.setParameter("registrarId", "TheRegistrar")
|
||||
.getSingleResult();
|
||||
byte[] salt = base64().decode(encodedSalt);
|
||||
String newHash = PasswordUtils.hashPassword("hi", salt, SHA256);
|
||||
// Set password directly, as the Java method would have used Scrypt.
|
||||
tm().query("UPDATE RegistrarPoc SET registryLockPasswordHash = :hash")
|
||||
.setParameter("hash", newHash)
|
||||
.executeUpdate();
|
||||
});
|
||||
RegistrarPoc registrarPoc =
|
||||
tm().transact(
|
||||
() ->
|
||||
tm().loadByKey(
|
||||
VKey.create(
|
||||
RegistrarPoc.class,
|
||||
new RegistrarPocId("Marla.Singer@crr.com", "TheRegistrar"))));
|
||||
assertThat(registrarPoc.getCurrentHashAlgorithm("hi").get()).isEqualTo(SHA256);
|
||||
Map<String, ?> response = action.handleJsonRequest(lockRequest());
|
||||
RegistrarPoc updatedRegistrarPoc =
|
||||
tm().transact(
|
||||
() ->
|
||||
tm().loadByKey(
|
||||
VKey.create(
|
||||
RegistrarPoc.class,
|
||||
new RegistrarPocId("Marla.Singer@crr.com", "TheRegistrar"))));
|
||||
assertThat(updatedRegistrarPoc.getCurrentHashAlgorithm("hi").get()).isEqualTo(SCRYPT);
|
||||
assertSuccess(response, "lock", "Marla.Singer.RegistryLock@crr.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_unlock() throws Exception {
|
||||
saveRegistryLock(createLock().asBuilder().setLockCompletionTime(clock.nowUtc()).build());
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
domain_check_fee_premium_response_v12.xml<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||
<response>
|
||||
<result code="1000">
|
||||
<msg>Command completed successfully</msg>
|
||||
</result>
|
||||
<resData>
|
||||
<domain:chkData xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
|
||||
<domain:cd>
|
||||
<domain:name avail="false">rich.example</domain:name>
|
||||
<domain:reason>In use</domain:reason>
|
||||
</domain:cd>
|
||||
</domain:chkData>
|
||||
</resData>
|
||||
<extension>
|
||||
<fee:chkData xmlns:fee="urn:ietf:params:xml:ns:fee-0.12"
|
||||
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
|
||||
<fee:cd>
|
||||
<fee:object>
|
||||
<domain:name>rich.example</domain:name>
|
||||
</fee:object>
|
||||
<fee:command name="renew">
|
||||
<fee:period unit="y">1</fee:period>
|
||||
<fee:fee description="renew">%RENEWPRICE%</fee:fee>
|
||||
</fee:command>
|
||||
</fee:cd>
|
||||
</fee:chkData>
|
||||
</extension>
|
||||
<trID>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
<svTRID>server-trid</svTRID>
|
||||
</trID>
|
||||
</response>
|
||||
</epp>
|
||||
@@ -0,0 +1,18 @@
|
||||
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||
<command>
|
||||
<check>
|
||||
<domain:check xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
|
||||
<domain:name>rich.example</domain:name>
|
||||
</domain:check>
|
||||
</check>
|
||||
<extension>
|
||||
<fee:check xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
|
||||
<fee:domain>
|
||||
<fee:name>rich.example</fee:name>
|
||||
<fee:command>renew</fee:command>
|
||||
</fee:domain>
|
||||
</fee:check>
|
||||
</extension>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
</command>
|
||||
</epp>
|
||||
@@ -0,0 +1,18 @@
|
||||
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||
<command>
|
||||
<check>
|
||||
<domain:check xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
|
||||
<domain:name>rich.example</domain:name>
|
||||
</domain:check>
|
||||
</check>
|
||||
<extension>
|
||||
<fee:check xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
|
||||
<fee:domain>
|
||||
<fee:name>rich.example</fee:name>
|
||||
<fee:command>transfer</fee:command>
|
||||
</fee:domain>
|
||||
</fee:check>
|
||||
</extension>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
</command>
|
||||
</epp>
|
||||
@@ -0,0 +1,15 @@
|
||||
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||
<command>
|
||||
<check>
|
||||
<domain:check xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
|
||||
<domain:name>rich.example</domain:name>
|
||||
</domain:check>
|
||||
</check>
|
||||
<extension>
|
||||
<fee:check xmlns:fee="urn:ietf:params:xml:ns:fee-0.12">
|
||||
<fee:command name="renew" />
|
||||
</fee:check>
|
||||
</extension>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
</command>
|
||||
</epp>
|
||||
@@ -0,0 +1,30 @@
|
||||
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||
<response>
|
||||
<result code="1000">
|
||||
<msg>Command completed successfully</msg>
|
||||
</result>
|
||||
<resData>
|
||||
<domain:chkData xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
|
||||
<domain:cd>
|
||||
<domain:name avail="0">rich.example</domain:name>
|
||||
<domain:reason>In use</domain:reason>
|
||||
</domain:cd>
|
||||
</domain:chkData>
|
||||
</resData>
|
||||
<extension>
|
||||
<fee:chkData xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
|
||||
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
|
||||
<fee:name>rich.example</fee:name>
|
||||
<fee:currency>USD</fee:currency>
|
||||
<fee:command>renew</fee:command>
|
||||
<fee:period unit="y">1</fee:period>
|
||||
<fee:fee description="renew">%RENEWPRICE%</fee:fee>
|
||||
</fee:cd>
|
||||
</fee:chkData>
|
||||
</extension>
|
||||
<trID>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
<svTRID>server-trid</svTRID>
|
||||
</trID>
|
||||
</response>
|
||||
</epp>
|
||||
@@ -0,0 +1,30 @@
|
||||
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||
<response>
|
||||
<result code="1000">
|
||||
<msg>Command completed successfully</msg>
|
||||
</result>
|
||||
<resData>
|
||||
<domain:chkData xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
|
||||
<domain:cd>
|
||||
<domain:name avail="0">rich.example</domain:name>
|
||||
<domain:reason>In use</domain:reason>
|
||||
</domain:cd>
|
||||
</domain:chkData>
|
||||
</resData>
|
||||
<extension>
|
||||
<fee:chkData xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
|
||||
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
|
||||
<fee:name>rich.example</fee:name>
|
||||
<fee:currency>USD</fee:currency>
|
||||
<fee:command>transfer</fee:command>
|
||||
<fee:period unit="y">1</fee:period>
|
||||
<fee:fee description="renew">%RENEWPRICE%</fee:fee>
|
||||
</fee:cd>
|
||||
</fee:chkData>
|
||||
</extension>
|
||||
<trID>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
<svTRID>server-trid</svTRID>
|
||||
</trID>
|
||||
</response>
|
||||
</epp>
|
||||
@@ -35,5 +35,4 @@ PATH CLASS
|
||||
/_dr/task/tmchDnl TmchDnlAction POST y API APP ADMIN
|
||||
/_dr/task/tmchSmdrl TmchSmdrlAction POST y API APP ADMIN
|
||||
/_dr/task/updateRegistrarRdapBaseUrls UpdateRegistrarRdapBaseUrlsAction GET y API APP ADMIN
|
||||
/_dr/task/wipeOutCloudSql WipeOutCloudSqlAction GET n API APP ADMIN
|
||||
/_dr/task/wipeOutContactHistoryPii WipeOutContactHistoryPiiAction GET n API APP ADMIN
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
PATH CLASS METHODS OK AUTH_METHODS MIN USER_POLICY
|
||||
/_dr/whois WhoisAction POST n API APP ADMIN
|
||||
/check CheckApiAction GET n API,LEGACY NONE PUBLIC
|
||||
/rdap/autnum/(*) RdapAutnumAction GET,HEAD n API,LEGACY NONE PUBLIC
|
||||
/rdap/domain/(*) RdapDomainAction GET,HEAD n API,LEGACY NONE PUBLIC
|
||||
/rdap/domains RdapDomainSearchAction GET,HEAD n API,LEGACY NONE PUBLIC
|
||||
/rdap/entities RdapEntitySearchAction GET,HEAD n API,LEGACY NONE PUBLIC
|
||||
/rdap/entity/(*) RdapEntityAction GET,HEAD n API,LEGACY NONE PUBLIC
|
||||
/rdap/help(*) RdapHelpAction GET,HEAD n API,LEGACY NONE PUBLIC
|
||||
/rdap/ip/(*) RdapIpAction GET,HEAD n API,LEGACY NONE PUBLIC
|
||||
/rdap/nameserver/(*) RdapNameserverAction GET,HEAD n API,LEGACY NONE PUBLIC
|
||||
/rdap/nameservers RdapNameserverSearchAction GET,HEAD n API,LEGACY NONE PUBLIC
|
||||
/whois/(*) WhoisHttpAction GET n API,LEGACY NONE PUBLIC
|
||||
/check CheckApiAction GET n API NONE PUBLIC
|
||||
/rdap/autnum/(*) RdapAutnumAction GET,HEAD n API NONE PUBLIC
|
||||
/rdap/domain/(*) RdapDomainAction GET,HEAD n API NONE PUBLIC
|
||||
/rdap/domains RdapDomainSearchAction GET,HEAD n API NONE PUBLIC
|
||||
/rdap/entities RdapEntitySearchAction GET,HEAD n API NONE PUBLIC
|
||||
/rdap/entity/(*) RdapEntityAction GET,HEAD n API NONE PUBLIC
|
||||
/rdap/help(*) RdapHelpAction GET,HEAD n API NONE PUBLIC
|
||||
/rdap/ip/(*) RdapIpAction GET,HEAD n API NONE PUBLIC
|
||||
/rdap/nameserver/(*) RdapNameserverAction GET,HEAD n API NONE PUBLIC
|
||||
/rdap/nameservers RdapNameserverSearchAction GET,HEAD n API NONE PUBLIC
|
||||
/whois/(*) WhoisHttpAction GET n API NONE PUBLIC
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -145,3 +145,5 @@ V144__drop_database_migration_state_schedule_table.sql
|
||||
V145__add_breakglass_mode_to_tld_table.sql
|
||||
V146__last_update_time_via_epp.sql
|
||||
V147__drop_gaia_id_from_user.sql
|
||||
V148__add_bsa_download_and_label_tables.sql
|
||||
V149__add_bsa_domain_in_use_table.sql
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
-- Copyright 2023 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 TABLE "BsaDownload" (
|
||||
job_id bigserial not null,
|
||||
block_list_checksums text not null,
|
||||
creation_time timestamptz not null,
|
||||
stage text not null,
|
||||
update_timestamp timestamptz,
|
||||
primary key (job_id)
|
||||
);
|
||||
|
||||
CREATE TABLE "BsaLabel" (
|
||||
label text not null,
|
||||
creation_time timestamptz not null,
|
||||
primary key (label)
|
||||
);
|
||||
|
||||
CREATE INDEX IDXj874kw19bgdnkxo1rue45jwlw on "BsaDownload" (creation_time);
|
||||
@@ -0,0 +1,27 @@
|
||||
-- Copyright 2023 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 TABLE "BsaDomainInUse" (
|
||||
label text not null,
|
||||
tld text not null,
|
||||
creation_time timestamptz not null,
|
||||
reason text not null,
|
||||
primary key (label, tld)
|
||||
);
|
||||
|
||||
ALTER TABLE IF EXISTS "BsaDomainInUse"
|
||||
ADD CONSTRAINT FKbsadomaininuse2label
|
||||
FOREIGN KEY (label)
|
||||
REFERENCES "BsaLabel" (label)
|
||||
ON DELETE CASCADE;
|
||||
@@ -123,6 +123,60 @@ CREATE TABLE public."BillingRecurrence" (
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- Name: BsaDomainInUse; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE TABLE public."BsaDomainInUse" (
|
||||
label text NOT NULL,
|
||||
tld text NOT NULL,
|
||||
creation_time timestamp with time zone NOT NULL,
|
||||
reason text NOT NULL
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- Name: BsaDownload; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE TABLE public."BsaDownload" (
|
||||
job_id bigint NOT NULL,
|
||||
block_list_checksums text NOT NULL,
|
||||
creation_time timestamp with time zone NOT NULL,
|
||||
stage text NOT NULL,
|
||||
update_timestamp timestamp with time zone
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- Name: BsaDownload_job_id_seq; Type: SEQUENCE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE SEQUENCE public."BsaDownload_job_id_seq"
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
--
|
||||
-- Name: BsaDownload_job_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER SEQUENCE public."BsaDownload_job_id_seq" OWNED BY public."BsaDownload".job_id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: BsaLabel; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE TABLE public."BsaLabel" (
|
||||
label text NOT NULL,
|
||||
creation_time timestamp with time zone NOT NULL
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- Name: ClaimsEntry; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
@@ -1155,6 +1209,13 @@ CREATE SEQUENCE public.project_wide_unique_id_seq
|
||||
CACHE 10;
|
||||
|
||||
|
||||
--
|
||||
-- Name: BsaDownload job_id; Type: DEFAULT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public."BsaDownload" ALTER COLUMN job_id SET DEFAULT nextval('public."BsaDownload_job_id_seq"'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: ClaimsList revision_id; Type: DEFAULT; Schema: public; Owner: -
|
||||
--
|
||||
@@ -1257,6 +1318,30 @@ ALTER TABLE ONLY public."BillingRecurrence"
|
||||
ADD CONSTRAINT "BillingRecurrence_pkey" PRIMARY KEY (billing_recurrence_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: BsaDomainInUse BsaDomainInUse_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public."BsaDomainInUse"
|
||||
ADD CONSTRAINT "BsaDomainInUse_pkey" PRIMARY KEY (label, tld);
|
||||
|
||||
|
||||
--
|
||||
-- Name: BsaDownload BsaDownload_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public."BsaDownload"
|
||||
ADD CONSTRAINT "BsaDownload_pkey" PRIMARY KEY (job_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: BsaLabel BsaLabel_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public."BsaLabel"
|
||||
ADD CONSTRAINT "BsaLabel_pkey" PRIMARY KEY (label);
|
||||
|
||||
|
||||
--
|
||||
-- Name: ClaimsEntry ClaimsEntry_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
@@ -1847,6 +1932,13 @@ CREATE INDEX idxj1mtx98ndgbtb1bkekahms18w ON public."GracePeriod" USING btree (d
|
||||
CREATE INDEX idxj77pfwhui9f0i7wjq6lmibovj ON public."HostHistory" USING btree (host_name);
|
||||
|
||||
|
||||
--
|
||||
-- Name: idxj874kw19bgdnkxo1rue45jwlw; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX idxj874kw19bgdnkxo1rue45jwlw ON public."BsaDownload" USING btree (creation_time);
|
||||
|
||||
|
||||
--
|
||||
-- Name: idxjny8wuot75b5e6p38r47wdawu; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@@ -2622,6 +2714,14 @@ ALTER TABLE ONLY public."DomainHistoryHost"
|
||||
ADD CONSTRAINT fka9woh3hu8gx5x0vly6bai327n FOREIGN KEY (domain_history_domain_repo_id, domain_history_history_revision_id) REFERENCES public."DomainHistory"(domain_repo_id, history_revision_id) DEFERRABLE INITIALLY DEFERRED;
|
||||
|
||||
|
||||
--
|
||||
-- Name: BsaDomainInUse fkbsadomaininuse2label; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public."BsaDomainInUse"
|
||||
ADD CONSTRAINT fkbsadomaininuse2label FOREIGN KEY (label) REFERENCES public."BsaLabel"(label) ON DELETE CASCADE;
|
||||
|
||||
|
||||
--
|
||||
-- Name: DomainTransactionRecord fkcjqe54u72kha71vkibvxhjye7; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
@@ -210,7 +210,7 @@ ext {
|
||||
'org.apache.httpcomponents:httpcore:[4.4.13,)',
|
||||
'org.apache.tomcat:tomcat-annotations-api:[8.0.5,)',
|
||||
'com.fasterxml.jackson.core:jackson-databind:[2.11.2,)',
|
||||
'org.flywaydb:flyway-core:[5.2.4,)',
|
||||
'org.flywaydb:flyway-core:[5.2.4,10.0)!!',
|
||||
'org.glassfish.jaxb:jaxb-runtime:[2.3.0,)',
|
||||
'org.hamcrest:hamcrest:[2.2,)',
|
||||
'org.hamcrest:hamcrest-core:[2.2,)',
|
||||
|
||||
14
package-lock.json
generated
14
package-lock.json
generated
@@ -1,10 +1,18 @@
|
||||
{
|
||||
"name": "nomulus",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"google-closure-library": {
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "nomulus",
|
||||
"version": "1.0.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"google-closure-library": "20210406.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/google-closure-library": {
|
||||
"version": "20210406.0.0",
|
||||
"resolved": "https://registry.npmjs.org/google-closure-library/-/google-closure-library-20210406.0.0.tgz",
|
||||
"integrity": "sha512-1lAC/KC9R2QM6nygniM0pRcGrv5bkCUrIZb2hXFxLtAkA+zRiVeWtRYpFWDHXXJzkavKjsn9upiffL4x/nmmVg=="
|
||||
|
||||
@@ -28,7 +28,7 @@ COPY go.sum ./
|
||||
COPY go.mod ./
|
||||
RUN go build -o /deployCloudSchedulerAndQueue
|
||||
|
||||
FROM marketplace.gcr.io/google/debian10
|
||||
FROM marketplace.gcr.io/google/debian11
|
||||
ENV DEBIAN_FRONTEND=noninteractive LANG=en_US.UTF-8
|
||||
# Add script for cloud scheduler and cloud tasks deployment
|
||||
COPY --from=deployCloudSchedulerAndQueueBuilder /deployCloudSchedulerAndQueue /usr/local/bin/deployCloudSchedulerAndQueue
|
||||
|
||||
@@ -24,7 +24,7 @@ apt-get install gnupg2 -y
|
||||
# Install Java
|
||||
apt-get install openjdk-11-jdk-headless -y
|
||||
# Install Python
|
||||
apt-get install python -y
|
||||
apt-get install python3 -y
|
||||
# As of March 2021 python3 is at v3.6. Get pip then install dataclasses
|
||||
# (introduced in 3.7) for nom_build
|
||||
apt-get install python3-pip -y
|
||||
@@ -32,8 +32,12 @@ python3 -m pip install dataclasses
|
||||
# Install curl.
|
||||
apt-get install curl -y
|
||||
# Install Node
|
||||
curl -sL https://deb.nodesource.com/setup_current.x | bash -
|
||||
apt-get install -y nodejs
|
||||
apt-get install npm -y
|
||||
npm cache clean -f
|
||||
npm install -g n
|
||||
# Retrying because fails are possible for node.js intallation. See -
|
||||
# https://github.com/nodejs/build/issues/1993
|
||||
for i in {1..5}; do n 16.19.0 && break || sleep 15; done
|
||||
# Install gcloud
|
||||
# Cribbed from https://cloud.google.com/sdk/docs/quickstart-debian-ubuntu
|
||||
apt-get install lsb-release -y
|
||||
|
||||
@@ -15,33 +15,114 @@
|
||||
package google.registry.util;
|
||||
|
||||
import static com.google.common.io.BaseEncoding.base64;
|
||||
import static google.registry.util.PasswordUtils.HashAlgorithm.SCRYPT;
|
||||
import static google.registry.util.PasswordUtils.HashAlgorithm.SHA256;
|
||||
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||
|
||||
import com.google.common.base.Supplier;
|
||||
import com.google.common.base.Suppliers;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import org.bouncycastle.crypto.generators.SCrypt;
|
||||
|
||||
/** Common utility class to handle password hashing and salting */
|
||||
public final class PasswordUtils {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
private static final Supplier<MessageDigest> SHA256_DIGEST_SUPPLIER =
|
||||
Suppliers.memoize(
|
||||
() -> {
|
||||
try {
|
||||
return MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// All implementations of MessageDigest are required to support SHA-256.
|
||||
throw new RuntimeException(
|
||||
"All MessageDigest implementations are required to support SHA-256 but this one"
|
||||
+ " didn't",
|
||||
e);
|
||||
}
|
||||
});
|
||||
|
||||
private PasswordUtils() {}
|
||||
|
||||
/**
|
||||
* Password hashing algorithm that takes a password and a salt (both as {@code byte[]}) and
|
||||
* returns a hash.
|
||||
*/
|
||||
public enum HashAlgorithm {
|
||||
/**
|
||||
* SHA-2 that returns a 256-bit digest.
|
||||
*
|
||||
* @see <a href="https://en.wikipedia.org/wiki/SHA-2">SHA-2</a>
|
||||
*/
|
||||
@Deprecated
|
||||
SHA256 {
|
||||
@Override
|
||||
byte[] hash(byte[] password, byte[] salt) {
|
||||
return SHA256_DIGEST_SUPPLIER
|
||||
.get()
|
||||
.digest((new String(password, US_ASCII) + base64().encode(salt)).getBytes(US_ASCII));
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Memory-hard hashing algorithm, preferred over SHA-256.
|
||||
*
|
||||
* @see <a href="https://en.wikipedia.org/wiki/Scrypt">Scrypt</a>
|
||||
*/
|
||||
SCRYPT {
|
||||
@Override
|
||||
byte[] hash(byte[] password, byte[] salt) {
|
||||
return SCrypt.generate(password, salt, 32768, 8, 1, 256);
|
||||
}
|
||||
};
|
||||
|
||||
abstract byte[] hash(byte[] password, byte[] salt);
|
||||
}
|
||||
|
||||
public static final Supplier<byte[]> SALT_SUPPLIER =
|
||||
() -> {
|
||||
// There are 32 bytes in a SHA-256 hash, and the salt should generally be the same size.
|
||||
// The generated hashes are 256 bits, and the salt should generally be of the same size.
|
||||
byte[] salt = new byte[32];
|
||||
new SecureRandom().nextBytes(salt);
|
||||
return salt;
|
||||
};
|
||||
|
||||
public static String hashPassword(String password, String salt) {
|
||||
try {
|
||||
return base64()
|
||||
.encode(
|
||||
MessageDigest.getInstance("SHA-256").digest((password + salt).getBytes(US_ASCII)));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// All implementations of MessageDigest are required to support SHA-256.
|
||||
throw new RuntimeException(
|
||||
"All MessageDigest implementations are required to support SHA-256 but this didn't", e);
|
||||
public static String hashPassword(String password, byte[] salt) {
|
||||
return hashPassword(password, salt, SCRYPT);
|
||||
}
|
||||
|
||||
/** Returns the hash of the password using the provided salt and {@link HashAlgorithm}. */
|
||||
public static String hashPassword(String password, byte[] salt, HashAlgorithm algorithm) {
|
||||
return base64().encode(algorithm.hash(password.getBytes(US_ASCII), salt));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a password by regenerating the hash with the provided salt and comparing it to the
|
||||
* provided hash.
|
||||
*
|
||||
* <p>This method will first try to use {@link HashAlgorithm#SCRYPT} to verify the password, and
|
||||
* falls back to {@link HashAlgorithm#SHA256} if the former fails.
|
||||
*
|
||||
* @return the {@link HashAlgorithm} used to successfully verify the password, or {@link
|
||||
* Optional#empty()} if neither works.
|
||||
*/
|
||||
public static Optional<HashAlgorithm> verifyPassword(String password, String hash, String salt) {
|
||||
byte[] decodedHash = base64().decode(hash);
|
||||
byte[] decodedSalt = base64().decode(salt);
|
||||
byte[] calculatedHash = SCRYPT.hash(password.getBytes(US_ASCII), decodedSalt);
|
||||
if (Arrays.equals(decodedHash, calculatedHash)) {
|
||||
logger.atInfo().log("Scrypt hash verified.");
|
||||
return Optional.of(SCRYPT);
|
||||
}
|
||||
calculatedHash = SHA256.hash(password.getBytes(US_ASCII), decodedSalt);
|
||||
if (Arrays.equals(decodedHash, calculatedHash)) {
|
||||
logger.atInfo().log("SHA256 hash verified.");
|
||||
return Optional.of(SHA256);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,13 +16,16 @@ package google.registry.util;
|
||||
|
||||
import static com.google.common.io.BaseEncoding.base64;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.util.PasswordUtils.HashAlgorithm.SCRYPT;
|
||||
import static google.registry.util.PasswordUtils.HashAlgorithm.SHA256;
|
||||
import static google.registry.util.PasswordUtils.SALT_SUPPLIER;
|
||||
import static google.registry.util.PasswordUtils.hashPassword;
|
||||
import static google.registry.util.PasswordUtils.verifyPassword;
|
||||
|
||||
import java.util.Arrays;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link google.registry.util.PasswordUtils}. */
|
||||
/** Unit tests for {@link PasswordUtils}. */
|
||||
final class PasswordUtilsTest {
|
||||
|
||||
@Test
|
||||
@@ -36,12 +39,40 @@ final class PasswordUtilsTest {
|
||||
|
||||
@Test
|
||||
void testHash() {
|
||||
String salt = base64().encode(SALT_SUPPLIER.get());
|
||||
byte[] salt = SALT_SUPPLIER.get();
|
||||
String password = "mySuperSecurePassword";
|
||||
String hashedPassword = hashPassword(password, salt);
|
||||
assertThat(hashedPassword).isEqualTo(hashPassword(password, salt));
|
||||
assertThat(hashedPassword).isNotEqualTo(hashPassword(password + "a", salt));
|
||||
String secondSalt = base64().encode(SALT_SUPPLIER.get());
|
||||
byte[] secondSalt = SALT_SUPPLIER.get();
|
||||
assertThat(hashedPassword).isNotEqualTo(hashPassword(password, secondSalt));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVerify_scrypt_default() {
|
||||
byte[] salt = SALT_SUPPLIER.get();
|
||||
String password = "mySuperSecurePassword";
|
||||
String hashedPassword = hashPassword(password, salt);
|
||||
assertThat(hashedPassword).isEqualTo(hashPassword(password, salt, SCRYPT));
|
||||
assertThat(verifyPassword(password, hashedPassword, base64().encode(salt)).get())
|
||||
.isEqualTo(SCRYPT);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVerify_sha256() {
|
||||
byte[] salt = SALT_SUPPLIER.get();
|
||||
String password = "mySuperSecurePassword";
|
||||
String hashedPassword = hashPassword(password, salt, SHA256);
|
||||
assertThat(verifyPassword(password, hashedPassword, base64().encode(salt)).get())
|
||||
.isEqualTo(SHA256);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVerify_failure() {
|
||||
byte[] salt = SALT_SUPPLIER.get();
|
||||
String password = "mySuperSecurePassword";
|
||||
String hashedPassword = hashPassword(password, salt);
|
||||
assertThat(verifyPassword(password + "a", hashedPassword, base64().encode(salt)).isEmpty())
|
||||
.isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user