mirror of
https://github.com/google/nomulus
synced 2026-08-13 18:56:14 +00:00
Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6123f10cdf | ||
|
|
f408dc03f1 | ||
|
|
575192016f | ||
|
|
76bd13ddf0 | ||
|
|
8cbf3242a7 | ||
|
|
b1f2eb5921 | ||
|
|
4f332d397e | ||
|
|
c9a82f1322 | ||
|
|
fabf0c07b2 | ||
|
|
aa54f9ddc9 | ||
|
|
92edbfbde2 | ||
|
|
b1e127798f | ||
|
|
3474cd6e9b | ||
|
|
1fc4a281c0 | ||
|
|
9a420a69b0 | ||
|
|
a3421f2999 | ||
|
|
72c610688a | ||
|
|
b5ae51a036 | ||
|
|
92b684d7ec | ||
|
|
49cecf6776 | ||
|
|
4cc3fc9cd2 | ||
|
|
74f441765e | ||
|
|
0ce83c8b2d | ||
|
|
fc4246ea95 | ||
|
|
29def8d78d | ||
|
|
0c79414a31 | ||
|
|
ea7d5d4a5e | ||
|
|
553fa1dc14 | ||
|
|
c36087dc93 |
@@ -169,8 +169,8 @@ public class BatchModule {
|
||||
|
||||
@Provides
|
||||
@Parameter("losingRegistrarId")
|
||||
static String provideLosingRegistrarId(HttpServletRequest req) {
|
||||
return extractRequiredParameter(req, "losingRegistrarId");
|
||||
static Optional<String> provideLosingRegistrarId(HttpServletRequest req) {
|
||||
return extractOptionalParameter(req, "losingRegistrarId");
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -109,7 +109,7 @@ public class BulkDomainTransferAction implements Runnable {
|
||||
private final RateLimiter rateLimiter;
|
||||
private final ImmutableList<String> bulkTransferDomainNames;
|
||||
private final String gainingRegistrarId;
|
||||
private final String losingRegistrarId;
|
||||
private final Optional<String> losingRegistrarId;
|
||||
private final boolean requestedByRegistrar;
|
||||
private final String reason;
|
||||
private final Response response;
|
||||
@@ -127,7 +127,7 @@ public class BulkDomainTransferAction implements Runnable {
|
||||
@Named("standardRateLimiter") RateLimiter rateLimiter,
|
||||
@Parameter("bulkTransferDomainNames") ImmutableList<String> bulkTransferDomainNames,
|
||||
@Parameter("gainingRegistrarId") String gainingRegistrarId,
|
||||
@Parameter("losingRegistrarId") String losingRegistrarId,
|
||||
@Parameter("losingRegistrarId") Optional<String> losingRegistrarId,
|
||||
@Parameter("requestedByRegistrar") boolean requestedByRegistrar,
|
||||
@Parameter("reason") String reason,
|
||||
Response response) {
|
||||
@@ -225,7 +225,7 @@ public class BulkDomainTransferAction implements Runnable {
|
||||
alreadyTransferred++;
|
||||
return true;
|
||||
}
|
||||
if (!currentRegistrarId.equals(losingRegistrarId)) {
|
||||
if (losingRegistrarId.isPresent() && !currentRegistrarId.equals(losingRegistrarId.get())) {
|
||||
logger.atWarning().log(
|
||||
"Domain '%s' had unexpected registrar '%s'", domainName, currentRegistrarId);
|
||||
errors++;
|
||||
|
||||
@@ -28,6 +28,9 @@ import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.net.MediaType;
|
||||
import com.google.monitoring.metrics.IncrementableMetric;
|
||||
import com.google.monitoring.metrics.LabelDescriptor;
|
||||
import com.google.monitoring.metrics.MetricRegistryImpl;
|
||||
import google.registry.cache.SimplifiedJedisClient;
|
||||
import google.registry.model.EppResource;
|
||||
import google.registry.model.common.Cursor;
|
||||
@@ -39,6 +42,7 @@ import google.registry.request.Action;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.request.lock.LockHandler;
|
||||
import google.registry.util.NonFinalForTesting;
|
||||
import jakarta.inject.Inject;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
@@ -61,6 +65,25 @@ public class SyncRemoteCacheAction implements Runnable {
|
||||
private static final String LOCK_NAME = "syncRemoteCacheAction";
|
||||
private static final int BATCH_SIZE = 10000;
|
||||
|
||||
public enum SyncStatus {
|
||||
SUCCESS,
|
||||
FAILURE,
|
||||
NOT_CONFIGURED
|
||||
}
|
||||
|
||||
private static final ImmutableSet<LabelDescriptor> LABEL_DESCRIPTORS =
|
||||
ImmutableSet.of(
|
||||
LabelDescriptor.create("status", "Whether SyncRemoteCacheAction succeeded or failed."));
|
||||
|
||||
@NonFinalForTesting
|
||||
static final IncrementableMetric SYNC_CACHE_RUNS_METRIC =
|
||||
MetricRegistryImpl.getDefault()
|
||||
.newIncrementableMetric(
|
||||
"/batch/sync_remote_cache/runs",
|
||||
"Count of SyncRemoteCacheAction executions",
|
||||
"count",
|
||||
LABEL_DESCRIPTORS);
|
||||
|
||||
private final LockHandler lockHandler;
|
||||
private final Response response;
|
||||
private final Optional<SimplifiedJedisClient> jedisClient;
|
||||
@@ -79,14 +102,17 @@ public class SyncRemoteCacheAction implements Runnable {
|
||||
if (jedisClient.isEmpty()) {
|
||||
response.setStatus(SC_NO_CONTENT);
|
||||
response.setPayload("No Jedis/Valkey configuration found");
|
||||
SYNC_CACHE_RUNS_METRIC.increment(SyncStatus.NOT_CONFIGURED.name());
|
||||
return;
|
||||
}
|
||||
Callable<Void> runner =
|
||||
() -> {
|
||||
try {
|
||||
runLocked();
|
||||
SYNC_CACHE_RUNS_METRIC.increment(SyncStatus.SUCCESS.name());
|
||||
response.setStatus(SC_OK);
|
||||
} catch (Exception e) {
|
||||
SYNC_CACHE_RUNS_METRIC.increment(SyncStatus.FAILURE.name());
|
||||
logger.atSevere().withCause(e).log("Errored out during execution.");
|
||||
response.setStatus(SC_INTERNAL_SERVER_ERROR);
|
||||
response.setPayload(String.format("Errored out with cause: %s", e));
|
||||
@@ -95,6 +121,7 @@ public class SyncRemoteCacheAction implements Runnable {
|
||||
};
|
||||
|
||||
if (!lockHandler.executeWithLocks(runner, null, Duration.ofHours(1), LOCK_NAME)) {
|
||||
SYNC_CACHE_RUNS_METRIC.increment(SyncStatus.FAILURE.name());
|
||||
// Send a 200-series status code to prevent this conflicting action from retrying.
|
||||
response.setStatus(SC_NO_CONTENT);
|
||||
response.setPayload("Could not acquire lock; already running?");
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
package google.registry.beam.spec11;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import dagger.Component;
|
||||
@@ -25,11 +24,9 @@ import google.registry.beam.common.RegistryJpaIO;
|
||||
import google.registry.beam.common.RegistryJpaIO.Read;
|
||||
import google.registry.beam.spec11.SafeBrowsingTransforms.EvaluateSafeBrowsingFn;
|
||||
import google.registry.config.RegistryConfig.ConfigModule;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.reporting.Spec11ThreatMatch;
|
||||
import google.registry.model.reporting.Spec11ThreatMatch.ThreatType;
|
||||
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.util.Clock;
|
||||
import google.registry.util.Retrier;
|
||||
import google.registry.util.UtilsModule;
|
||||
@@ -39,8 +36,7 @@ import java.time.LocalDate;
|
||||
import java.time.YearMonth;
|
||||
import org.apache.beam.sdk.Pipeline;
|
||||
import org.apache.beam.sdk.PipelineResult;
|
||||
import org.apache.beam.sdk.coders.KvCoder;
|
||||
import org.apache.beam.sdk.coders.StringUtf8Coder;
|
||||
import org.apache.beam.sdk.coders.SerializableCoder;
|
||||
import org.apache.beam.sdk.io.TextIO;
|
||||
import org.apache.beam.sdk.options.PipelineOptionsFactory;
|
||||
import org.apache.beam.sdk.transforms.DoFn;
|
||||
@@ -112,44 +108,22 @@ public class Spec11Pipeline implements Serializable {
|
||||
}
|
||||
|
||||
static PCollection<DomainNameInfo> readFromCloudSql(Pipeline pipeline) {
|
||||
Read<Object[], KV<String, String>> read =
|
||||
Read<Object[], DomainNameInfo> read =
|
||||
RegistryJpaIO.read(
|
||||
"select d.repoId, r.emailAddress from Domain d join Registrar r on"
|
||||
+ " d.currentSponsorRegistrarId = r.registrarId where r.type = 'REAL' and"
|
||||
+ " d.deletionTime > CAST(now() AS timestamp)",
|
||||
"""
|
||||
SELECT d.domainName, d.repoId, d.currentSponsorRegistrarId, r.emailAddress FROM
|
||||
Domain d JOIN Registrar r ON d.currentSponsorRegistrarId = r.registrarId WHERE
|
||||
r.type = 'REAL' AND d.deletionTime > CAST(now() AS timestamp)
|
||||
""",
|
||||
false,
|
||||
Spec11Pipeline::parseRow)
|
||||
.withCoder(KvCoder.of(StringUtf8Coder.of(), StringUtf8Coder.of()));
|
||||
|
||||
return pipeline
|
||||
.apply("Read active domains from Cloud SQL", read)
|
||||
.apply(
|
||||
"Build DomainNameInfo",
|
||||
ParDo.of(
|
||||
new DoFn<KV<String, String>, DomainNameInfo>() {
|
||||
@ProcessElement
|
||||
public void processElement(
|
||||
@Element KV<String, String> input, OutputReceiver<DomainNameInfo> output) {
|
||||
Domain domain =
|
||||
tm().transact(
|
||||
() -> tm().loadByKey(VKey.create(Domain.class, input.getKey())));
|
||||
String emailAddress = input.getValue();
|
||||
if (emailAddress == null) {
|
||||
emailAddress = "";
|
||||
}
|
||||
DomainNameInfo domainNameInfo =
|
||||
DomainNameInfo.create(
|
||||
domain.getDomainName(),
|
||||
domain.getRepoId(),
|
||||
domain.getCurrentSponsorRegistrarId(),
|
||||
emailAddress);
|
||||
output.output(domainNameInfo);
|
||||
}
|
||||
}));
|
||||
.withCoder(SerializableCoder.of(DomainNameInfo.class));
|
||||
return pipeline.apply("Read active domains from Cloud SQL", read);
|
||||
}
|
||||
|
||||
private static KV<String, String> parseRow(Object[] row) {
|
||||
return KV.of((String) row[0], (String) row[1]);
|
||||
private static DomainNameInfo parseRow(Object[] row) {
|
||||
String emailAddress = row[3] != null ? (String) row[3] : "";
|
||||
return new DomainNameInfo((String) row[0], (String) row[1], (String) row[2], emailAddress);
|
||||
}
|
||||
|
||||
static void saveToSql(
|
||||
|
||||
@@ -1587,6 +1587,12 @@ public final class RegistryConfig {
|
||||
return config.eppServer.readTimeoutSeconds;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Config("eppServerPreLoginReadTimeoutSeconds")
|
||||
public static int provideEppServerPreLoginReadTimeoutSeconds(RegistryConfigSettings config) {
|
||||
return config.eppServer.preLoginReadTimeoutSeconds;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Config("eppServerMaxConnectionsPerIp")
|
||||
public static int provideEppServerMaxConnectionsPerIp(RegistryConfigSettings config) {
|
||||
@@ -1594,9 +1600,9 @@ public final class RegistryConfig {
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Config("eppServerMaxConnectionsPerCert")
|
||||
public static int provideEppServerMaxConnectionsPerCert(RegistryConfigSettings config) {
|
||||
return config.eppServer.maxConnectionsPerCert;
|
||||
@Config("eppServerMaxConnectionsPerRegistrar")
|
||||
public static int provideEppServerMaxConnectionsPerRegistrar(RegistryConfigSettings config) {
|
||||
return config.eppServer.maxConnectionsPerRegistrar;
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -217,8 +217,9 @@ public class RegistryConfigSettings {
|
||||
public int maxMessageLengthBytes;
|
||||
public int headerLengthBytes;
|
||||
public int readTimeoutSeconds;
|
||||
public int preLoginReadTimeoutSeconds;
|
||||
public int maxConnectionsPerIp;
|
||||
public int maxConnectionsPerCert;
|
||||
public int maxConnectionsPerRegistrar;
|
||||
public int serverCertificateCacheSeconds;
|
||||
public Quota quota;
|
||||
}
|
||||
|
||||
@@ -457,20 +457,22 @@ eppServer:
|
||||
headerLengthBytes: 4
|
||||
# Time after which an idle connection will be closed.
|
||||
readTimeoutSeconds: 3600
|
||||
# Time after which an idle connection will be closed before login.
|
||||
preLoginReadTimeoutSeconds: 10
|
||||
# Max concurrent connections per IP address.
|
||||
maxConnectionsPerIp: 10
|
||||
# Max concurrent connections per authenticated certificate.
|
||||
maxConnectionsPerCert: 10
|
||||
# Max concurrent connections per authenticated registrar.
|
||||
maxConnectionsPerRegistrar: 10
|
||||
# Server certificate cache duration.
|
||||
serverCertificateCacheSeconds: 1800
|
||||
|
||||
# Quota configuration for EPP
|
||||
quota:
|
||||
refreshSeconds: 0
|
||||
# Default quota applies individually to any IP or registrar NOT listed in customQuota
|
||||
defaultQuota:
|
||||
userId: []
|
||||
tokenAmount: 100
|
||||
refillSeconds: 0
|
||||
refillSeconds: 10
|
||||
# To implement a shared quota group across multiple registrars, place a virtual
|
||||
# group name as the FIRST element of the userId list.
|
||||
# e.g., userId: ["my_group", "registrar1", "registrar2"]
|
||||
|
||||
+1
@@ -326,6 +326,7 @@
|
||||
<task>
|
||||
<url><![CDATA[/_dr/task/syncRemoteCache]]></url>
|
||||
<name>syncRemoteCache</name>
|
||||
<method>POST</method>
|
||||
<description>
|
||||
Syncs remote (Valkey/Redis) EPP resource caches with changes made recently.
|
||||
</description>
|
||||
|
||||
@@ -14,14 +14,18 @@
|
||||
|
||||
package google.registry.dns;
|
||||
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static google.registry.dns.DnsUtils.requestDomainDnsRefresh;
|
||||
import static google.registry.dns.RefreshDnsOnHostRenameAction.PATH;
|
||||
import static google.registry.model.EppResourceUtils.getLinkedDomainKeys;
|
||||
import static google.registry.model.EppResourceUtils.isDeleted;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_NO_CONTENT;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.net.MediaType;
|
||||
import google.registry.model.EppResourceUtils;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.host.Host;
|
||||
import google.registry.persistence.VKey;
|
||||
@@ -29,8 +33,11 @@ import google.registry.request.Action;
|
||||
import google.registry.request.Parameter;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.util.Clock;
|
||||
import jakarta.inject.Inject;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Action(
|
||||
service = Action.Service.BACKEND,
|
||||
@@ -43,45 +50,52 @@ public class RefreshDnsOnHostRenameAction implements Runnable {
|
||||
public static final String PARAM_HOST_KEY = "hostKey";
|
||||
public static final String PATH = "/_dr/task/refreshDnsOnHostRename";
|
||||
|
||||
private static final int DNS_REFRESH_BATCH_SIZE = 1000;
|
||||
|
||||
private final VKey<Host> hostKey;
|
||||
private final Response response;
|
||||
private final Clock clock;
|
||||
|
||||
@Inject
|
||||
RefreshDnsOnHostRenameAction(@Parameter(PARAM_HOST_KEY) String hostKey, Response response) {
|
||||
RefreshDnsOnHostRenameAction(
|
||||
@Parameter(PARAM_HOST_KEY) String hostKey, Response response, Clock clock) {
|
||||
this.hostKey = VKey.createEppVKeyFromString(hostKey);
|
||||
this.response = response;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
tm().transact(
|
||||
() -> {
|
||||
Instant now = tm().getTxTime();
|
||||
Host host = tm().loadByKeyIfPresent(hostKey).orElse(null);
|
||||
boolean hostValid = true;
|
||||
String failureMessage = null;
|
||||
if (host == null) {
|
||||
hostValid = false;
|
||||
failureMessage = String.format("Host to refresh does not exist: %s", hostKey);
|
||||
} else if (EppResourceUtils.isDeleted(host, now)) {
|
||||
hostValid = false;
|
||||
failureMessage =
|
||||
String.format("Host to refresh is already deleted: %s", host.getHostName());
|
||||
} else {
|
||||
getLinkedDomainKeys(
|
||||
host.createVKey(), host.getUpdateTimestamp().getTimestamp(), null)
|
||||
.stream()
|
||||
.map(domainKey -> tm().loadByKey(domainKey))
|
||||
.filter(Domain::shouldPublishToDns)
|
||||
.forEach(domain -> requestDomainDnsRefresh(domain.getDomainName()));
|
||||
}
|
||||
Optional<Host> optionalHost = tm().transact(() -> tm().loadByKeyIfPresent(hostKey));
|
||||
if (optionalHost.isEmpty()) {
|
||||
setFailedStatus(String.format("Host to refresh does not exist: %s", hostKey));
|
||||
return;
|
||||
}
|
||||
Instant now = clock.now();
|
||||
Host host = optionalHost.get();
|
||||
if (isDeleted(host, now)) {
|
||||
setFailedStatus(String.format("Host to refresh is already deleted: %s", host.getHostName()));
|
||||
return;
|
||||
}
|
||||
ImmutableSet<VKey<Domain>> linkedDomainKeys =
|
||||
getLinkedDomainKeys(hostKey, host.getUpdateTimestamp().getTimestamp(), null);
|
||||
for (List<VKey<Domain>> batch : Iterables.partition(linkedDomainKeys, DNS_REFRESH_BATCH_SIZE)) {
|
||||
tm().transact(
|
||||
() -> {
|
||||
ImmutableSet<String> domainNames =
|
||||
tm().loadByKeysIfPresent(batch).values().stream()
|
||||
.filter(Domain::shouldPublishToDns)
|
||||
.map(Domain::getDomainName)
|
||||
.collect(toImmutableSet());
|
||||
requestDomainDnsRefresh(domainNames);
|
||||
});
|
||||
}
|
||||
response.setStatus(SC_OK);
|
||||
}
|
||||
|
||||
if (!hostValid) {
|
||||
// Set the response status code to be 204 so to not retry.
|
||||
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
|
||||
response.setStatus(SC_NO_CONTENT);
|
||||
response.setPayload(failureMessage);
|
||||
}
|
||||
});
|
||||
private void setFailedStatus(String message) {
|
||||
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
|
||||
response.setStatus(SC_NO_CONTENT);
|
||||
response.setPayload(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import dagger.multibindings.IntoSet;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.config.RegistryConfigSettings;
|
||||
import google.registry.eppserver.Protocol.FrontendProtocol;
|
||||
import google.registry.eppserver.handler.EppProxyProtocolHandler;
|
||||
import google.registry.eppserver.handler.EppServiceHandler;
|
||||
import google.registry.eppserver.quota.QuotaManager;
|
||||
import google.registry.networking.handler.SslServerInitializer;
|
||||
@@ -78,14 +77,12 @@ public final class EppProtocolModule {
|
||||
@Provides
|
||||
@EppProtocol
|
||||
static ImmutableList<Provider<? extends ChannelHandler>> provideHandlerProviders(
|
||||
Provider<EppProxyProtocolHandler> proxyProtocolHandlerProvider,
|
||||
@EppProtocol Provider<SslServerInitializer<NioSocketChannel>> sslServerInitializerProvider,
|
||||
@EppProtocol Provider<ReadTimeoutHandler> readTimeoutHandlerProvider,
|
||||
Provider<LengthFieldBasedFrameDecoder> lengthFieldBasedFrameDecoderProvider,
|
||||
Provider<LengthFieldPrepender> lengthFieldPrependerProvider,
|
||||
Provider<EppServiceHandler> eppServiceHandlerProvider) {
|
||||
return ImmutableList.of(
|
||||
proxyProtocolHandlerProvider,
|
||||
sslServerInitializerProvider,
|
||||
readTimeoutHandlerProvider,
|
||||
lengthFieldBasedFrameDecoderProvider,
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.eppserver.handler;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.ByteToMessageDecoder;
|
||||
import io.netty.util.AttributeKey;
|
||||
import jakarta.inject.Inject;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Handler that processes possible existence of a PROXY protocol v1 header.
|
||||
*
|
||||
* <p>When an EPP client connects to the registry (through the proxy), the registry performs two
|
||||
* validations to ensure that only known registrars are allowed. First it checks the sha265 hash of
|
||||
* the client SSL certificate and match it to the hash stored in the database for the registrar. It
|
||||
* then checks if the connection is from an allow-listed IP address that belongs to that registrar.
|
||||
*
|
||||
* <p>The proxy receives client connects via the GCP load balancer, which results in the loss of
|
||||
* original client IP from the channel. Luckily, the load balancer supports the PROXY protocol v1,
|
||||
* which adds a header with source IP information, among other things, to the TCP request at the
|
||||
* start of the connection.
|
||||
*
|
||||
* <p>This handler determines if a connection is proxied (PROXY protocol v1 header present) and
|
||||
* correctly sets the source IP address to the channel's attribute regardless of whether it is
|
||||
* proxied. After that it removes itself from the channel pipeline because the proxy header is only
|
||||
* present at the beginning of the connection.
|
||||
*
|
||||
* <p>This handler must be the very first handler in a protocol, even before SSL handlers, because
|
||||
* PROXY protocol header comes as the very first thing, even before SSL handshake request.
|
||||
*
|
||||
* @see <a href="https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt">The PROXY protocol</a>
|
||||
*/
|
||||
public class EppProxyProtocolHandler extends ByteToMessageDecoder {
|
||||
|
||||
/** Key used to retrieve origin IP address from a channel's attribute. */
|
||||
public static final AttributeKey<String> REMOTE_ADDRESS_KEY =
|
||||
AttributeKey.valueOf("REMOTE_ADDRESS_KEY");
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
// The proxy header must start with this prefix.
|
||||
// Sample header: "PROXY TCP4 255.255.255.255 255.255.255.255 65535 65535\r\n".
|
||||
private static final byte[] HEADER_PREFIX = "PROXY".getBytes(US_ASCII);
|
||||
|
||||
private boolean finished = false;
|
||||
private String proxyHeader = null;
|
||||
|
||||
@Inject
|
||||
EppProxyProtocolHandler() {}
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
super.channelRead(ctx, msg);
|
||||
if (finished) {
|
||||
String remoteIP;
|
||||
if (proxyHeader != null) {
|
||||
logger.atFine().log("PROXIED CONNECTION: %s", ctx.channel());
|
||||
logger.atFine().log("PROXY HEADER for channel %s: %s", ctx.channel(), proxyHeader);
|
||||
String[] headerArray = proxyHeader.split(" ", -1);
|
||||
if (headerArray.length == 6) {
|
||||
remoteIP = headerArray[2];
|
||||
logger.atFine().log(
|
||||
"Header parsed, using %s as remote IP for channel %s", remoteIP, ctx.channel());
|
||||
// If the header is "PROXY UNKNOWN"
|
||||
// (see https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt), likely when the
|
||||
// remote connection to the external load balancer is through special means, make it
|
||||
// 0.0.0.0 so that it can be treated accordingly by the relevant quota configs.
|
||||
} else if (headerArray.length == 2 && headerArray[1].equals("UNKNOWN")) {
|
||||
logger.atFine().log(
|
||||
"Header parsed, source IP unknown, using 0.0.0.0 as remote IP for channel %s",
|
||||
ctx.channel());
|
||||
remoteIP = "0.0.0.0";
|
||||
} else {
|
||||
logger.atFine().log(
|
||||
"Cannot parse the header, using source IP as remote IP for channel %s",
|
||||
ctx.channel());
|
||||
remoteIP = getSourceIP(ctx);
|
||||
}
|
||||
} else {
|
||||
logger.atFine().log(
|
||||
"No header present, using source IP directly for channel %s", ctx.channel());
|
||||
remoteIP = getSourceIP(ctx);
|
||||
}
|
||||
if (remoteIP != null) {
|
||||
ctx.channel().attr(REMOTE_ADDRESS_KEY).set(remoteIP);
|
||||
} else {
|
||||
logger.atWarning().log("Not able to obtain remote IP for channel %s", ctx.channel());
|
||||
}
|
||||
// ByteToMessageDecoder automatically flushes unread bytes in the ByteBuf to the next handler
|
||||
// when itself is being removed.
|
||||
ctx.pipeline().remove(this);
|
||||
}
|
||||
}
|
||||
|
||||
private static String getSourceIP(ChannelHandlerContext ctx) {
|
||||
SocketAddress remoteAddress = ctx.channel().remoteAddress();
|
||||
return (remoteAddress instanceof InetSocketAddress inetSocketAddress)
|
||||
? inetSocketAddress.getAddress().getHostAddress()
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to decode an internally accumulated buffer and find the proxy protocol header.
|
||||
*
|
||||
* <p>When the connection is not proxied (i. e. the initial bytes are not "PROXY"), simply set
|
||||
* {@link #finished} to true and allow the handler to be removed. Otherwise the handler waits
|
||||
* until there's enough bytes to parse the header, save the parsed header to {@link #proxyHeader},
|
||||
* and then mark {@link #finished}.
|
||||
*
|
||||
* @param in internally accumulated buffer, newly arrived bytes are appended to it.
|
||||
* @param out objects passed to the next handler, in this case nothing is ever passed because the
|
||||
* header itself is processed and written to the attribute of the proxy, and the handler is
|
||||
* then removed from the pipeline.
|
||||
*/
|
||||
@Override
|
||||
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
|
||||
// Wait until there are more bytes available than the header's length before processing.
|
||||
if (in.readableBytes() >= HEADER_PREFIX.length) {
|
||||
if (containsHeader(in)) {
|
||||
// The inbound message contains the header, it must be a proxied connection. Note that
|
||||
// currently proxied connection is only used for EPP protocol, which requires the connection
|
||||
// to be SSL enabled. So the beginning of the inbound message upon connection can only be
|
||||
// either the proxy header (when proxied), or SSL handshake request (when not proxied),
|
||||
// which does not start with "PROXY". Therefore it is safe to assume that if the beginning
|
||||
// of the message contains "PROXY", it must be proxied, and must contain \r\n.
|
||||
int eol = findEndOfLine(in);
|
||||
// If eol is not found, that is because that we do not yet have enough inbound message, do
|
||||
// nothing and wait for more bytes to be readable. eol will eventually be positive because
|
||||
// of the reasoning above: The connection starts with "PROXY", so it must be a proxied
|
||||
// connection and contain \r\n.
|
||||
if (eol >= 0) {
|
||||
// ByteBuf.readBytes is called so that the header is processed and not passed to handlers
|
||||
// further in the pipeline.
|
||||
byte[] headerBytes = new byte[eol];
|
||||
in.readBytes(headerBytes);
|
||||
proxyHeader = new String(headerBytes, US_ASCII);
|
||||
// Skip \r\n.
|
||||
in.skipBytes(2);
|
||||
// Proxy header processed, mark finished so that this handler is removed.
|
||||
finished = true;
|
||||
}
|
||||
} else {
|
||||
// The inbound message does not contain a proxy header, mark finished so that this handler
|
||||
// is removed. Note that no inbound bytes are actually processed by this handler because we
|
||||
// did not call ByteBuf.readBytes(), but ByteBuf.getByte(), which does not change reader
|
||||
// index of the ByteBuf. So any inbound byte is then passed to the next handler to process.
|
||||
finished = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index in the buffer of the end of line found. Returns -1 if no end of line was
|
||||
* found in the buffer.
|
||||
*/
|
||||
private static int findEndOfLine(final ByteBuf buffer) {
|
||||
final int n = buffer.writerIndex();
|
||||
for (int i = buffer.readerIndex(); i < n; i++) {
|
||||
final byte b = buffer.getByte(i);
|
||||
if (b == '\r' && i < n - 1 && buffer.getByte(i + 1) == '\n') {
|
||||
return i; // \r\n
|
||||
}
|
||||
}
|
||||
return -1; // Not found.
|
||||
}
|
||||
|
||||
/** Checks if the given buffer contains the proxy header prefix. */
|
||||
private boolean containsHeader(ByteBuf buffer) {
|
||||
// The readable bytes is always more or equal to the size of the header prefix because this
|
||||
// method is only called when this condition is true.
|
||||
checkState(buffer.readableBytes() >= HEADER_PREFIX.length);
|
||||
for (int i = 0; i < HEADER_PREFIX.length; ++i) {
|
||||
if (buffer.getByte(buffer.readerIndex() + i) != HEADER_PREFIX[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@
|
||||
|
||||
package google.registry.eppserver.handler;
|
||||
|
||||
import static google.registry.eppserver.handler.EppProxyProtocolHandler.REMOTE_ADDRESS_KEY;
|
||||
import static google.registry.networking.handler.SslServerInitializer.CLIENT_CERTIFICATE_PROMISE_KEY;
|
||||
import static google.registry.util.GcpJsonFormatter.setCurrentRequest;
|
||||
import static google.registry.util.GcpJsonFormatter.setCurrentTraceId;
|
||||
@@ -42,10 +41,14 @@ import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.util.AttributeKey;
|
||||
import io.netty.util.concurrent.Future;
|
||||
import io.netty.util.concurrent.Promise;
|
||||
import io.netty.util.concurrent.ScheduledFuture;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -67,20 +70,26 @@ public class EppServiceHandler extends SimpleChannelInboundHandler<ByteBuf> {
|
||||
public static final AttributeKey<String> CLIENT_CERTIFICATE_HASH_KEY =
|
||||
AttributeKey.valueOf("CLIENT_CERTIFICATE_HASH_KEY");
|
||||
|
||||
public static final AttributeKey<String> REMOTE_ADDRESS_KEY =
|
||||
AttributeKey.valueOf("REMOTE_ADDRESS_KEY");
|
||||
|
||||
private final byte[] helloBytes;
|
||||
private final FrontendMetrics metrics;
|
||||
private final LocalConnectionLimiter localConnectionLimiter;
|
||||
private final QuotaManager commandQuotaManager;
|
||||
private final Supplier<String> idTokenSupplier;
|
||||
private final String projectId;
|
||||
private final int preLoginReadTimeoutSeconds;
|
||||
|
||||
private String sslClientCertificateHash;
|
||||
private String clientAddress;
|
||||
private String registrarId; // The clID extracted from login
|
||||
private String authenticatedRegistrarId; // The verified registrar ID after successful login
|
||||
private String sessionCookie;
|
||||
|
||||
private boolean ipAcquired = false;
|
||||
private boolean certAcquired = false;
|
||||
private boolean registrarAcquired = false;
|
||||
private ScheduledFuture<?> preLoginTimeoutTask;
|
||||
|
||||
@VisibleForTesting RequestHandler<?> requestHandler = RegistryServlet.component.requestHandler();
|
||||
|
||||
@@ -91,13 +100,15 @@ public class EppServiceHandler extends SimpleChannelInboundHandler<ByteBuf> {
|
||||
LocalConnectionLimiter localConnectionLimiter,
|
||||
@CommandQuota QuotaManager commandQuotaManager,
|
||||
@Named("idToken") Supplier<String> idTokenSupplier,
|
||||
@Config("projectId") String projectId) {
|
||||
@Config("projectId") String projectId,
|
||||
@Config("eppServerPreLoginReadTimeoutSeconds") int preLoginReadTimeoutSeconds) {
|
||||
this.helloBytes = helloBytes.clone();
|
||||
this.metrics = metrics;
|
||||
this.localConnectionLimiter = localConnectionLimiter;
|
||||
this.commandQuotaManager = commandQuotaManager;
|
||||
this.idTokenSupplier = idTokenSupplier;
|
||||
this.projectId = projectId;
|
||||
this.preLoginReadTimeoutSeconds = preLoginReadTimeoutSeconds;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -110,8 +121,7 @@ public class EppServiceHandler extends SimpleChannelInboundHandler<ByteBuf> {
|
||||
ctx.executor().execute(() -> onSslHandshakeComplete(ctx, promise.getNow()));
|
||||
} else {
|
||||
logger.atWarning().withCause(promise.cause()).log("SSL handshake failed");
|
||||
@SuppressWarnings("unused")
|
||||
Future<?> unusedFuture = ctx.close();
|
||||
closeConnection(ctx);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -119,26 +129,50 @@ public class EppServiceHandler extends SimpleChannelInboundHandler<ByteBuf> {
|
||||
}
|
||||
|
||||
private void onSslHandshakeComplete(ChannelHandlerContext ctx, X509Certificate cert) {
|
||||
sslClientCertificateHash = getCertificateHash(cert);
|
||||
if (!ctx.channel().isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
clientAddress = ctx.channel().attr(REMOTE_ADDRESS_KEY).get();
|
||||
if (clientAddress == null) {
|
||||
SocketAddress remoteAddress = ctx.channel().remoteAddress();
|
||||
if (remoteAddress instanceof InetSocketAddress inetSocketAddress) {
|
||||
clientAddress = inetSocketAddress.getAddress().getHostAddress();
|
||||
}
|
||||
}
|
||||
|
||||
if (clientAddress == null || clientAddress.isEmpty()) {
|
||||
logger.atSevere().log("Failed to resolve client IP address, closing connection");
|
||||
closeConnection(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
sslClientCertificateHash = getCertificateHash(cert);
|
||||
ctx.channel().attr(CLIENT_CERTIFICATE_HASH_KEY).set(sslClientCertificateHash);
|
||||
|
||||
// 1. Connection throttling (IP and Certificate)
|
||||
// 1. Connection throttling (IP only pre-login)
|
||||
if (!localConnectionLimiter.acquireIp(clientAddress)) {
|
||||
metrics.registerQuotaRejection("epp_connection_ip", clientAddress);
|
||||
@SuppressWarnings("unused")
|
||||
Future<?> unusedFuture = ctx.close();
|
||||
closeConnection(ctx);
|
||||
return;
|
||||
}
|
||||
ipAcquired = true;
|
||||
|
||||
if (!localConnectionLimiter.acquireCert(sslClientCertificateHash)) {
|
||||
metrics.registerQuotaRejection("epp_connection", sslClientCertificateHash);
|
||||
@SuppressWarnings("unused")
|
||||
Future<?> unusedFuture = ctx.close();
|
||||
return;
|
||||
}
|
||||
certAcquired = true;
|
||||
// Schedule login timeout
|
||||
preLoginTimeoutTask =
|
||||
ctx.executor()
|
||||
.schedule(
|
||||
() -> {
|
||||
if (!registrarAcquired) {
|
||||
logger.atWarning().log(
|
||||
"EPP login timeout expired for channel %s, closing connection",
|
||||
ctx.channel());
|
||||
metrics.registerQuotaRejection("epp_login_timeout", clientAddress);
|
||||
closeConnection(ctx);
|
||||
}
|
||||
},
|
||||
preLoginReadTimeoutSeconds,
|
||||
TimeUnit.SECONDS);
|
||||
|
||||
metrics.registerActiveConnection("epp", sslClientCertificateHash, ctx.channel());
|
||||
|
||||
@@ -154,7 +188,32 @@ public class EppServiceHandler extends SimpleChannelInboundHandler<ByteBuf> {
|
||||
private void handleEppFrame(ChannelHandlerContext ctx, ByteBuf frame) {
|
||||
String xml = frame.toString(UTF_8);
|
||||
|
||||
// 1. Maturing Identity: If we don't have clID yet, try to extract it from a login command.
|
||||
extractRegistrarId(xml);
|
||||
|
||||
if (!acquireCommandQuota(ctx)) {
|
||||
return;
|
||||
}
|
||||
|
||||
FakeHttpServletRequest req = buildServletRequest(xml);
|
||||
FakeHttpServletResponse rsp = new FakeHttpServletResponse();
|
||||
String traceId =
|
||||
String.format(
|
||||
"projects/%s/traces/%s", projectId, UUID.randomUUID().toString().replace("-", ""));
|
||||
setCurrentTraceId(traceId);
|
||||
setCurrentRequest("POST", "/_dr/epp", "Netty-EPP", "EPP/1.0");
|
||||
try {
|
||||
requestHandler.handleRequest(req, rsp);
|
||||
processServletResponse(ctx, rsp);
|
||||
} catch (Exception e) {
|
||||
logger.atSevere().withCause(e).log("Internal EPP processing error");
|
||||
closeConnection(ctx);
|
||||
} finally {
|
||||
setCurrentTraceId(null);
|
||||
unsetCurrentRequest();
|
||||
}
|
||||
}
|
||||
|
||||
private void extractRegistrarId(String xml) {
|
||||
if (registrarId == null) {
|
||||
Matcher matcher = CLID_PATTERN.matcher(xml);
|
||||
if (matcher.find()) {
|
||||
@@ -162,20 +221,22 @@ public class EppServiceHandler extends SimpleChannelInboundHandler<ByteBuf> {
|
||||
logger.atInfo().log("Identified registrar: %s", registrarId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Command-level rate limiting
|
||||
// Use clID if identified, otherwise fallback to cert hash (for the login command itself).
|
||||
String throttleId = (registrarId != null) ? registrarId : sslClientCertificateHash;
|
||||
private boolean acquireCommandQuota(ChannelHandlerContext ctx) {
|
||||
String throttleId =
|
||||
(authenticatedRegistrarId != null) ? authenticatedRegistrarId : sslClientCertificateHash;
|
||||
if (throttleId != null) {
|
||||
if (!commandQuotaManager.acquireQuota(new QuotaManager.QuotaRequest(throttleId)).success()) {
|
||||
metrics.registerQuotaRejection("epp_command", throttleId);
|
||||
@SuppressWarnings("unused")
|
||||
Future<?> unusedFuture = ctx.close();
|
||||
return;
|
||||
closeConnection(ctx);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. Execute command in-process
|
||||
private FakeHttpServletRequest buildServletRequest(String xml) {
|
||||
FakeHttpServletRequest req = new FakeHttpServletRequest();
|
||||
req.setRequestUri("/_dr/epp");
|
||||
req.setBody(xml.getBytes(UTF_8));
|
||||
@@ -188,42 +249,60 @@ public class EppServiceHandler extends SimpleChannelInboundHandler<ByteBuf> {
|
||||
req.setHeader("Cookie", sessionCookie);
|
||||
}
|
||||
req.setHeader("Authorization", "Bearer " + idTokenSupplier.get());
|
||||
return req;
|
||||
}
|
||||
|
||||
FakeHttpServletResponse rsp = new FakeHttpServletResponse();
|
||||
String traceId =
|
||||
String.format(
|
||||
"projects/%s/traces/%s", projectId, UUID.randomUUID().toString().replace("-", ""));
|
||||
setCurrentTraceId(traceId);
|
||||
setCurrentRequest("POST", "/_dr/epp", "Netty-EPP", "EPP/1.0");
|
||||
try {
|
||||
requestHandler.handleRequest(req, rsp);
|
||||
String setCookie = rsp.getHeader("Set-Cookie");
|
||||
if (setCookie != null) {
|
||||
sessionCookie = setCookie;
|
||||
}
|
||||
|
||||
ByteBuf out = Unpooled.wrappedBuffer(rsp.getPayload());
|
||||
if ("close".equals(rsp.getHeader(ProxyHttpHeaders.EPP_SESSION))) {
|
||||
@SuppressWarnings("unused")
|
||||
Future<?> unusedFuture = ctx.writeAndFlush(out).addListener(ChannelFutureListener.CLOSE);
|
||||
} else {
|
||||
@SuppressWarnings("unused")
|
||||
Future<?> unusedFuture = ctx.writeAndFlush(out);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.atSevere().withCause(e).log("Internal EPP processing error");
|
||||
@SuppressWarnings("unused")
|
||||
Future<?> unusedFuture = ctx.close();
|
||||
} finally {
|
||||
setCurrentTraceId(null);
|
||||
unsetCurrentRequest();
|
||||
private void processServletResponse(ChannelHandlerContext ctx, FakeHttpServletResponse rsp) {
|
||||
String setCookie = rsp.getHeader("Set-Cookie");
|
||||
if (setCookie != null) {
|
||||
sessionCookie = setCookie;
|
||||
}
|
||||
|
||||
String authRegistrarId = rsp.getHeader(ProxyHttpHeaders.LOGGED_IN_REGISTRAR);
|
||||
if (authRegistrarId != null && !registrarAcquired) {
|
||||
logger.atInfo().log("Registrar %s successfully authenticated", authRegistrarId);
|
||||
if (!localConnectionLimiter.acquireRegistrar(authRegistrarId)) {
|
||||
logger.atWarning().log(
|
||||
"Registrar %s exceeded concurrent connection limit, closing connection",
|
||||
authRegistrarId);
|
||||
metrics.registerQuotaRejection("epp_connection_registrar", authRegistrarId);
|
||||
closeConnection(ctx);
|
||||
return;
|
||||
}
|
||||
registrarAcquired = true;
|
||||
authenticatedRegistrarId = authRegistrarId;
|
||||
registrarId = authRegistrarId;
|
||||
|
||||
// Cancel pre-login timeout task
|
||||
if (preLoginTimeoutTask != null) {
|
||||
preLoginTimeoutTask.cancel(false);
|
||||
preLoginTimeoutTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
ByteBuf out = Unpooled.wrappedBuffer(rsp.getPayload());
|
||||
if ("close".equals(rsp.getHeader(ProxyHttpHeaders.EPP_SESSION))) {
|
||||
@SuppressWarnings("unused")
|
||||
Future<?> unusedFuture = ctx.writeAndFlush(out).addListener(ChannelFutureListener.CLOSE);
|
||||
} else {
|
||||
@SuppressWarnings("unused")
|
||||
Future<?> unusedFuture = ctx.writeAndFlush(out);
|
||||
}
|
||||
}
|
||||
|
||||
private void closeConnection(ChannelHandlerContext ctx) {
|
||||
@SuppressWarnings("unused")
|
||||
Future<?> unusedFuture = ctx.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
|
||||
if (certAcquired) {
|
||||
localConnectionLimiter.releaseCert(sslClientCertificateHash);
|
||||
if (preLoginTimeoutTask != null) {
|
||||
preLoginTimeoutTask.cancel(false);
|
||||
preLoginTimeoutTask = null;
|
||||
}
|
||||
if (registrarAcquired) {
|
||||
localConnectionLimiter.releaseRegistrar(authenticatedRegistrarId);
|
||||
}
|
||||
if (ipAcquired) {
|
||||
localConnectionLimiter.releaseIp(clientAddress);
|
||||
|
||||
@@ -22,24 +22,24 @@ import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
/**
|
||||
* Thread-safe, in-memory rate limiter for restricting the number of concurrent connections allowed
|
||||
* per IP address and per authenticated certificate.
|
||||
* per IP address and per authenticated registrar.
|
||||
*/
|
||||
@ThreadSafe
|
||||
@Singleton
|
||||
public class LocalConnectionLimiter {
|
||||
|
||||
private final int maxConnectionsPerIp;
|
||||
private final int maxConnectionsPerCert;
|
||||
private final int maxConnectionsPerRegistrar;
|
||||
|
||||
private final ConcurrentHashMap<String, Integer> ipConnections = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, Integer> certConnections = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, Integer> registrarConnections = new ConcurrentHashMap<>();
|
||||
|
||||
@Inject
|
||||
public LocalConnectionLimiter(
|
||||
@Config("eppServerMaxConnectionsPerIp") int maxConnectionsPerIp,
|
||||
@Config("eppServerMaxConnectionsPerCert") int maxConnectionsPerCert) {
|
||||
@Config("eppServerMaxConnectionsPerRegistrar") int maxConnectionsPerRegistrar) {
|
||||
this.maxConnectionsPerIp = maxConnectionsPerIp;
|
||||
this.maxConnectionsPerCert = maxConnectionsPerCert;
|
||||
this.maxConnectionsPerRegistrar = maxConnectionsPerRegistrar;
|
||||
}
|
||||
|
||||
/** Attempts to acquire a slot for the given IP address. */
|
||||
@@ -52,14 +52,14 @@ public class LocalConnectionLimiter {
|
||||
release(ipAddress, ipConnections);
|
||||
}
|
||||
|
||||
/** Attempts to acquire a slot for the given certificate hash. */
|
||||
public boolean acquireCert(String certHash) {
|
||||
return acquire(certHash, certConnections, maxConnectionsPerCert);
|
||||
/** Attempts to acquire a slot for the given registrar ID. */
|
||||
public boolean acquireRegistrar(String registrarId) {
|
||||
return acquire(registrarId, registrarConnections, maxConnectionsPerRegistrar);
|
||||
}
|
||||
|
||||
/** Releases a slot for the given certificate hash. */
|
||||
public void releaseCert(String certHash) {
|
||||
release(certHash, certConnections);
|
||||
/** Releases a slot for the given registrar ID. */
|
||||
public void releaseRegistrar(String registrarId) {
|
||||
release(registrarId, registrarConnections);
|
||||
}
|
||||
|
||||
private boolean acquire(String key, ConcurrentHashMap<String, Integer> map, int limit) {
|
||||
|
||||
@@ -75,6 +75,15 @@ public class EppRequestHandler {
|
||||
// closed by the proxy. Whether the EPP proxy actually terminates the connection with the
|
||||
// client is up to its implementation.
|
||||
// See: https://tools.ietf.org/html/rfc5734#section-2
|
||||
String authRegistrarId = null;
|
||||
try {
|
||||
authRegistrarId = sessionMetadata.getRegistrarId();
|
||||
} catch (IllegalStateException e) {
|
||||
// Session was invalidated (e.g. during logout)
|
||||
}
|
||||
if (authRegistrarId != null) {
|
||||
response.setHeader(ProxyHttpHeaders.LOGGED_IN_REGISTRAR, authRegistrarId);
|
||||
}
|
||||
if (eppOutput.isResponse()
|
||||
&& eppOutput.getResponse().getResult().getCode() == SUCCESS_AND_CLOSE) {
|
||||
response.setHeader(ProxyHttpHeaders.EPP_SESSION, "close");
|
||||
|
||||
@@ -15,20 +15,38 @@
|
||||
package google.registry.flows;
|
||||
|
||||
import static com.google.common.primitives.Longs.BYTES;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.USE_RANDOM_SERVER_TRID;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import google.registry.model.common.FeatureFlag;
|
||||
import jakarta.inject.Inject;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/** A server Trid provider that generates globally incrementing UUIDs. */
|
||||
/** A server Trid provider that generates transaction IDs. */
|
||||
public class ServerTridProviderImpl implements ServerTridProvider {
|
||||
|
||||
private static final String SERVER_ID = getServerId();
|
||||
private static final AtomicLong idCounter = new AtomicLong();
|
||||
|
||||
@Inject public ServerTridProviderImpl() {}
|
||||
@VisibleForTesting
|
||||
static final ThreadLocal<SecureRandom> secureRandom =
|
||||
ThreadLocal.withInitial(
|
||||
() -> {
|
||||
try {
|
||||
return SecureRandom.getInstance("DRBG");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
|
||||
@Inject
|
||||
public ServerTridProviderImpl() {}
|
||||
|
||||
/** Creates a unique id for this server instance, as a base64 encoded UUID. */
|
||||
private static String getServerId() {
|
||||
@@ -42,6 +60,15 @@ public class ServerTridProviderImpl implements ServerTridProvider {
|
||||
|
||||
@Override
|
||||
public String createServerTrid() {
|
||||
if (tm().reTransact(() -> FeatureFlag.isActiveNow(USE_RANDOM_SERVER_TRID))) {
|
||||
// The server TRID can be at most 64 characters. We generate 24 random bytes
|
||||
// (192 bits), which base64url-encodes without padding to 32 characters.
|
||||
// This provides an unpredictable TRID that does not leak pod identity or
|
||||
// command volume.
|
||||
byte[] randomBytes = new byte[24];
|
||||
secureRandom.get().nextBytes(randomBytes);
|
||||
return BaseEncoding.base64Url().omitPadding().encode(randomBytes);
|
||||
}
|
||||
// The server id can be at most 64 characters. The SERVER_ID is at most 22 characters (128
|
||||
// bits in base64), plus the dash. That leaves 41 characters, so we just append the counter in
|
||||
// hex.
|
||||
|
||||
@@ -25,7 +25,6 @@ import static google.registry.flows.domain.DomainFlowUtils.checkHasBillingAccoun
|
||||
import static google.registry.flows.domain.DomainFlowUtils.newAutorenewBillingEvent;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.newAutorenewPollMessage;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.validateFeeChallenge;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyNotReserved;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyPremiumNameIsNotBlocked;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyRegistrarIsActive;
|
||||
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_RESTORE;
|
||||
@@ -35,7 +34,6 @@ import static java.time.ZoneOffset.UTC;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.net.InternetDomainName;
|
||||
import google.registry.flows.EppException;
|
||||
import google.registry.flows.EppException.CommandUseErrorException;
|
||||
import google.registry.flows.EppException.StatusProhibitsOperationException;
|
||||
@@ -101,7 +99,6 @@ import org.joda.money.Money;
|
||||
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
|
||||
* @error {@link DomainFlowUtils.CurrencyUnitMismatchException}
|
||||
* @error {@link DomainFlowUtils.CurrencyValueScaleException}
|
||||
* @error {@link DomainFlowUtils.DomainReservedException}
|
||||
* @error {@link DomainFlowUtils.FeesMismatchException}
|
||||
* @error {@link DomainFlowUtils.FeesRequiredForPremiumNameException}
|
||||
* @error {@link DomainFlowUtils.MissingBillingAccountMapException}
|
||||
@@ -221,7 +218,6 @@ public final class DomainRestoreRequestFlow implements MutatingFlow {
|
||||
verifyOptionalAuthInfo(authInfo, existingDomain);
|
||||
if (!isSuperuser) {
|
||||
verifyResourceOwnership(registrarId, existingDomain);
|
||||
verifyNotReserved(InternetDomainName.from(targetId), false);
|
||||
verifyPremiumNameIsNotBlocked(targetId, now, registrarId);
|
||||
checkAllowedAccessToTld(registrarId, existingDomain.getTld());
|
||||
checkHasBillingAccount(registrarId, existingDomain.getTld());
|
||||
|
||||
@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.collect.Lists.partition;
|
||||
import static google.registry.util.ResourceUtils.readResourceUtf8;
|
||||
import static java.util.concurrent.Executors.newFixedThreadPool;
|
||||
|
||||
import com.google.cloud.tasks.v2.Task;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
@@ -39,11 +40,15 @@ import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Simple load test action that can generate configurable QPSes of various EPP actions.
|
||||
*
|
||||
* <p>This is not an end-to-end test. It exercises the Nomulus EPP service and the database, but
|
||||
* does not cover the proxy.
|
||||
*
|
||||
* <p>All aspects of the load test are configured via URL parameters that are specified when the
|
||||
* loadtest URL is being POSTed to. The {@code clientId} and {@code tld} parameters are required.
|
||||
* All of the other parameters are optional, but if none are specified then no actual load testing
|
||||
@@ -60,7 +65,7 @@ public class LoadTestAction implements Runnable {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
private static final int NUM_QUEUES = 10;
|
||||
private static final int NUM_QUEUES = 20;
|
||||
private static final int MAX_TASKS_PER_LOAD = 100;
|
||||
private static final int ARBITRARY_VALID_HOST_LENGTH = 40;
|
||||
private static final int MAX_DOMAIN_LABEL_LENGTH = 63;
|
||||
@@ -72,20 +77,17 @@ public class LoadTestAction implements Runnable {
|
||||
|
||||
public static final String PATH = "/_dr/loadtest";
|
||||
|
||||
// Average task insertion rate with a dedicated thread enqueuing to one queue. This is used to
|
||||
// calculate the EPP request dispatch time. This value is based on observation and needs not to
|
||||
// be accurate. However, it should be low enough so that all EPP tasks are enqueued before the
|
||||
// first task is dispatched.
|
||||
private static final int TASK_INSERTIONS_PER_QUEUE_PER_MINUTE = 1000;
|
||||
|
||||
/** The ID of the registrar to use for load testing. */
|
||||
@Inject
|
||||
@Parameter("loadtestClientId")
|
||||
String registrarId;
|
||||
|
||||
/**
|
||||
* The number of seconds to delay the execution of the first load testing tasks by. Preparatory
|
||||
* work of creating independent hosts that will be used for later domain creation testing occurs
|
||||
* during this period, so make sure that it is long enough.
|
||||
*/
|
||||
@Inject
|
||||
@Parameter("delaySeconds")
|
||||
int delaySeconds;
|
||||
|
||||
/**
|
||||
* The number of seconds that tasks will be enqueued for. Note that if system QPS cannot handle
|
||||
* the given load then it will take longer than this number of seconds for the test to complete.
|
||||
@@ -157,9 +159,25 @@ public class LoadTestAction implements Runnable {
|
||||
xmlHostInfo = loadXml("host_info").replace("%host%", EXISTING_HOST);
|
||||
}
|
||||
|
||||
private int eppTaskCount() {
|
||||
// See `run()` below for details: summing two task-generating loops.
|
||||
return successfulDomainCreatesPerSecond
|
||||
+ runSeconds
|
||||
* (successfulHostCreatesPerSecond
|
||||
+ failedHostCreatesPerSecond
|
||||
+ domainInfosPerSecond
|
||||
+ domainChecksPerSecond
|
||||
+ hostInfosPerSecond
|
||||
+ successfulDomainCreatesPerSecond
|
||||
+ failedDomainCreatesPerSecond);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
validateAndLogRequest();
|
||||
// Delay the EPP request dispatch time to account for queue-insertion time.
|
||||
int delaySeconds =
|
||||
Math.ceilDiv(eppTaskCount(), TASK_INSERTIONS_PER_QUEUE_PER_MINUTE * NUM_QUEUES) * 60;
|
||||
validateAndLogRequest(delaySeconds);
|
||||
Instant initialStartSecond = clock.now().plus(Duration.ofSeconds(delaySeconds));
|
||||
ImmutableList.Builder<String> preTaskXmls = new ImmutableList.Builder<>();
|
||||
ImmutableList.Builder<String> hostPrefixesBuilder = new ImmutableList.Builder<>();
|
||||
@@ -209,7 +227,7 @@ public class LoadTestAction implements Runnable {
|
||||
logger.atInfo().log("Added %d total load test tasks.", taskOptions.size());
|
||||
}
|
||||
|
||||
private void validateAndLogRequest() {
|
||||
private void validateAndLogRequest(int delaySeconds) {
|
||||
checkArgument(
|
||||
RegistryEnvironment.get() != RegistryEnvironment.PRODUCTION,
|
||||
"DO NOT RUN LOADTESTS IN PROD!");
|
||||
@@ -297,9 +315,21 @@ public class LoadTestAction implements Runnable {
|
||||
|
||||
private void enqueue(ImmutableList<Task> tasks) {
|
||||
List<List<Task>> chunks = partition(tasks, MAX_TASKS_PER_LOAD);
|
||||
// Farm out tasks to multiple queues to work around queue qps quotas.
|
||||
for (int i = 0; i < chunks.size(); i++) {
|
||||
cloudTasksUtils.enqueue("load" + (i % NUM_QUEUES), chunks.get(i));
|
||||
// Farm out tasks to multiple queues to work around queue qps quotas. Use multiple threads to
|
||||
// speed up the enqueuing.
|
||||
try (ExecutorService executorService = newFixedThreadPool(NUM_QUEUES)) {
|
||||
for (int i = 0; i < chunks.size(); i++) {
|
||||
final int index = i;
|
||||
// Ignore `Future` returned by the pool b/c individual failures do not affect analysis.
|
||||
// lgtm[java/local-variable-is-never-read] Suppress Github CodeQL's outdated warning
|
||||
var _ =
|
||||
executorService.submit(
|
||||
() -> cloudTasksUtils.enqueue(getQueueName(index % NUM_QUEUES), chunks.get(index)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String getQueueName(int queueId) {
|
||||
return String.format("load%d", queueId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,12 +38,6 @@ public final class LoadTestModule {
|
||||
return extractRequiredParameter(req, "clientId");
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Parameter("delaySeconds")
|
||||
static int provideDelaySeconds(HttpServletRequest req) {
|
||||
return extractOptionalIntParameter(req, "delaySeconds").orElse(60);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Parameter("runSeconds")
|
||||
static int provideRunSeconds(HttpServletRequest req) {
|
||||
@@ -74,24 +68,6 @@ public final class LoadTestModule {
|
||||
return extractOptionalIntParameter(req, "domainChecks").orElse(0);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Parameter("successfulContactCreates")
|
||||
static int provideSuccessfulContactCreates(HttpServletRequest req) {
|
||||
return extractOptionalIntParameter(req, "successfulContactCreates").orElse(0);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Parameter("failedContactCreates")
|
||||
static int provideFailedContactCreates(HttpServletRequest req) {
|
||||
return extractOptionalIntParameter(req, "failedContactCreates").orElse(0);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Parameter("contactInfos")
|
||||
static int provideContactInfos(HttpServletRequest req) {
|
||||
return extractOptionalIntParameter(req, "contactInfos").orElse(0);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Parameter("successfulHostCreates")
|
||||
static int provideSuccessfulHostCreates(HttpServletRequest req) {
|
||||
|
||||
@@ -87,7 +87,10 @@ public class FeatureFlag extends ImmutableObject implements Buildable {
|
||||
PROHIBIT_CONTACT_OBJECTS_ON_LOGIN(FeatureStatus.INACTIVE),
|
||||
|
||||
/** If we're prohibiting insecure algorithms as detailed by RFC 9904. */
|
||||
FORBID_INSECURE_ALGORITHMS_RFC_9904(FeatureStatus.INACTIVE);
|
||||
FORBID_INSECURE_ALGORITHMS_RFC_9904(FeatureStatus.INACTIVE),
|
||||
|
||||
/** If we're using secure random base64 encoded server TRIDs. */
|
||||
USE_RANDOM_SERVER_TRID(FeatureStatus.INACTIVE);
|
||||
|
||||
private final FeatureStatus defaultStatus;
|
||||
|
||||
|
||||
@@ -59,10 +59,10 @@ public final class PremiumList extends BaseDomainLabelList<BigDecimal, PremiumEn
|
||||
* Mapping from unqualified domain names to their prices.
|
||||
*
|
||||
* <p>This field requires special treatment since we want to lazy load it. We have to remove it
|
||||
* from the immutability contract so we can modify it after construction and we have to handle the
|
||||
* database processing on our own so we can detach it after load.
|
||||
* from the immutability contract so we can modify it after construction, and we have to handle
|
||||
* the database processing on our own so we can detach it after load.
|
||||
*/
|
||||
@Insignificant @Transient ImmutableMap<String, BigDecimal> labelsToPrices;
|
||||
@Insignificant @Transient volatile ImmutableMap<String, BigDecimal> labelsToPrices;
|
||||
|
||||
@Column(nullable = false)
|
||||
BloomFilter<String> bloomFilter;
|
||||
@@ -76,18 +76,27 @@ public final class PremiumList extends BaseDomainLabelList<BigDecimal, PremiumEn
|
||||
* Returns a {@link Map} of domain labels to prices.
|
||||
*
|
||||
* <p>Note that this is lazily loaded and thus must be called inside a transaction. You generally
|
||||
* should not be using this anyway as it's inefficient to load all of the PremiumEntry rows if you
|
||||
* should not be using this anyway as it's inefficient to load all the PremiumEntry rows if you
|
||||
* don't need them. To check prices, use {@link PremiumListDao#getPremiumPrice} instead.
|
||||
*
|
||||
* <p>We use locking to memoize the resulting object. We cannot use a simple memoizing Supplier
|
||||
* because we need to be able to set this value when creating the lists.
|
||||
*/
|
||||
public synchronized ImmutableMap<String, BigDecimal> getLabelsToPrices() {
|
||||
public ImmutableMap<String, BigDecimal> getLabelsToPrices() {
|
||||
if (labelsToPrices == null) {
|
||||
labelsToPrices =
|
||||
PremiumListDao.loadAllPremiumEntries(name).stream()
|
||||
.collect(
|
||||
toImmutableMap(
|
||||
PremiumEntry::getDomainLabel,
|
||||
// Set the correct amount of precision for the premium list's currency.
|
||||
premiumEntry -> convertAmountToMoney(premiumEntry.getValue()).getAmount()));
|
||||
synchronized (this) {
|
||||
// Extra null check to avoid race conditions
|
||||
if (labelsToPrices == null) {
|
||||
labelsToPrices =
|
||||
PremiumListDao.loadAllPremiumEntries(name).stream()
|
||||
.collect(
|
||||
toImmutableMap(
|
||||
PremiumEntry::getDomainLabel,
|
||||
// Set the correct amount of precision for the list's currency.
|
||||
premiumEntry ->
|
||||
convertAmountToMoney(premiumEntry.getValue()).getAmount()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return labelsToPrices;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import static google.registry.config.RegistryConfig.getDomainLabelListCacheDurat
|
||||
import static google.registry.model.tld.label.ReservationType.FULLY_BLOCKED;
|
||||
import static google.registry.persistence.transaction.QueryComposer.Comparator.EQ;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.util.CollectionUtils.nullToEmpty;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.LoadingCache;
|
||||
import com.google.common.base.Splitter;
|
||||
@@ -69,7 +68,7 @@ public final class ReservedList
|
||||
* from the immutability contract so we can modify it after construction and we have to handle the
|
||||
* database processing on our own so we can detach it after load.
|
||||
*/
|
||||
@Insignificant @Transient Map<String, ReservedListEntry> reservedListMap;
|
||||
@Insignificant @Transient volatile ImmutableMap<String, ReservedListEntry> reservedListMap;
|
||||
|
||||
@RecursivePreRemove
|
||||
void preRemove() {
|
||||
@@ -149,7 +148,7 @@ public final class ReservedList
|
||||
}
|
||||
|
||||
/** A builder for constructing {@link ReservedListEntry} objects, since they are immutable. */
|
||||
private static class Builder
|
||||
public static class Builder
|
||||
extends DomainLabelEntry.Builder<ReservedListEntry, ReservedListEntry.Builder> {
|
||||
|
||||
Builder() {}
|
||||
@@ -185,19 +184,27 @@ public final class ReservedList
|
||||
*
|
||||
* <p>Note that this involves a database fetch of a potentially large number of elements and
|
||||
* should be avoided unless necessary.
|
||||
*
|
||||
* <p>We use locking to memoize the resulting object. We cannot use a simple memoizing Supplier
|
||||
* because we need to be able to set this value when creating the lists.
|
||||
*/
|
||||
public synchronized ImmutableMap<String, ReservedListEntry> getReservedListEntries() {
|
||||
public ImmutableMap<String, ReservedListEntry> getReservedListEntries() {
|
||||
if (reservedListMap == null) {
|
||||
reservedListMap =
|
||||
tm().reTransact(
|
||||
() ->
|
||||
tm()
|
||||
.createQueryComposer(ReservedListEntry.class)
|
||||
.where("revisionId", EQ, revisionId)
|
||||
.stream()
|
||||
.collect(toImmutableMap(ReservedListEntry::getDomainLabel, e -> e)));
|
||||
synchronized (this) {
|
||||
// Extra null check to avoid race conditions
|
||||
if (reservedListMap == null) {
|
||||
reservedListMap =
|
||||
tm().reTransact(
|
||||
() ->
|
||||
tm()
|
||||
.createQueryComposer(ReservedListEntry.class)
|
||||
.where("revisionId", EQ, revisionId)
|
||||
.stream()
|
||||
.collect(toImmutableMap(ReservedListEntry::getDomainLabel, e -> e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return ImmutableMap.copyOf(nullToEmpty(reservedListMap));
|
||||
return reservedListMap;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -220,7 +227,7 @@ public final class ReservedList
|
||||
*/
|
||||
public static ImmutableSet<ReservationType> getReservationTypes(String label, String tld) {
|
||||
checkNotNull(label, "label");
|
||||
if (label.length() == 0) {
|
||||
if (label.isEmpty()) {
|
||||
return ImmutableSet.of(FULLY_BLOCKED);
|
||||
}
|
||||
return getReservedListEntries(label, tld).stream()
|
||||
|
||||
@@ -271,7 +271,7 @@ public interface TransactionManager {
|
||||
* 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.
|
||||
* checked exceptions as unchecked ones.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
interface ThrowingRunnable {
|
||||
|
||||
@@ -64,7 +64,7 @@ public final class NordnVerifyAction implements Runnable {
|
||||
static final String NORDN_URL_PARAM = "nordnUrl";
|
||||
static final String NORDN_LOG_ID_PARAM = "nordnLogId";
|
||||
|
||||
private static final String MARKSDB_URL_BEGINNING = "ry.marksdb.org";
|
||||
private static final String MARKSDB_HOST_NAME = "ry.marksdb.org";
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
@@ -109,11 +109,7 @@ public final class NordnVerifyAction implements Runnable {
|
||||
@VisibleForTesting
|
||||
LordnLog verify() throws IOException, GeneralSecurityException {
|
||||
String host = Ascii.toLowerCase(url.getHost());
|
||||
checkArgument(
|
||||
host.startsWith(MARKSDB_URL_BEGINNING),
|
||||
"URL %s must start with %s",
|
||||
url,
|
||||
MARKSDB_URL_BEGINNING);
|
||||
checkArgument(host.equals(MARKSDB_HOST_NAME), "Host %s must equal %s", host, MARKSDB_HOST_NAME);
|
||||
logger.atInfo().log("LORDN verify task %s: Sending request to URL %s", actionLogId, url);
|
||||
HttpURLConnection connection = urlConnectionService.createConnection(url);
|
||||
lordnRequestInitializer.initialize(connection, tld);
|
||||
|
||||
@@ -127,9 +127,7 @@ public final class TmchCertificateAuthority {
|
||||
* @see X509Utils#verifyCertificate
|
||||
*/
|
||||
public void verify(X509Certificate cert) throws GeneralSecurityException {
|
||||
synchronized (TmchCertificateAuthority.class) {
|
||||
X509Utils.verifyCertificate(getAndValidateRoot(), getCrl(), cert, clock.now());
|
||||
}
|
||||
X509Utils.verifyCertificate(getAndValidateRoot(), getCrl(), cert, clock.now());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,33 +155,23 @@ public final class TmchCertificateAuthority {
|
||||
}
|
||||
|
||||
public X509Certificate getAndValidateRoot() throws GeneralSecurityException {
|
||||
try {
|
||||
X509Certificate root = ROOT_CERTS.get(tmchCaMode);
|
||||
// The current production certificate expires on 2023-07-23. Future code monkey be reminded,
|
||||
// if you are looking at this code because the next line throws an exception, ask ICANN for a
|
||||
// new root certificate! (preferably before the current one expires...)
|
||||
root.checkValidity(Date.from(clock.now()));
|
||||
return root;
|
||||
} catch (Exception e) {
|
||||
if (e instanceof GeneralSecurityException generalSecurityException) {
|
||||
throw generalSecurityException;
|
||||
} else if (e instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
X509Certificate root = ROOT_CERTS.get(tmchCaMode);
|
||||
// The current production certificate expires on 2042-11-15. Future code monkey be reminded,
|
||||
// if you are looking at this code because the next line throws an exception, ask ICANN for a
|
||||
// new root certificate! (preferably before the current one expires...)
|
||||
root.checkValidity(Date.from(clock.now()));
|
||||
return root;
|
||||
}
|
||||
|
||||
public X509CRL getCrl() throws GeneralSecurityException {
|
||||
try {
|
||||
return CRL_CACHE.get(tmchCaMode);
|
||||
} catch (Exception e) {
|
||||
} catch (RuntimeException e) {
|
||||
if (e.getCause() instanceof GeneralSecurityException generalSecurityException) {
|
||||
throw generalSecurityException;
|
||||
} else if (e instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import google.registry.util.DomainNameUtils;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* A command to bulk-transfer any number of domains from one registrar to another.
|
||||
@@ -76,8 +77,7 @@ public class BulkDomainTransferCommand extends ConfirmingCommand implements Comm
|
||||
|
||||
@Parameter(
|
||||
names = {"-l", "--losing_registrar_id"},
|
||||
description = "The ID of the registrar from which domains should be transferred",
|
||||
required = true)
|
||||
description = "The ID of the registrar from which domains should be transferred")
|
||||
private String losingRegistrarId;
|
||||
|
||||
@Parameter(
|
||||
@@ -119,14 +119,17 @@ public class BulkDomainTransferCommand extends ConfirmingCommand implements Comm
|
||||
Registrar.loadByRegistrarIdCached(gainingRegistrarId).isPresent(),
|
||||
"Gaining registrar %s doesn't exist",
|
||||
gainingRegistrarId);
|
||||
checkArgument(
|
||||
Registrar.loadByRegistrarIdCached(losingRegistrarId).isPresent(),
|
||||
"Losing registrar %s doesn't exist",
|
||||
losingRegistrarId);
|
||||
if (losingRegistrarId != null) {
|
||||
checkArgument(
|
||||
Registrar.loadByRegistrarIdCached(losingRegistrarId).isPresent(),
|
||||
"Losing registrar %s doesn't exist",
|
||||
losingRegistrarId);
|
||||
}
|
||||
|
||||
ImmutableMap.Builder<String, Object> paramsBuilder = new ImmutableMap.Builder<>();
|
||||
paramsBuilder.put("gainingRegistrarId", gainingRegistrarId);
|
||||
paramsBuilder.put("losingRegistrarId", losingRegistrarId);
|
||||
Optional.ofNullable(losingRegistrarId)
|
||||
.ifPresent(id -> paramsBuilder.put("losingRegistrarId", id));
|
||||
paramsBuilder.put("requestedByRegistrar", requestedByRegistrar);
|
||||
paramsBuilder.put("reason", reason);
|
||||
if (maxQps > 0) {
|
||||
|
||||
@@ -138,7 +138,7 @@ abstract class CreateOrUpdateRegistrarCommand extends MutatingCommand {
|
||||
description =
|
||||
"Used together with --cert_file when updating an registrar. "
|
||||
+ "If set, current cert is saved as failover.")
|
||||
private Boolean rotatePrimaryCert = Boolean.FALSE;
|
||||
private boolean rotatePrimaryCert = false;
|
||||
|
||||
@Nullable
|
||||
@Parameter(
|
||||
|
||||
@@ -113,11 +113,10 @@ public abstract class ConsoleApiAction implements Runnable {
|
||||
() ->
|
||||
new ConsolePermissionForbiddenException(
|
||||
String.format("Registrar %s does not exist", registrarId)));
|
||||
if (!registrar.isLive()) {
|
||||
if (registrar.getState().equals(Registrar.State.DISABLED)) {
|
||||
throw new ConsolePermissionForbiddenException(
|
||||
String.format(
|
||||
"Permission forbidden because registrar %s is currently %s",
|
||||
registrarId, registrar.getState()));
|
||||
"Permission forbidden because registrar %s is currently DISABLED", registrarId));
|
||||
}
|
||||
if (!user.getUserRoles().hasPermission(registrarId, permission)) {
|
||||
throw new ConsolePermissionForbiddenException(
|
||||
|
||||
@@ -59,28 +59,31 @@ public class ConsoleOteAction extends ConsoleApiAction {
|
||||
private static final String STAT_TYPE_DESCRIPTION_PARAM = "description";
|
||||
private static final String STAT_TYPE_REQUIREMENT_PARAM = "requirement";
|
||||
private static final String STAT_TYPE_TIMES_PERFORMED_PARAM = "timesPerformed";
|
||||
private final IamClient iamClient;
|
||||
private final StringGenerator passwordGenerator;
|
||||
private final Optional<OteCreateData> oteCreateData;
|
||||
private final Optional<String> maybeGroupEmailAddress;
|
||||
private final Optional<String> consoleIapServiceId;
|
||||
private final IamClient iamClient;
|
||||
private final String gSuiteDomainName;
|
||||
private final String registrarId;
|
||||
|
||||
@Inject
|
||||
public ConsoleOteAction(
|
||||
ConsoleApiParams consoleApiParams,
|
||||
IamClient iamClient,
|
||||
@Parameter("registrarId") String registrarId, // Get request param
|
||||
@Named("base58StringGenerator") StringGenerator passwordGenerator,
|
||||
@Parameter("oteCreateData") Optional<OteCreateData> oteCreateData,
|
||||
@Config("gSuiteConsoleUserGroupEmailAddress") Optional<String> maybeGroupEmailAddress,
|
||||
@Config("consoleIapServiceId") Optional<String> consoleIapServiceId,
|
||||
@Named("base58StringGenerator") StringGenerator passwordGenerator,
|
||||
@Parameter("oteCreateData") Optional<OteCreateData> oteCreateData) {
|
||||
@Config("gSuiteDomainName") String gSuiteDomainName,
|
||||
@Parameter("registrarId") String registrarId) {
|
||||
super(consoleApiParams);
|
||||
this.iamClient = iamClient;
|
||||
this.passwordGenerator = passwordGenerator;
|
||||
this.oteCreateData = oteCreateData;
|
||||
this.maybeGroupEmailAddress = maybeGroupEmailAddress;
|
||||
this.consoleIapServiceId = consoleIapServiceId;
|
||||
this.iamClient = iamClient;
|
||||
this.gSuiteDomainName = gSuiteDomainName;
|
||||
this.registrarId = registrarId;
|
||||
}
|
||||
|
||||
@@ -97,8 +100,11 @@ public class ConsoleOteAction extends ConsoleApiAction {
|
||||
this.oteCreateData.isPresent()
|
||||
&& !this.oteCreateData.get().registrarId.isEmpty()
|
||||
&& !this.oteCreateData.get().registrarEmail.isEmpty();
|
||||
|
||||
checkArgument(isBodyValid, "OT&E create body is invalid");
|
||||
checkArgument(
|
||||
this.oteCreateData.get().registrarEmail.endsWith("@" + gSuiteDomainName),
|
||||
"Email address must exist in the %s domain",
|
||||
gSuiteDomainName);
|
||||
|
||||
String password = passwordGenerator.createString(PASSWORD_LENGTH);
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
package google.registry.ui.server.console;
|
||||
|
||||
import static com.google.common.base.Strings.isNullOrEmpty;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static google.registry.model.console.RegistrarRole.ACCOUNT_MANAGER;
|
||||
import static google.registry.model.console.RegistrarRole.TECH_CONTACT;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
@@ -357,12 +356,17 @@ public class ConsoleUsersAction extends ConsoleApiAction {
|
||||
return updatedUser;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private ImmutableList<User> getAllRegistrarUsers(String registrarId) {
|
||||
return tm().transact(
|
||||
() ->
|
||||
tm().loadAllOf(User.class).stream()
|
||||
.filter(u -> u.getUserRoles().getRegistrarRoles().containsKey(registrarId))
|
||||
.collect(toImmutableList()));
|
||||
ImmutableList.copyOf(
|
||||
tm().getEntityManager()
|
||||
.createNativeQuery(
|
||||
"SELECT * FROM \"User\" WHERE exist(registrar_roles, :registrarId)",
|
||||
User.class)
|
||||
.setParameter("registrarId", registrarId)
|
||||
.getResultList()));
|
||||
}
|
||||
|
||||
/** Maps a request role string to a RegistrarRole, using ACCOUNT_MANAGER as the default. */
|
||||
|
||||
@@ -39,6 +39,7 @@ import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.FakeLockHandler;
|
||||
import google.registry.testing.FakeResponse;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
@@ -127,7 +128,22 @@ public class BulkDomainTransferActionTest {
|
||||
assertThat(deletedDomain.getUpdateTimestamp().getTimestamp()).isEqualTo(preRunTime);
|
||||
}
|
||||
|
||||
private BulkDomainTransferAction createAction(String... domains) {
|
||||
@Test
|
||||
void testSuccess_withoutLosingRegistrarId() {
|
||||
BulkDomainTransferAction action =
|
||||
createActionWithOptionalLosingRegistrar(
|
||||
Optional.empty(), "active.tld", "alreadytransferred.tld");
|
||||
fakeClock.advanceOneMilli();
|
||||
Instant now = fakeClock.now();
|
||||
action.run();
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
activeDomain = loadByEntity(activeDomain);
|
||||
assertThat(activeDomain.cloneProjectedAtTime(now).getCurrentSponsorRegistrarId())
|
||||
.isEqualTo("NewRegistrar");
|
||||
}
|
||||
|
||||
private BulkDomainTransferAction createActionWithOptionalLosingRegistrar(
|
||||
Optional<String> losingRegistrarId, String... domains) {
|
||||
EppController eppController =
|
||||
DaggerEppTestComponent.builder()
|
||||
.fakesAndMocksModule(FakesAndMocksModule.create(new FakeClock()))
|
||||
@@ -140,9 +156,13 @@ public class BulkDomainTransferActionTest {
|
||||
rateLimiter,
|
||||
ImmutableList.copyOf(domains),
|
||||
"NewRegistrar",
|
||||
"TheRegistrar",
|
||||
losingRegistrarId,
|
||||
true,
|
||||
"reason",
|
||||
response);
|
||||
}
|
||||
|
||||
private BulkDomainTransferAction createAction(String... domains) {
|
||||
return createActionWithOptionalLosingRegistrar(Optional.of("TheRegistrar"), domains);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
package google.registry.batch;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.monitoring.metrics.contrib.LongMetricSubject.assertThat;
|
||||
import static google.registry.batch.SyncRemoteCacheAction.SyncStatus.FAILURE;
|
||||
import static google.registry.batch.SyncRemoteCacheAction.SyncStatus.NOT_CONFIGURED;
|
||||
import static google.registry.batch.SyncRemoteCacheAction.SyncStatus.SUCCESS;
|
||||
import static google.registry.model.common.Cursor.CursorType.REMOTE_CACHE_DOMAIN_SYNC;
|
||||
import static google.registry.model.common.Cursor.CursorType.REMOTE_CACHE_HOST_SYNC;
|
||||
import static google.registry.testing.DatabaseHelper.createTld;
|
||||
@@ -73,15 +77,24 @@ class SyncRemoteCacheActionTest {
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
createTld("tld");
|
||||
SyncRemoteCacheAction.SYNC_CACHE_RUNS_METRIC.reset();
|
||||
action = new SyncRemoteCacheAction(lockHandler, response, Optional.of(jedisClient));
|
||||
}
|
||||
|
||||
private static void verifyMetrics(SyncRemoteCacheAction.SyncStatus status) {
|
||||
assertThat(SyncRemoteCacheAction.SYNC_CACHE_RUNS_METRIC)
|
||||
.hasValueForLabels(1, status.name())
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_noJedisConfig() {
|
||||
action = new SyncRemoteCacheAction(lockHandler, response, Optional.empty());
|
||||
action.run();
|
||||
assertThat(response.getStatus()).isEqualTo(SC_NO_CONTENT);
|
||||
assertThat(response.getPayload()).contains("No Jedis/Valkey configuration found");
|
||||
verifyMetrics(NOT_CONFIGURED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -91,6 +104,7 @@ class SyncRemoteCacheActionTest {
|
||||
action.run();
|
||||
assertThat(response.getStatus()).isEqualTo(SC_NO_CONTENT);
|
||||
assertThat(response.getPayload()).contains("Could not acquire lock");
|
||||
verifyMetrics(FAILURE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -100,6 +114,7 @@ class SyncRemoteCacheActionTest {
|
||||
action.run();
|
||||
assertThat(response.getStatus()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
|
||||
assertThat(response.getPayload()).contains("Errored out with cause");
|
||||
verifyMetrics(FAILURE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -109,6 +124,7 @@ class SyncRemoteCacheActionTest {
|
||||
verifyNoInteractions(jedisClient);
|
||||
assertThat(DatabaseHelper.loadByKeyIfPresent(Cursor.createGlobalVKey(REMOTE_CACHE_DOMAIN_SYNC)))
|
||||
.isEmpty();
|
||||
verifyMetrics(SUCCESS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -131,6 +147,7 @@ class SyncRemoteCacheActionTest {
|
||||
.getCursorTime()
|
||||
.toString())
|
||||
.isEqualTo("2025-01-01T00:00:00.001Z");
|
||||
verifyMetrics(SUCCESS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -146,6 +163,7 @@ class SyncRemoteCacheActionTest {
|
||||
ImmutableList.of(
|
||||
new SimplifiedJedisClient.JedisResource<>("active.tld", activeDomain)));
|
||||
verify(jedisClient).deleteAll(Domain.class, ImmutableList.of("deleted.tld"));
|
||||
verifyMetrics(SUCCESS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -166,6 +184,7 @@ class SyncRemoteCacheActionTest {
|
||||
verify(jedisClient)
|
||||
.setAll(
|
||||
ImmutableList.of(new SimplifiedJedisClient.JedisResource<>("example2.tld", domain2)));
|
||||
verifyMetrics(SUCCESS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -175,6 +194,7 @@ class SyncRemoteCacheActionTest {
|
||||
verifyNoInteractions(jedisClient);
|
||||
assertThat(DatabaseHelper.loadByKeyIfPresent(Cursor.createGlobalVKey(REMOTE_CACHE_HOST_SYNC)))
|
||||
.isEmpty();
|
||||
verifyMetrics(SUCCESS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -197,6 +217,7 @@ class SyncRemoteCacheActionTest {
|
||||
.getCursorTime()
|
||||
.toString())
|
||||
.isEqualTo("2025-01-01T00:00:00.001Z");
|
||||
verifyMetrics(SUCCESS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -212,5 +233,6 @@ class SyncRemoteCacheActionTest {
|
||||
ImmutableList.of(
|
||||
new SimplifiedJedisClient.JedisResource<>(active.getRepoId(), active)));
|
||||
verify(jedisClient).deleteAll(Host.class, ImmutableList.of(deleted.getRepoId()));
|
||||
verifyMetrics(SUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import static jakarta.servlet.http.HttpServletResponse.SC_NO_CONTENT;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Iterables;
|
||||
import google.registry.model.eppcommon.StatusValue;
|
||||
import google.registry.model.host.Host;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
@@ -52,7 +53,7 @@ public class RefreshDnsOnHostRenameActionTest {
|
||||
private RefreshDnsOnHostRenameAction action;
|
||||
|
||||
private void createAction(String hostKey) {
|
||||
action = new RefreshDnsOnHostRenameAction(hostKey, response);
|
||||
action = new RefreshDnsOnHostRenameAction(hostKey, response, clock);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
@@ -99,4 +100,28 @@ public class RefreshDnsOnHostRenameActionTest {
|
||||
assertThat(response.getPayload())
|
||||
.isEqualTo("Host to refresh is already deleted: ns1.example.tld");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_multipleBatches() {
|
||||
Host host = persistActiveHost("ns1.example.tld");
|
||||
ImmutableSet.Builder<String> domainNamesBuilder = new ImmutableSet.Builder<>();
|
||||
for (int i = 1; i <= 1001; i++) {
|
||||
String domainName = "example" + i + ".tld";
|
||||
domainNamesBuilder.add(domainName);
|
||||
persistResource(newDomain(domainName, host));
|
||||
}
|
||||
createAction(host.createVKey().stringify());
|
||||
action.run();
|
||||
assertDomainDnsRequests(Iterables.toArray(domainNamesBuilder.build(), String.class));
|
||||
assertThat(response.getStatus()).isEqualTo(SC_OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_noLinkedDomains() {
|
||||
Host host = persistActiveHost("ns1.example.tld");
|
||||
createAction(host.createVKey().stringify());
|
||||
action.run();
|
||||
assertNoDnsRequests();
|
||||
assertThat(response.getStatus()).isEqualTo(SC_OK);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
// Copyright 2024 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.eppserver.handler;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.embedded.EmbeddedChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class EppProxyProtocolHandlerTest {
|
||||
|
||||
@Test
|
||||
void testProxyProtocol_parsesValidHeader() {
|
||||
EppProxyProtocolHandler handler = new EppProxyProtocolHandler();
|
||||
EmbeddedChannel channel = new EmbeddedChannel(handler);
|
||||
|
||||
String proxyHeader = "PROXY TCP4 192.168.1.1 10.0.0.1 50000 443\r\n";
|
||||
ByteBuf buffer = Unpooled.wrappedBuffer(proxyHeader.getBytes(StandardCharsets.US_ASCII));
|
||||
|
||||
channel.writeInbound(buffer);
|
||||
|
||||
String remoteAddress = channel.attr(EppProxyProtocolHandler.REMOTE_ADDRESS_KEY).get();
|
||||
assertThat(remoteAddress).isEqualTo("192.168.1.1");
|
||||
assertThat(channel.pipeline().get(EppProxyProtocolHandler.class)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProxyProtocol_unknownHeader() {
|
||||
EppProxyProtocolHandler handler = new EppProxyProtocolHandler();
|
||||
EmbeddedChannel channel = new EmbeddedChannel(handler);
|
||||
|
||||
String proxyHeader = "PROXY UNKNOWN\r\n";
|
||||
ByteBuf buffer = Unpooled.wrappedBuffer(proxyHeader.getBytes(StandardCharsets.US_ASCII));
|
||||
|
||||
channel.writeInbound(buffer);
|
||||
|
||||
String remoteAddress = channel.attr(EppProxyProtocolHandler.REMOTE_ADDRESS_KEY).get();
|
||||
assertThat(remoteAddress).isEqualTo("0.0.0.0");
|
||||
assertThat(channel.pipeline().get(EppProxyProtocolHandler.class)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProxyProtocol_noHeader_notProxied() {
|
||||
EppProxyProtocolHandler handler = new EppProxyProtocolHandler();
|
||||
EmbeddedChannel channel = new EmbeddedChannel(handler);
|
||||
|
||||
String normalData = "NOT_A_PROXY_HEADER";
|
||||
ByteBuf buffer = Unpooled.wrappedBuffer(normalData.getBytes(StandardCharsets.US_ASCII));
|
||||
|
||||
channel.writeInbound(buffer);
|
||||
|
||||
String remoteAddress = channel.attr(EppProxyProtocolHandler.REMOTE_ADDRESS_KEY).get();
|
||||
// In EmbeddedChannel without remoteAddress mock, getSourceIP returns null
|
||||
assertThat(remoteAddress).isNull();
|
||||
assertThat(channel.pipeline().get(EppProxyProtocolHandler.class)).isNull();
|
||||
|
||||
ByteBuf passedOn = channel.readInbound();
|
||||
assertThat(passedOn.toString(StandardCharsets.US_ASCII)).isEqualTo("NOT_A_PROXY_HEADER");
|
||||
}
|
||||
}
|
||||
@@ -14,14 +14,18 @@
|
||||
|
||||
package google.registry.eppserver.handler;
|
||||
|
||||
import static google.registry.eppserver.handler.EppProxyProtocolHandler.REMOTE_ADDRESS_KEY;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.eppserver.handler.EppServiceHandler.REMOTE_ADDRESS_KEY;
|
||||
import static google.registry.networking.handler.SslServerInitializer.CLIENT_CERTIFICATE_PROMISE_KEY;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -34,6 +38,7 @@ import google.registry.request.RequestHandler;
|
||||
import google.registry.util.FakeHttpServletRequest;
|
||||
import google.registry.util.FakeHttpServletResponse;
|
||||
import google.registry.util.ProxyHttpHeaders;
|
||||
import google.registry.util.X509Utils;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.Channel;
|
||||
@@ -42,13 +47,19 @@ import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.util.Attribute;
|
||||
import io.netty.util.concurrent.DefaultPromise;
|
||||
import io.netty.util.concurrent.EventExecutor;
|
||||
import io.netty.util.concurrent.ImmediateEventExecutor;
|
||||
import io.netty.util.concurrent.Promise;
|
||||
import io.netty.util.concurrent.ScheduledFuture;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
@@ -61,6 +72,8 @@ class EppServiceHandlerTest {
|
||||
@Mock private Supplier<String> idTokenSupplier;
|
||||
@Mock private ChannelHandlerContext ctx;
|
||||
@Mock private Channel channel;
|
||||
@Mock private EventExecutor executor;
|
||||
@Mock private ScheduledFuture<?> scheduledFuture;
|
||||
@Mock private RequestHandler<?> requestHandler;
|
||||
|
||||
@Mock private Attribute<Promise<X509Certificate>> certPromiseAttr;
|
||||
@@ -80,12 +93,32 @@ class EppServiceHandlerTest {
|
||||
localConnectionLimiter,
|
||||
commandQuotaManager,
|
||||
idTokenSupplier,
|
||||
"test-project");
|
||||
"test-project",
|
||||
10); // preLoginReadTimeoutSeconds
|
||||
|
||||
handler.requestHandler = requestHandler;
|
||||
|
||||
when(ctx.channel()).thenReturn(channel);
|
||||
when(ctx.executor()).thenReturn(ImmediateEventExecutor.INSTANCE);
|
||||
when(ctx.executor()).thenReturn(executor);
|
||||
lenient().when(channel.isActive()).thenReturn(true);
|
||||
|
||||
doAnswer(
|
||||
invocation -> {
|
||||
Runnable runnable = invocation.getArgument(0);
|
||||
runnable.run();
|
||||
return null;
|
||||
})
|
||||
.when(executor)
|
||||
.execute(any(Runnable.class));
|
||||
|
||||
lenient()
|
||||
.doReturn(scheduledFuture)
|
||||
.when(executor)
|
||||
.schedule(any(Runnable.class), anyLong(), any(TimeUnit.class));
|
||||
|
||||
lenient()
|
||||
.when(commandQuotaManager.acquireQuota(any(QuotaRequest.class)))
|
||||
.thenReturn(new QuotaResponse(true));
|
||||
}
|
||||
|
||||
private void setUpSuccessfulHandshake() throws Exception {
|
||||
@@ -101,7 +134,6 @@ class EppServiceHandlerTest {
|
||||
when(certificate.getEncoded()).thenReturn(new byte[] {1, 2, 3});
|
||||
|
||||
when(localConnectionLimiter.acquireIp(any(String.class))).thenReturn(true);
|
||||
when(localConnectionLimiter.acquireCert(any(String.class))).thenReturn(true);
|
||||
|
||||
certPromise.setSuccess(certificate);
|
||||
}
|
||||
@@ -156,24 +188,31 @@ class EppServiceHandlerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testChannelActive_certQuotaRejected() throws Exception {
|
||||
certPromise = new DefaultPromise<>(ImmediateEventExecutor.INSTANCE);
|
||||
when(channel.attr(CLIENT_CERTIFICATE_PROMISE_KEY)).thenReturn(certPromiseAttr);
|
||||
when(certPromiseAttr.get()).thenReturn(certPromise);
|
||||
void testChannelRead0_registrarQuotaRejected() throws Exception {
|
||||
setUpSuccessfulHandshake();
|
||||
|
||||
handler.channelActive(ctx);
|
||||
when(idTokenSupplier.get()).thenReturn("fake_id_token");
|
||||
when(commandQuotaManager.acquireQuota(any(QuotaRequest.class)))
|
||||
.thenReturn(new QuotaResponse(true));
|
||||
|
||||
when(channel.attr(REMOTE_ADDRESS_KEY)).thenReturn(remoteAddressAttr);
|
||||
when(remoteAddressAttr.get()).thenReturn("192.168.1.1");
|
||||
when(channel.attr(EppServiceHandler.CLIENT_CERTIFICATE_HASH_KEY)).thenReturn(certHashAttr);
|
||||
when(certificate.getEncoded()).thenReturn(new byte[] {1, 2, 3});
|
||||
String eppLoginXml = "<epp><command><login><clID>RegistrarA</clID></login></command></epp>";
|
||||
ByteBuf inFrame = Unpooled.wrappedBuffer(eppLoginXml.getBytes(UTF_8));
|
||||
|
||||
when(localConnectionLimiter.acquireIp(any(String.class))).thenReturn(true);
|
||||
when(localConnectionLimiter.acquireCert(any(String.class))).thenReturn(false);
|
||||
doAnswer(
|
||||
invocation -> {
|
||||
FakeHttpServletResponse rsp = invocation.getArgument(1);
|
||||
rsp.setHeader(ProxyHttpHeaders.LOGGED_IN_REGISTRAR, "RegistrarA");
|
||||
rsp.getWriter().write("<epp><response>success</response></epp>");
|
||||
return null;
|
||||
})
|
||||
.when(requestHandler)
|
||||
.handleRequest(any(FakeHttpServletRequest.class), any(FakeHttpServletResponse.class));
|
||||
|
||||
certPromise.setSuccess(certificate);
|
||||
when(localConnectionLimiter.acquireRegistrar("RegistrarA")).thenReturn(false);
|
||||
|
||||
verify(metrics).registerQuotaRejection(eq("epp_connection"), any(String.class));
|
||||
handler.channelRead0(ctx, inFrame);
|
||||
|
||||
verify(metrics).registerQuotaRejection(eq("epp_connection_registrar"), eq("RegistrarA"));
|
||||
verify(ctx).close();
|
||||
}
|
||||
|
||||
@@ -194,17 +233,24 @@ class EppServiceHandlerTest {
|
||||
FakeHttpServletRequest req = invocation.getArgument(0);
|
||||
FakeHttpServletResponse rsp = invocation.getArgument(1);
|
||||
|
||||
rsp.setHeader("Set-Cookie", "SESSION_INFO=xyz123");
|
||||
rsp.setHeader("Set-Cookie", "SESSION_INFO=Y2xpZW50SWQ9UmVnaXN0cmFyQQ==");
|
||||
rsp.setHeader(ProxyHttpHeaders.LOGGED_IN_REGISTRAR, "RegistrarA");
|
||||
rsp.getWriter().write("<epp><response>success</response></epp>");
|
||||
return null;
|
||||
})
|
||||
.when(requestHandler)
|
||||
.handleRequest(any(FakeHttpServletRequest.class), any(FakeHttpServletResponse.class));
|
||||
|
||||
// Mock successful registrar connection acquisition
|
||||
when(localConnectionLimiter.acquireRegistrar("RegistrarA")).thenReturn(true);
|
||||
|
||||
handler.channelRead0(ctx, inFrame);
|
||||
|
||||
// Verify command quota was requested for the extracted clID "RegistrarA"
|
||||
verify(commandQuotaManager).acquireQuota(eq(new QuotaRequest("RegistrarA")));
|
||||
// Verify command quota was requested for the cert hash pre-login
|
||||
String certHash = X509Utils.getCertificateHash(certificate);
|
||||
verify(commandQuotaManager, times(2)).acquireQuota(eq(new QuotaRequest(certHash)));
|
||||
verify(localConnectionLimiter).acquireRegistrar("RegistrarA");
|
||||
verify(scheduledFuture).cancel(eq(false));
|
||||
|
||||
// Verify the response from the servlet was written back to the channel
|
||||
verify(ctx)
|
||||
@@ -223,7 +269,7 @@ class EppServiceHandlerTest {
|
||||
invocation -> {
|
||||
FakeHttpServletRequest req = invocation.getArgument(0);
|
||||
// Verify the cookie was properly propagated
|
||||
if (!"SESSION_INFO=xyz123".equals(req.getHeader("Cookie"))) {
|
||||
if (!"SESSION_INFO=Y2xpZW50SWQ9UmVnaXN0cmFyQQ==".equals(req.getHeader("Cookie"))) {
|
||||
throw new AssertionError("Missing or incorrect cookie");
|
||||
}
|
||||
// Verify the registrar ID was properly propagated
|
||||
@@ -236,6 +282,9 @@ class EppServiceHandlerTest {
|
||||
.handleRequest(any(FakeHttpServletRequest.class), any(FakeHttpServletResponse.class));
|
||||
|
||||
handler.channelRead0(ctx, inFrame2);
|
||||
|
||||
// Verify command quota was requested for the authenticated registrar post-login
|
||||
verify(commandQuotaManager).acquireQuota(eq(new QuotaRequest("RegistrarA")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -285,13 +334,129 @@ class EppServiceHandlerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testChannelInactive_releasesQuotas() throws Exception {
|
||||
void testChannelInactive_releasesIp() throws Exception {
|
||||
setUpSuccessfulHandshake();
|
||||
|
||||
handler.channelInactive(ctx);
|
||||
|
||||
// Verify the in-memory limiter releases both IP and Cert
|
||||
// Verify the in-memory limiter releases IP
|
||||
verify(localConnectionLimiter).releaseIp(eq("192.168.1.1"));
|
||||
verify(localConnectionLimiter).releaseCert(any(String.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testChannelInactive_postLogin_releasesIpAndRegistrar() throws Exception {
|
||||
setUpSuccessfulHandshake();
|
||||
|
||||
when(idTokenSupplier.get()).thenReturn("fake_id_token");
|
||||
when(commandQuotaManager.acquireQuota(any(QuotaRequest.class)))
|
||||
.thenReturn(new QuotaResponse(true));
|
||||
when(localConnectionLimiter.acquireRegistrar("RegistrarA")).thenReturn(true);
|
||||
|
||||
String eppLoginXml = "<epp><command><login><clID>RegistrarA</clID></login></command></epp>";
|
||||
ByteBuf inFrame = Unpooled.wrappedBuffer(eppLoginXml.getBytes(UTF_8));
|
||||
|
||||
doAnswer(
|
||||
invocation -> {
|
||||
FakeHttpServletResponse rsp = invocation.getArgument(1);
|
||||
rsp.setHeader("Set-Cookie", "SESSION_INFO=Y2xpZW50SWQ9UmVnaXN0cmFyQQ==");
|
||||
rsp.setHeader(ProxyHttpHeaders.LOGGED_IN_REGISTRAR, "RegistrarA");
|
||||
rsp.getWriter().write("<epp><response>success</response></epp>");
|
||||
return null;
|
||||
})
|
||||
.when(requestHandler)
|
||||
.handleRequest(any(FakeHttpServletRequest.class), any(FakeHttpServletResponse.class));
|
||||
|
||||
handler.channelRead0(ctx, inFrame);
|
||||
|
||||
handler.channelInactive(ctx);
|
||||
|
||||
// Verify the in-memory limiter releases both IP and Registrar
|
||||
verify(localConnectionLimiter).releaseIp(eq("192.168.1.1"));
|
||||
verify(localConnectionLimiter).releaseRegistrar(eq("RegistrarA"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testChannelActive_loginTimeoutTriggered() throws Exception {
|
||||
ArgumentCaptor<Runnable> timeoutTaskCaptor = ArgumentCaptor.forClass(Runnable.class);
|
||||
when(executor.schedule(timeoutTaskCaptor.capture(), eq(10L), eq(TimeUnit.SECONDS)))
|
||||
.thenReturn(null);
|
||||
|
||||
setUpSuccessfulHandshake();
|
||||
|
||||
Runnable timeoutTask = timeoutTaskCaptor.getValue();
|
||||
assertThat(timeoutTask).isNotNull();
|
||||
|
||||
ChannelFuture closeFuture = mock(ChannelFuture.class);
|
||||
when(ctx.close()).thenReturn(closeFuture);
|
||||
|
||||
timeoutTask.run();
|
||||
|
||||
verify(metrics).registerQuotaRejection("epp_login_timeout", "192.168.1.1");
|
||||
verify(ctx).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFallbackToSocketIpAddress_whenRemoteAddressKeyIsNull() throws Exception {
|
||||
certPromise = new DefaultPromise<>(ImmediateEventExecutor.INSTANCE);
|
||||
when(channel.attr(CLIENT_CERTIFICATE_PROMISE_KEY)).thenReturn(certPromiseAttr);
|
||||
when(certPromiseAttr.get()).thenReturn(certPromise);
|
||||
|
||||
handler.channelActive(ctx);
|
||||
|
||||
// Mock REMOTE_ADDRESS_KEY to return null
|
||||
when(channel.attr(REMOTE_ADDRESS_KEY)).thenReturn(remoteAddressAttr);
|
||||
when(remoteAddressAttr.get()).thenReturn(null);
|
||||
|
||||
// Mock channel.remoteAddress() to return a socket address
|
||||
InetSocketAddress socketAddress = new InetSocketAddress("10.0.0.1", 12345);
|
||||
when(channel.remoteAddress()).thenReturn(socketAddress);
|
||||
|
||||
when(channel.attr(EppServiceHandler.CLIENT_CERTIFICATE_HASH_KEY)).thenReturn(certHashAttr);
|
||||
when(certificate.getEncoded()).thenReturn(new byte[] {1, 2, 3});
|
||||
|
||||
when(localConnectionLimiter.acquireIp("10.0.0.1")).thenReturn(true);
|
||||
when(idTokenSupplier.get()).thenReturn("fake_id_token");
|
||||
|
||||
// Stub request handler to capture the request
|
||||
ArgumentCaptor<FakeHttpServletRequest> requestCaptor =
|
||||
ArgumentCaptor.forClass(FakeHttpServletRequest.class);
|
||||
doAnswer(
|
||||
invocation -> {
|
||||
FakeHttpServletResponse rsp = invocation.getArgument(1);
|
||||
rsp.getWriter().write("<epp><greeting/></epp>");
|
||||
return null;
|
||||
})
|
||||
.when(requestHandler)
|
||||
.handleRequest(requestCaptor.capture(), any(FakeHttpServletResponse.class));
|
||||
|
||||
certPromise.setSuccess(certificate);
|
||||
|
||||
// Verify it resolved IP to 10.0.0.1
|
||||
verify(localConnectionLimiter).acquireIp("10.0.0.1");
|
||||
FakeHttpServletRequest capturedRequest = requestCaptor.getValue();
|
||||
assertThat(capturedRequest).isNotNull();
|
||||
assertThat(capturedRequest.getHeader(ProxyHttpHeaders.IP_ADDRESS)).isEqualTo("10.0.0.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFallbackToSocketIpAddress_fails_closesConnection() throws Exception {
|
||||
certPromise = new DefaultPromise<>(ImmediateEventExecutor.INSTANCE);
|
||||
when(channel.attr(CLIENT_CERTIFICATE_PROMISE_KEY)).thenReturn(certPromiseAttr);
|
||||
when(certPromiseAttr.get()).thenReturn(certPromise);
|
||||
|
||||
handler.channelActive(ctx);
|
||||
|
||||
// Mock REMOTE_ADDRESS_KEY to return null
|
||||
when(channel.attr(REMOTE_ADDRESS_KEY)).thenReturn(remoteAddressAttr);
|
||||
when(remoteAddressAttr.get()).thenReturn(null);
|
||||
|
||||
// Mock channel.remoteAddress() to return a non-InetSocketAddress (e.g. mock SocketAddress)
|
||||
SocketAddress mockSocketAddress = mock(SocketAddress.class);
|
||||
when(channel.remoteAddress()).thenReturn(mockSocketAddress);
|
||||
|
||||
certPromise.setSuccess(certificate);
|
||||
|
||||
// Verify it logged error and closed connection
|
||||
verify(ctx).close();
|
||||
}
|
||||
}
|
||||
|
||||
+20
-20
@@ -65,37 +65,37 @@ class LocalConnectionLimiterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAcquireCert_successUpToLimit() {
|
||||
assertThat(limiter.acquireCert("cert_hash_1")).isTrue();
|
||||
assertThat(limiter.acquireCert("cert_hash_1")).isTrue();
|
||||
void testAcquireRegistrar_successUpToLimit() {
|
||||
assertThat(limiter.acquireRegistrar("registrar_1")).isTrue();
|
||||
assertThat(limiter.acquireRegistrar("registrar_1")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAcquireCert_rejectsOverLimit() {
|
||||
assertThat(limiter.acquireCert("cert_hash_1")).isTrue();
|
||||
assertThat(limiter.acquireCert("cert_hash_1")).isTrue();
|
||||
// 3rd attempt from same cert should be rejected
|
||||
assertThat(limiter.acquireCert("cert_hash_1")).isFalse();
|
||||
void testAcquireRegistrar_rejectsOverLimit() {
|
||||
assertThat(limiter.acquireRegistrar("registrar_1")).isTrue();
|
||||
assertThat(limiter.acquireRegistrar("registrar_1")).isTrue();
|
||||
// 3rd attempt from same registrar should be rejected
|
||||
assertThat(limiter.acquireRegistrar("registrar_1")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAcquireCert_independentAcrossCerts() {
|
||||
assertThat(limiter.acquireCert("cert_hash_1")).isTrue();
|
||||
assertThat(limiter.acquireCert("cert_hash_1")).isTrue();
|
||||
assertThat(limiter.acquireCert("cert_hash_1")).isFalse();
|
||||
void testAcquireRegistrar_independentAcrossRegistrars() {
|
||||
assertThat(limiter.acquireRegistrar("registrar_1")).isTrue();
|
||||
assertThat(limiter.acquireRegistrar("registrar_1")).isTrue();
|
||||
assertThat(limiter.acquireRegistrar("registrar_1")).isFalse();
|
||||
|
||||
// A different cert should still be allowed
|
||||
assertThat(limiter.acquireCert("cert_hash_2")).isTrue();
|
||||
// A different registrar should still be allowed
|
||||
assertThat(limiter.acquireRegistrar("registrar_2")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReleaseCert_freesSlot() {
|
||||
assertThat(limiter.acquireCert("cert_hash_1")).isTrue();
|
||||
assertThat(limiter.acquireCert("cert_hash_1")).isTrue();
|
||||
assertThat(limiter.acquireCert("cert_hash_1")).isFalse();
|
||||
void testReleaseRegistrar_freesSlot() {
|
||||
assertThat(limiter.acquireRegistrar("registrar_1")).isTrue();
|
||||
assertThat(limiter.acquireRegistrar("registrar_1")).isTrue();
|
||||
assertThat(limiter.acquireRegistrar("registrar_1")).isFalse();
|
||||
|
||||
limiter.releaseCert("cert_hash_1");
|
||||
limiter.releaseRegistrar("registrar_1");
|
||||
// Now we should be able to acquire again
|
||||
assertThat(limiter.acquireCert("cert_hash_1")).isTrue();
|
||||
assertThat(limiter.acquireRegistrar("registrar_1")).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
// Copyright 2026 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.flows;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.USE_RANDOM_SERVER_TRID;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureStatus.ACTIVE;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static google.registry.util.DateTimeUtils.START_INSTANT;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import google.registry.model.common.FeatureFlag;
|
||||
import google.registry.model.common.FeatureFlag.FeatureStatus;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Instant;
|
||||
import java.util.regex.Pattern;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
/** Unit tests for {@link ServerTridProviderImpl}. */
|
||||
class ServerTridProviderImplTest {
|
||||
|
||||
@RegisterExtension
|
||||
final JpaIntegrationTestExtension jpa =
|
||||
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
ServerTridProviderImpl.secureRandom.remove();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateServerTrid_flagInactive_generatesLegacyFormat() {
|
||||
ServerTridProviderImpl provider = new ServerTridProviderImpl();
|
||||
String trid1 = provider.createServerTrid();
|
||||
String trid2 = provider.createServerTrid();
|
||||
|
||||
assertThat(trid1).contains("-");
|
||||
assertThat(trid2).contains("-");
|
||||
assertThat(trid1).isNotEqualTo(trid2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateServerTrid_flagActive_generatesCorrectFormat() {
|
||||
persistResource(
|
||||
new FeatureFlag.Builder()
|
||||
.setFeatureName(USE_RANDOM_SERVER_TRID)
|
||||
.setStatusMap(
|
||||
ImmutableSortedMap.<Instant, FeatureStatus>naturalOrder()
|
||||
.put(START_INSTANT, ACTIVE)
|
||||
.build())
|
||||
.build());
|
||||
|
||||
SecureRandom mockSecureRandom = mock(SecureRandom.class);
|
||||
|
||||
// Mock secureRandom to return a deterministic sequence of bytes: 0, 1, 2, ..., 23
|
||||
doAnswer(
|
||||
invocation -> {
|
||||
byte[] bytes = invocation.getArgument(0);
|
||||
for (int i = 0; i < bytes.length; i++) {
|
||||
bytes[i] = (byte) i;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.when(mockSecureRandom)
|
||||
.nextBytes(any(byte[].class));
|
||||
|
||||
ServerTridProviderImpl.secureRandom.set(mockSecureRandom);
|
||||
ServerTridProviderImpl provider = new ServerTridProviderImpl();
|
||||
String trid = provider.createServerTrid();
|
||||
|
||||
String expectedTrid = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYX";
|
||||
|
||||
Pattern tridPattern = Pattern.compile("^[A-Za-z0-9_-]{32}$");
|
||||
assertThat(trid).matches(tridPattern);
|
||||
assertThat(trid.length()).isAtMost(64);
|
||||
assertThat(trid).isEqualTo(expectedTrid);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateServerTrid_flagActive_withMaxByteValues() {
|
||||
persistResource(
|
||||
new FeatureFlag.Builder()
|
||||
.setFeatureName(USE_RANDOM_SERVER_TRID)
|
||||
.setStatusMap(
|
||||
ImmutableSortedMap.<Instant, FeatureStatus>naturalOrder()
|
||||
.put(START_INSTANT, ACTIVE)
|
||||
.build())
|
||||
.build());
|
||||
|
||||
SecureRandom mockSecureRandom = mock(SecureRandom.class);
|
||||
|
||||
// Mock secureRandom to return all 0xFF bytes
|
||||
doAnswer(
|
||||
invocation -> {
|
||||
byte[] bytes = invocation.getArgument(0);
|
||||
for (int i = 0; i < bytes.length; i++) {
|
||||
bytes[i] = (byte) 0xFF;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.when(mockSecureRandom)
|
||||
.nextBytes(any(byte[].class));
|
||||
|
||||
ServerTridProviderImpl.secureRandom.set(mockSecureRandom);
|
||||
ServerTridProviderImpl provider = new ServerTridProviderImpl();
|
||||
String trid = provider.createServerTrid();
|
||||
|
||||
String expectedTrid = "________________________________";
|
||||
assertThat(trid).isEqualTo(expectedTrid);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateServerTrid_flagActive_realInitializationWorks() {
|
||||
persistResource(
|
||||
new FeatureFlag.Builder()
|
||||
.setFeatureName(USE_RANDOM_SERVER_TRID)
|
||||
.setStatusMap(
|
||||
ImmutableSortedMap.<Instant, FeatureStatus>naturalOrder()
|
||||
.put(START_INSTANT, ACTIVE)
|
||||
.build())
|
||||
.build());
|
||||
|
||||
ServerTridProviderImpl provider = new ServerTridProviderImpl();
|
||||
String trid1 = provider.createServerTrid();
|
||||
String trid2 = provider.createServerTrid();
|
||||
|
||||
Pattern tridPattern = Pattern.compile("^[A-Za-z0-9_-]{32}$");
|
||||
assertThat(trid1).matches(tridPattern);
|
||||
assertThat(trid2).matches(tridPattern);
|
||||
assertThat(trid1).isNotEqualTo(trid2);
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,6 @@ import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
|
||||
import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.CurrencyUnitMismatchException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.CurrencyValueScaleException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.DomainReservedException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.FeesMismatchException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.FeesRequiredForPremiumNameException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.MissingBillingAccountMapException;
|
||||
@@ -397,15 +396,14 @@ class DomainRestoreRequestFlowTest extends ResourceFlowTestCase<DomainRestoreReq
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_superuserOverridesReservedList() throws Exception {
|
||||
void testSuccess_reservedDomain() throws Exception {
|
||||
persistResource(
|
||||
Tld.get("tld")
|
||||
.asBuilder()
|
||||
.setReservedLists(persistReservedList("tld-reserved", "example,FULLY_BLOCKED"))
|
||||
.build());
|
||||
persistPendingDeleteDomain();
|
||||
runFlowAssertResponse(
|
||||
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("generic_success_response.xml"));
|
||||
runFlowAssertResponse(loadFile("generic_success_response.xml"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -631,19 +629,6 @@ class DomainRestoreRequestFlowTest extends ResourceFlowTestCase<DomainRestoreReq
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_reservedBlocked() throws Exception {
|
||||
createTld("tld");
|
||||
persistResource(
|
||||
Tld.get("tld")
|
||||
.asBuilder()
|
||||
.setReservedLists(persistReservedList("tld-reserved", "example,FULLY_BLOCKED"))
|
||||
.build());
|
||||
persistPendingDeleteDomain();
|
||||
EppException thrown = assertThrows(DomainReservedException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_premiumNotAcked() throws Exception {
|
||||
createTld("example");
|
||||
|
||||
+5
@@ -139,6 +139,11 @@ public abstract class JpaTransactionManagerExtension
|
||||
private static JdbcDatabaseContainer<?> create() {
|
||||
PostgreSQLContainer<?> container =
|
||||
new PostgreSQLContainer<>(NomulusPostgreSql.getDockerImageName())
|
||||
// Locale configs in use on Cloud SQL in all environments
|
||||
.withEnv(
|
||||
"POSTGRES_INITDB_ARGS",
|
||||
"--encoding=UTF8 --lc-collate=en_US.UTF8 --lc-ctype=en_US.UTF8"
|
||||
+ " --locale-provider=libc --no-locale")
|
||||
.withDatabaseName(POSTGRES_DB_NAME);
|
||||
container.start();
|
||||
return container;
|
||||
|
||||
+37
@@ -25,7 +25,10 @@ import google.registry.persistence.transaction.JpaTestExtensions.JpaUnitTestExte
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.PersistenceException;
|
||||
import jakarta.persistence.Tuple;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
@@ -59,6 +62,40 @@ public class JpaTransactionManagerExtensionTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifyDatabaseEncodingConfig() {
|
||||
String sql =
|
||||
"""
|
||||
SELECT
|
||||
pg_encoding_to_char(encoding) AS encoding,
|
||||
datcollate AS collation,
|
||||
datctype AS ctype,
|
||||
datlocprovider AS locale_provider,
|
||||
datlocale AS locale
|
||||
FROM
|
||||
pg_database
|
||||
WHERE
|
||||
datname = 'postgres'
|
||||
""";
|
||||
List<Tuple> rows =
|
||||
tm().transact(
|
||||
() -> tm().getEntityManager().createNativeQuery(sql, Tuple.class).getResultList());
|
||||
assertThat(rows).hasSize(1);
|
||||
var row =
|
||||
rows.get(0).getElements().stream()
|
||||
.collect(
|
||||
HashMap::new, // Use HashMap since there may be null value
|
||||
(m, element) -> m.put(element.getAlias(), rows.get(0).get(element)),
|
||||
Map::putAll);
|
||||
assertThat(row)
|
||||
.containsExactly(
|
||||
"encoding", "UTF8",
|
||||
"collation", "en_US.UTF8",
|
||||
"ctype", "en_US.UTF8",
|
||||
"locale_provider", 'c', // c --> libc
|
||||
"locale", null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReplicaJpaTm() {
|
||||
TestEntity testEntity = new TestEntity("foo", "bar");
|
||||
|
||||
@@ -170,9 +170,7 @@ class NordnVerifyActionTest {
|
||||
void testFailure_badUrl() throws Exception {
|
||||
action.url = URI.create("http://example.com/blobio").toURL();
|
||||
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, action::run);
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo("URL http://example.com/blobio must start with ry.marksdb.org");
|
||||
assertThat(thrown).hasMessageThat().isEqualTo("Host example.com must equal ry.marksdb.org");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -193,4 +193,27 @@ public class BulkDomainTransferCommandTest extends CommandTestCase<BulkDomainTra
|
||||
MediaType.PLAIN_TEXT_UTF_8,
|
||||
"[\"foo.tld\",\"bar.tld\"]".getBytes(UTF_8));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_noLosingRegistrarId() throws Exception {
|
||||
runCommandForced(
|
||||
"--gaining_registrar_id",
|
||||
"NewRegistrar",
|
||||
"--reason",
|
||||
"someReason",
|
||||
"--domains",
|
||||
"foo.tld,bar.tld");
|
||||
verify(connection)
|
||||
.sendPostRequest(
|
||||
"/_dr/task/bulkDomainTransfer",
|
||||
ImmutableMap.of(
|
||||
"gainingRegistrarId",
|
||||
"NewRegistrar",
|
||||
"requestedByRegistrar",
|
||||
false,
|
||||
"reason",
|
||||
"someReason"),
|
||||
MediaType.PLAIN_TEXT_UTF_8,
|
||||
"[\"foo.tld\",\"bar.tld\"]".getBytes(UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +203,24 @@ class ConsoleOteActionTest extends ConsoleActionBaseTestCase {
|
||||
ImmutableList.of("domain creates idn", "domain restores", "host deletes"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_invalidEmailDomain() {
|
||||
AuthResult authResult = AuthResult.createUser(fteUser);
|
||||
consoleApiParams = ConsoleApiParamsUtils.createFake(authResult);
|
||||
ConsoleOteAction action =
|
||||
createAction(
|
||||
Action.Method.POST,
|
||||
authResult,
|
||||
"theregistrar",
|
||||
Optional.of("someRandomString@email.test"),
|
||||
Optional.of(new OteCreateData("theregistrar", "contact@invalid.com")));
|
||||
action.cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
|
||||
action.run();
|
||||
assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST);
|
||||
assertThat(response.getPayload())
|
||||
.isEqualTo("Email address must exist in the registry.example domain");
|
||||
}
|
||||
|
||||
private ConsoleOteAction createAction(
|
||||
Action.Method method,
|
||||
AuthResult authResult,
|
||||
@@ -215,10 +233,11 @@ class ConsoleOteActionTest extends ConsoleActionBaseTestCase {
|
||||
return new ConsoleOteAction(
|
||||
consoleApiParams,
|
||||
iamClient,
|
||||
registrarId,
|
||||
passwordGenerator,
|
||||
oteCreateData,
|
||||
maybeGroupEmailAddress,
|
||||
Optional.of("consoleIapServiceId"),
|
||||
passwordGenerator,
|
||||
oteCreateData);
|
||||
"registry.example",
|
||||
registrarId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
package google.registry.ui.server.console;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.DatabaseHelper.loadByKey;
|
||||
import static google.registry.testing.DatabaseHelper.loadRegistrar;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_CREATED;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
|
||||
@@ -36,6 +39,7 @@ import google.registry.model.console.GlobalRole;
|
||||
import google.registry.model.console.RegistrarRole;
|
||||
import google.registry.model.console.User;
|
||||
import google.registry.model.console.UserRoles;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.request.auth.AuthResult;
|
||||
import google.registry.testing.CloudTasksHelper;
|
||||
@@ -126,6 +130,23 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase {
|
||||
"[{\"emailAddress\":\"test1@test.com\",\"role\":\"PRIMARY_CONTACT\"},{\"emailAddress\":\"test2@test.com\",\"role\":\"PRIMARY_CONTACT\"}]");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_pendingRegistrar() throws Exception {
|
||||
persistResource(
|
||||
loadRegistrar("TheRegistrar").asBuilder().setState(Registrar.State.PENDING).build());
|
||||
AuthResult authResult =
|
||||
AuthResult.createUser(loadByKey(VKey.create(User.class, "test1@test.com")));
|
||||
ConsoleUsersAction action =
|
||||
createAction(
|
||||
Optional.of(ConsoleApiParamsUtils.createFake(authResult)),
|
||||
Optional.of("GET"),
|
||||
Optional.empty());
|
||||
|
||||
action.run();
|
||||
assertThat(response.getStatus()).isEqualTo(SC_OK);
|
||||
assertThat(response.getPayload()).contains("test1@test.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_noPermission() throws IOException {
|
||||
UserRoles userRoles =
|
||||
@@ -205,7 +226,7 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase {
|
||||
|
||||
@Test
|
||||
void testFailure_noPermissionToDeleteUser() throws IOException {
|
||||
User user1 = DatabaseHelper.loadByKey(VKey.create(User.class, "test1@test.com"));
|
||||
User user1 = loadByKey(VKey.create(User.class, "test1@test.com"));
|
||||
AuthResult authResult =
|
||||
AuthResult.createUser(
|
||||
user1
|
||||
@@ -248,7 +269,7 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase {
|
||||
@Test
|
||||
void testSuccess_deletesUser_nonConsoleMintedAddress_skipsWorkspaceAccountDeletion()
|
||||
throws IOException {
|
||||
User user1 = DatabaseHelper.loadByKey(VKey.create(User.class, "test1@test.com"));
|
||||
User user1 = loadByKey(VKey.create(User.class, "test1@test.com"));
|
||||
AuthResult authResult =
|
||||
AuthResult.createUser(
|
||||
user1
|
||||
@@ -274,7 +295,7 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase {
|
||||
|
||||
@Test
|
||||
void testSuccess_deletesUser_consoleMintedAddress_deletesWorkspaceAccount() throws IOException {
|
||||
User user1 = DatabaseHelper.loadByKey(VKey.create(User.class, "test1@test.com"));
|
||||
User user1 = loadByKey(VKey.create(User.class, "test1@test.com"));
|
||||
AuthResult authResult =
|
||||
AuthResult.createUser(
|
||||
user1
|
||||
@@ -282,7 +303,7 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase {
|
||||
.setUserRoles(user1.getUserRoles().asBuilder().setIsAdmin(true).build())
|
||||
.build());
|
||||
String mintedEmail = "abc.TheRegistrar@email.com";
|
||||
DatabaseHelper.persistResource(
|
||||
persistResource(
|
||||
new User.Builder()
|
||||
.setEmailAddress(mintedEmail)
|
||||
.setUserRoles(
|
||||
@@ -311,14 +332,14 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase {
|
||||
|
||||
@Test
|
||||
void testSuccess_removesRole() throws IOException {
|
||||
User user1 = DatabaseHelper.loadByKey(VKey.create(User.class, "test1@test.com"));
|
||||
User user1 = loadByKey(VKey.create(User.class, "test1@test.com"));
|
||||
AuthResult authResult =
|
||||
AuthResult.createUser(
|
||||
user1
|
||||
.asBuilder()
|
||||
.setUserRoles(user1.getUserRoles().asBuilder().setIsAdmin(true).build())
|
||||
.build());
|
||||
DatabaseHelper.persistResource(
|
||||
persistResource(
|
||||
new User.Builder()
|
||||
.setEmailAddress("test4@test.com")
|
||||
.setUserRoles(
|
||||
@@ -355,7 +376,7 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase {
|
||||
|
||||
@Test
|
||||
void testFailure_limitedTo4UsersPerRegistrar() throws IOException {
|
||||
User user1 = DatabaseHelper.loadByKey(VKey.create(User.class, "test1@test.com"));
|
||||
User user1 = loadByKey(VKey.create(User.class, "test1@test.com"));
|
||||
AuthResult authResult =
|
||||
AuthResult.createUser(
|
||||
user1
|
||||
@@ -395,7 +416,7 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase {
|
||||
|
||||
@Test
|
||||
void testSuccess_updatesUserRole() throws IOException {
|
||||
User user1 = DatabaseHelper.loadByKey(VKey.create(User.class, "test1@test.com"));
|
||||
User user1 = loadByKey(VKey.create(User.class, "test1@test.com"));
|
||||
AuthResult authResult =
|
||||
AuthResult.createUser(
|
||||
user1
|
||||
@@ -404,7 +425,7 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase {
|
||||
.build());
|
||||
|
||||
assertThat(
|
||||
DatabaseHelper.loadByKey(VKey.create(User.class, "test2@test.com"))
|
||||
loadByKey(VKey.create(User.class, "test2@test.com"))
|
||||
.getUserRoles()
|
||||
.getRegistrarRoles()
|
||||
.get("TheRegistrar"))
|
||||
@@ -420,7 +441,7 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase {
|
||||
action.run();
|
||||
assertThat(response.getStatus()).isEqualTo(SC_OK);
|
||||
assertThat(
|
||||
DatabaseHelper.loadByKey(VKey.create(User.class, "test2@test.com"))
|
||||
loadByKey(VKey.create(User.class, "test2@test.com"))
|
||||
.getUserRoles()
|
||||
.getRegistrarRoles()
|
||||
.get("TheRegistrar"))
|
||||
@@ -429,7 +450,7 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase {
|
||||
|
||||
@Test
|
||||
void testFailure_noPermissionToUpdateUser() throws IOException {
|
||||
User user1 = DatabaseHelper.loadByKey(VKey.create(User.class, "test1@test.com"));
|
||||
User user1 = loadByKey(VKey.create(User.class, "test1@test.com"));
|
||||
AuthResult authResult =
|
||||
AuthResult.createUser(
|
||||
user1
|
||||
@@ -461,14 +482,14 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase {
|
||||
new UserData("test3@test.com", null, RegistrarRole.TECH_CONTACT.name(), null)));
|
||||
action.run();
|
||||
assertThat(response.getStatus()).isEqualTo(SC_OK);
|
||||
User appendedUser = DatabaseHelper.loadByKey(VKey.create(User.class, "test3@test.com"));
|
||||
User appendedUser = loadByKey(VKey.create(User.class, "test3@test.com"));
|
||||
assertThat(appendedUser.getUserRoles().getRegistrarRoles().get("TheRegistrar"))
|
||||
.isEqualTo(RegistrarRole.TECH_CONTACT);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_appendUser_crossTenantNoPermission() throws IOException {
|
||||
User callingUser = DatabaseHelper.loadByKey(VKey.create(User.class, "test1@test.com"));
|
||||
User callingUser = loadByKey(VKey.create(User.class, "test1@test.com"));
|
||||
AuthResult authResult = AuthResult.createUser(callingUser);
|
||||
ConsoleUsersAction action =
|
||||
createAction(
|
||||
@@ -483,7 +504,7 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase {
|
||||
@Test
|
||||
void testSuccess_appendUser_crossTenantWithPermission() throws IOException {
|
||||
User callingUser =
|
||||
DatabaseHelper.persistResource(
|
||||
persistResource(
|
||||
new User.Builder()
|
||||
.setEmailAddress("multitenant@test.com")
|
||||
.setUserRoles(
|
||||
@@ -506,7 +527,7 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase {
|
||||
new UserData("test3@test.com", null, RegistrarRole.TECH_CONTACT.name(), null)));
|
||||
action.run();
|
||||
assertThat(response.getStatus()).isEqualTo(SC_OK);
|
||||
User appendedUser = DatabaseHelper.loadByKey(VKey.create(User.class, "test3@test.com"));
|
||||
User appendedUser = loadByKey(VKey.create(User.class, "test3@test.com"));
|
||||
assertThat(appendedUser.getUserRoles().getRegistrarRoles().get("TheRegistrar"))
|
||||
.isEqualTo(RegistrarRole.TECH_CONTACT);
|
||||
}
|
||||
@@ -515,7 +536,7 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase {
|
||||
void testFailure_appendUser_globalAdmin() throws IOException {
|
||||
User user = DatabaseHelper.createAdminUser("email@email.com");
|
||||
AuthResult authResult = AuthResult.createUser(user);
|
||||
DatabaseHelper.persistResource(
|
||||
persistResource(
|
||||
new User.Builder()
|
||||
.setEmailAddress("globaladmin@test.com")
|
||||
.setUserRoles(
|
||||
@@ -539,7 +560,7 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase {
|
||||
void testFailure_appendUser_globalRole() throws IOException {
|
||||
User user = DatabaseHelper.createAdminUser("email@email.com");
|
||||
AuthResult authResult = AuthResult.createUser(user);
|
||||
DatabaseHelper.persistResource(
|
||||
persistResource(
|
||||
new User.Builder()
|
||||
.setEmailAddress("support@test.com")
|
||||
.setUserRoles(
|
||||
@@ -566,7 +587,7 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase {
|
||||
User user = DatabaseHelper.createAdminUser("email@email.com");
|
||||
AuthResult authResult = AuthResult.createUser(user);
|
||||
// Historically associated global admin
|
||||
DatabaseHelper.persistResource(
|
||||
persistResource(
|
||||
new User.Builder()
|
||||
.setEmailAddress("globaladmin@test.com")
|
||||
.setUserRoles(
|
||||
@@ -596,7 +617,7 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase {
|
||||
User user = DatabaseHelper.createAdminUser("email@email.com");
|
||||
AuthResult authResult = AuthResult.createUser(user);
|
||||
// Historically associated global admin
|
||||
DatabaseHelper.persistResource(
|
||||
persistResource(
|
||||
new User.Builder()
|
||||
.setEmailAddress("globaladmin@test.com")
|
||||
.setUserRoles(
|
||||
@@ -626,7 +647,7 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase {
|
||||
User user = DatabaseHelper.createAdminUser("email@email.com");
|
||||
AuthResult authResult = AuthResult.createUser(user);
|
||||
// Historically associated user with global role
|
||||
DatabaseHelper.persistResource(
|
||||
persistResource(
|
||||
new User.Builder()
|
||||
.setEmailAddress("support@test.com")
|
||||
.setUserRoles(
|
||||
|
||||
-12
@@ -251,18 +251,6 @@ public class ConsoleBulkDomainActionTest extends ConsoleActionBaseTestCase {
|
||||
assertThat(response.getStatus()).isEqualTo(SC_FORBIDDEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_pendingRegistrar() {
|
||||
persistResource(
|
||||
loadRegistrar("TheRegistrar").asBuilder().setState(Registrar.State.PENDING).build());
|
||||
JsonElement payload =
|
||||
GSON.toJsonTree(
|
||||
ImmutableMap.of("domainList", ImmutableList.of("example.tld"), "reason", "test"));
|
||||
ConsoleBulkDomainAction action = createAction("DELETE", payload);
|
||||
action.run();
|
||||
assertThat(response.getStatus()).isEqualTo(SC_FORBIDDEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_nonexistentRegistrar() {
|
||||
JsonElement payload =
|
||||
|
||||
@@ -257,15 +257,15 @@ td.section {
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="property_name">generated by</td>
|
||||
<td class="property_value">SchemaCrawler 17.11.1</td>
|
||||
<td class="property_value">SchemaCrawler 17.12.2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="property_name">generated on</td>
|
||||
<td class="property_value">2026-07-14 19:18:20</td>
|
||||
<td class="property_value">2026-08-12 15:34:00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="property_name">last flyway file</td>
|
||||
<td id="lastFlywayFile" class="property_value">V225__user_registry_lock_email_address_index.sql</td>
|
||||
<td id="lastFlywayFile" class="property_value">V228__hosthistory_repo_id_mod_time_idx.sql</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -273,7 +273,7 @@ td.section {
|
||||
<p> </p>
|
||||
<svg viewBox="0.00 0.00 4783.00 3613.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="erDiagram" style="overflow: hidden; width: 100%; height: 800px">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 3608.5)">
|
||||
<title>SchemaCrawler_Diagram</title> <polygon fill="white" stroke="none" points="-4,4 -4,-3608.5 4778.75,-3608.5 4778.75,4 -4,4" /> <text xml:space="preserve" text-anchor="start" x="4535.5" y="-29.2" font-family="Helvetica,sans-Serif" font-size="14.00">generated by</text> <text xml:space="preserve" text-anchor="start" x="4618.25" y="-29.2" font-family="Helvetica,sans-Serif" font-size="14.00">SchemaCrawler 17.11.1</text> <text xml:space="preserve" text-anchor="start" x="4534.75" y="-9.45" font-family="Helvetica,sans-Serif" font-size="14.00">generated on</text> <text xml:space="preserve" text-anchor="start" x="4618.25" y="-9.45" font-family="Helvetica,sans-Serif" font-size="14.00">2026-07-14 19:18:20</text> <polygon fill="none" stroke="#888888" points="4531.75,-4 4531.75,-45.5 4766.75,-45.5 4766.75,-4 4531.75,-4" /> <!-- allocationtoken_a08ccbef -->
|
||||
<title>SchemaCrawler_Diagram</title> <polygon fill="white" stroke="none" points="-4,4 -4,-3608.5 4778.75,-3608.5 4778.75,4 -4,4" /> <text xml:space="preserve" text-anchor="start" x="4535.5" y="-29.2" font-family="Helvetica,sans-Serif" font-size="14.00">generated by</text> <text xml:space="preserve" text-anchor="start" x="4618.25" y="-29.2" font-family="Helvetica,sans-Serif" font-size="14.00">SchemaCrawler 17.12.2</text> <text xml:space="preserve" text-anchor="start" x="4534.75" y="-9.45" font-family="Helvetica,sans-Serif" font-size="14.00">generated on</text> <text xml:space="preserve" text-anchor="start" x="4618.25" y="-9.45" font-family="Helvetica,sans-Serif" font-size="14.00">2026-08-12 15:34:00</text> <polygon fill="none" stroke="#888888" points="4531.75,-4 4531.75,-45.5 4766.75,-45.5 4766.75,-4 4531.75,-4" /> <!-- allocationtoken_a08ccbef -->
|
||||
<g id="node1" class="node">
|
||||
<title>allocationtoken_a08ccbef</title> <polygon fill="#e9c2f2" stroke="none" points="479.25,-1014.62 479.25,-1034.38 664.25,-1034.38 664.25,-1014.62 479.25,-1014.62" /> <text xml:space="preserve" text-anchor="start" x="481.25" y="-1020.08" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."AllocationToken"</text> <polygon fill="#e9c2f2" stroke="none" points="664.25,-1014.62 664.25,-1034.38 737.25,-1034.38 737.25,-1014.62 664.25,-1014.62" /> <text xml:space="preserve" text-anchor="start" x="698.5" y="-1019.08" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text> <text xml:space="preserve" text-anchor="start" x="481.25" y="-1000.33" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">token</text> <text xml:space="preserve" text-anchor="start" x="658.5" y="-999.33" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="666.25" y="-999.33" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text> <text xml:space="preserve" text-anchor="start" x="481.25" y="-979.58" font-family="Helvetica,sans-Serif" font-size="14.00">domain_name</text> <text xml:space="preserve" text-anchor="start" x="658.5" y="-979.58" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="666.25" y="-979.58" font-family="Helvetica,sans-Serif" font-size="14.00">text</text> <text xml:space="preserve" text-anchor="start" x="481.25" y="-959.83" font-family="Helvetica,sans-Serif" font-size="14.00">redemption_domain_repo_id</text> <text xml:space="preserve" text-anchor="start" x="658.5" y="-959.83" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="666.25" y="-959.83" font-family="Helvetica,sans-Serif" font-size="14.00">text</text> <text xml:space="preserve" text-anchor="start" x="481.25" y="-940.08" font-family="Helvetica,sans-Serif" font-size="14.00">token_type</text> <text xml:space="preserve" text-anchor="start" x="658.5" y="-940.08" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="666.25" y="-940.08" font-family="Helvetica,sans-Serif" font-size="14.00">text</text> <polygon fill="none" stroke="#888888" points="478.25,-934.62 478.25,-1035.38 738.25,-1035.38 738.25,-934.62 478.25,-934.62" />
|
||||
</g>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -223,3 +223,6 @@ V222__remove_contact.sql
|
||||
V223__tld_change_xap_enabled_to_transitions.sql
|
||||
V224__add_registrar_expiry_access_period_enabled.sql
|
||||
V225__user_registry_lock_email_address_index.sql
|
||||
V226__tld_domain_name_index.sql
|
||||
V227__domainhistory_repo_id_mod_time_idx.sql
|
||||
V228__hosthistory_repo_id_mod_time_idx.sql
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
-- Copyright 2026 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.
|
||||
|
||||
-- RDAP queries by TLD need to order results. Using a btree that combines
|
||||
-- TLD and name allows us to do this quickly
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS domain_tld_domain_name_idx ON "Domain" (tld, domain_name);
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Copyright 2026 The Nomulus Authors. All Rights Reserved.
|
||||
--
|
||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
||||
-- you may not use this file except in compliance with the License.
|
||||
-- You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS domainhistory_repo_id_modification_time
|
||||
ON "DomainHistory" (domain_repo_id, history_modification_time);
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Copyright 2026 The Nomulus Authors. All Rights Reserved.
|
||||
--
|
||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
||||
-- you may not use this file except in compliance with the License.
|
||||
-- You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS hosthistory_repo_id_modification_time
|
||||
ON "HostHistory" (host_repo_id, history_modification_time);
|
||||
@@ -333,7 +333,7 @@
|
||||
);
|
||||
|
||||
create table "FeatureFlag" (
|
||||
feature_name text not null check ((feature_name in ('TEST_FEATURE','FEE_EXTENSION_1_DOT_0_IN_PROD','MINIMUM_DATASET_CONTACTS_OPTIONAL','MINIMUM_DATASET_CONTACTS_PROHIBITED','INCLUDE_PENDING_DELETE_DATE_FOR_DOMAINS','PROHIBIT_CONTACT_OBJECTS_ON_LOGIN','FORBID_INSECURE_ALGORITHMS_RFC_9904'))),
|
||||
feature_name text not null check ((feature_name in ('TEST_FEATURE','FEE_EXTENSION_1_DOT_0_IN_PROD','MINIMUM_DATASET_CONTACTS_OPTIONAL','MINIMUM_DATASET_CONTACTS_PROHIBITED','INCLUDE_PENDING_DELETE_DATE_FOR_DOMAINS','PROHIBIT_CONTACT_OBJECTS_ON_LOGIN','FORBID_INSECURE_ALGORITHMS_RFC_9904','USE_RANDOM_SERVER_TRID'))),
|
||||
status hstore not null,
|
||||
primary key (feature_name)
|
||||
);
|
||||
|
||||
@@ -1841,6 +1841,13 @@ CREATE INDEX domain_history_to_transaction_record_idx ON public."DomainTransacti
|
||||
CREATE UNIQUE INDEX domain_no_duplicate_active ON public."Domain" USING btree (domain_name) WHERE (deletion_time = '294247-01-10 04:00:54.775+00'::timestamp with time zone);
|
||||
|
||||
|
||||
--
|
||||
-- Name: domain_tld_domain_name_idx; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX domain_tld_domain_name_idx ON public."Domain" USING btree (tld, domain_name);
|
||||
|
||||
|
||||
--
|
||||
-- Name: domaindsdatahistory_domain_history_revision_id_hash; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@@ -1869,6 +1876,13 @@ CREATE INDEX domainhistory_domain_repo_id_hash ON public."DomainHistory" USING h
|
||||
CREATE INDEX domainhistory_history_revision_id_hash ON public."DomainHistory" USING hash (history_revision_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: domainhistory_repo_id_modification_time; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX domainhistory_repo_id_modification_time ON public."DomainHistory" USING btree (domain_repo_id, history_modification_time);
|
||||
|
||||
|
||||
--
|
||||
-- Name: domainhistoryhost_domain_history_history_revision_id_hash; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@@ -1939,6 +1953,13 @@ CREATE INDEX host_host_name_hash ON public."Host" USING hash (host_name);
|
||||
CREATE INDEX host_repo_id_hash ON public."Host" USING hash (repo_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: hosthistory_repo_id_modification_time; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX hosthistory_repo_id_modification_time ON public."HostHistory" USING btree (host_repo_id, history_modification_time);
|
||||
|
||||
|
||||
--
|
||||
-- Name: idx1dyqmqb61xbnj7mt7bk27ds25; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
@@ -12,12 +12,13 @@ spec:
|
||||
metadata:
|
||||
labels:
|
||||
service: epp-server
|
||||
traffic: epp-all
|
||||
spec:
|
||||
serviceAccountName: nomulus
|
||||
nodeSelector:
|
||||
cloud.google.com/machine-family: c4
|
||||
containers:
|
||||
- name: frontend
|
||||
- name: epp-server
|
||||
image: gcr.io/GCP_PROJECT/nomulus
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
@@ -61,7 +62,7 @@ spec:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
- name: CONTAINER_NAME
|
||||
value: frontend
|
||||
value: epp-server
|
||||
---
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
@@ -98,7 +99,7 @@ spec:
|
||||
ipFamilies: [IPv4, IPv6]
|
||||
ipFamilyPolicy: RequireDualStack
|
||||
selector:
|
||||
service: epp-server
|
||||
traffic: epp-all
|
||||
ports:
|
||||
- port: 700
|
||||
targetPort: epp
|
||||
|
||||
@@ -71,6 +71,7 @@ type Task struct {
|
||||
Timeout string `xml:"timeout"`
|
||||
Schedule string `xml:"schedule"`
|
||||
Name string `xml:"name"`
|
||||
Method string `xml:"method"`
|
||||
}
|
||||
|
||||
type QueuesSyncManager struct {
|
||||
@@ -191,6 +192,11 @@ func (manager TasksSyncManager) getArgs(task Task, operationType string) []strin
|
||||
var uri string
|
||||
uri = fmt.Sprintf("https://%s.%s%s", service, baseDomain, strings.TrimSpace(task.URL))
|
||||
|
||||
method := "get"
|
||||
if task.Method != "" {
|
||||
method = strings.ToLower(task.Method)
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"--project", projectName,
|
||||
"scheduler", "jobs", operationType,
|
||||
@@ -199,7 +205,7 @@ func (manager TasksSyncManager) getArgs(task Task, operationType string) []strin
|
||||
"--schedule", task.Schedule,
|
||||
"--uri", uri,
|
||||
"--description", description,
|
||||
"--http-method", "get",
|
||||
"--http-method", method,
|
||||
"--oidc-service-account-email", getCloudSchedulerServiceAccountEmail(),
|
||||
"--oidc-token-audience", clientId,
|
||||
}
|
||||
|
||||
@@ -80,10 +80,13 @@ steps:
|
||||
artifact_storage=$(sed -n 's/^artifactStorage: //p' "$config_file")
|
||||
service_account=$(sed -n 's/^serviceAccount: //p' "$config_file")
|
||||
cluster_val=$(sed -n 's/^cluster: //p' "$config_file")
|
||||
worker_pool=$(sed -n 's/^workerPool: //p' "$config_file")
|
||||
|
||||
sed -i "s|artifactStorage: artifactStorage|artifactStorage: $artifact_storage|" "$target_file"
|
||||
sed -i "s|serviceAccount: serviceAccount|serviceAccount: $service_account|" "$target_file"
|
||||
sed -i "s|serviceAccount: serviceAccount|serviceAccount: $service_account|" release/clouddeploy/delivery-pipeline.yaml
|
||||
sed -i "s|cluster: cluster|cluster: $cluster_val|" "$target_file"
|
||||
sed -i "s|workerPool: workerPool|workerPool: $worker_pool|" "$target_file"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -76,6 +76,7 @@ steps:
|
||||
echo "$nomulus_digest" > /workspace/nomulus_digest
|
||||
proxy_digest=$(gcloud container images list-tags gcr.io/${PROJECT_ID}/proxy \
|
||||
--format="get(digest)" --filter="tags = ${TAG_NAME}")
|
||||
echo "$proxy_digest" > /workspace/proxy_digest
|
||||
gcloud --project=${PROJECT_ID} beta container binauthz attestations \
|
||||
sign-and-create --artifact-url=gcr.io/${PROJECT_ID}/nomulus@$nomulus_digest \
|
||||
--attestor=build-attestor --attestor-project=${PROJECT_ID} \
|
||||
@@ -196,14 +197,15 @@ steps:
|
||||
echo "============================================="
|
||||
# Read the pre-fetched image digest from the workspace file
|
||||
nomulus_digest=$(cat /workspace/nomulus_digest)
|
||||
proxy_digest=$(cat /workspace/proxy_digest)
|
||||
gcloud deploy releases create "$release_name" \
|
||||
--delivery-pipeline="$pipeline" \
|
||||
--region="$region" \
|
||||
--project=${PROJECT_ID} \
|
||||
--images="nomulus=gcr.io/${PROJECT_ID}/nomulus@${nomulus_digest}" \
|
||||
--images="gcr.io/${PROJECT_ID}/nomulus=gcr.io/${PROJECT_ID}/nomulus@${nomulus_digest},gcr.io/${PROJECT_ID}/proxy=gcr.io/${PROJECT_ID}/proxy@${proxy_digest}" \
|
||||
--source=. \
|
||||
--skaffold-file=release/clouddeploy/skaffold.yaml \
|
||||
--deploy-parameters="deployed_image=gcr.io/${PROJECT_ID}/nomulus@${nomulus_digest},base_image=us-docker.pkg.dev/${PROJECT_ID}/gcr.io/nomulus"
|
||||
--deploy-parameters="deployed_image=gcr.io/${PROJECT_ID}/nomulus@${nomulus_digest},base_image=us-docker.pkg.dev/${PROJECT_ID}/gcr.io/nomulus,tag_name=${TAG_NAME},project_id=${PROJECT_ID}"
|
||||
# The tarballs and jars to upload to GCS.
|
||||
artifacts:
|
||||
objects:
|
||||
|
||||
@@ -236,11 +236,17 @@ steps:
|
||||
# partial phase manifests
|
||||
for stage in 1 5
|
||||
do
|
||||
if [ ${service} == backend ] || [ ${service} == console ]
|
||||
then
|
||||
replicas=1
|
||||
else
|
||||
replicas=${stage}
|
||||
fi
|
||||
awk 'NR==1,/^---$/ {if ($0 != "---") print}' ./jetty/kubernetes/nomulus-${env}-${service}.yaml | \
|
||||
sed s/name:\ ${service}/name:\ ${service}-partial-phase/g | \
|
||||
sed s/service:\ ${service}/deployment:\ ${service}-partial-phase/g | \
|
||||
sed s/value:\ ${service}/value:\ ${service}-partial-phase/g | \
|
||||
sed "/^spec:$/a\ replicas: ${stage}" \
|
||||
sed "/^spec:$/a\ replicas: ${replicas}" \
|
||||
> ./jetty/kubernetes/nomulus-${env}-${service}-partial-phase-${stage}.yaml
|
||||
done
|
||||
# gateway
|
||||
@@ -253,6 +259,21 @@ steps:
|
||||
sed s/SERVICE/${service}-canary/g ./jetty/kubernetes/gateway/nomulus-backend-policy-${env}.yaml \
|
||||
> ./jetty/kubernetes/gateway/nomulus-backend-policy-${env}-${service}-canary.yaml
|
||||
done
|
||||
# Generate manifests for epp-server (which doesn't use HTTP gateway routing)
|
||||
# Lowercase EPP-v2 to epp-v2 to match reserved static GCE IP names.
|
||||
sed s/GCP_PROJECT/${PROJECT_ID}/g ./jetty/kubernetes/nomulus-epp-server.yaml | \
|
||||
sed s/latest/${TAG_NAME}/g | \
|
||||
sed s/ENVIRONMENT/${env}/g | \
|
||||
sed s/EPP-v2/epp-v2/g > ./jetty/kubernetes/nomulus-${env}-epp-server.yaml
|
||||
# Generate partial-phase manifests for epp-server for Cloud Deploy canary stages
|
||||
for stage in 1 5
|
||||
do
|
||||
awk 'NR==1,/^---$/ {if ($0 != "---") print}' ./jetty/kubernetes/nomulus-${env}-epp-server.yaml | \
|
||||
sed s/name:\ epp-server/name:\ epp-server-partial-phase/g | \
|
||||
sed s/service:\ epp-server/deployment:\ epp-server-partial-phase/g | \
|
||||
sed "/^spec:$/a\ replicas: ${stage}" \
|
||||
> ./jetty/kubernetes/nomulus-${env}-epp-server-partial-phase-${stage}.yaml
|
||||
done
|
||||
done
|
||||
# Upload the Gradle binary to GCS if it does not exist and point URL in Gradle wrapper to it.
|
||||
- name: 'gcr.io/cloud-builders/gcloud'
|
||||
|
||||
@@ -5,7 +5,7 @@ This directory contains the Google Cloud Deploy configuration files for the Nomu
|
||||
## Files
|
||||
|
||||
### `delivery-pipeline.yaml`
|
||||
Defines the `DeliveryPipeline` resource named `deploy-nomulus`. It sets up the serial pipeline for rolling out changes to different targets.
|
||||
Defines the `DeliveryPipeline` resource named `deploy-nomulus` and its associated `Automation` resource (`deploy-nomulus/auto-advance-canary`). It sets up the serial pipeline for rolling out changes to different targets and automatically advancing canary rollouts.
|
||||
|
||||
### Target Configurations (e.g., `crash-target.yaml`)
|
||||
Files matching this format define the `Target` resources for Cloud Deploy. They specify the GKE cluster and other environment-specific settings for deployment.
|
||||
|
||||
@@ -6,8 +6,8 @@ metadata:
|
||||
requireApproval: true
|
||||
executionConfigs:
|
||||
- usages:
|
||||
- PREDEPLOY
|
||||
- RENDER
|
||||
- DEPLOY
|
||||
- ANALYSIS
|
||||
- POSTDEPLOY
|
||||
executionTimeout: 3600s
|
||||
@@ -16,6 +16,16 @@ executionConfigs:
|
||||
artifactStorage: artifactStorage
|
||||
# Placeholder: Replace with project number.
|
||||
serviceAccount: serviceAccount
|
||||
- usages:
|
||||
- DEPLOY
|
||||
executionTimeout: 3600s
|
||||
privatePool:
|
||||
# Placeholder: Replace with worker pool name.
|
||||
workerPool: workerPool
|
||||
# Placeholder: Replace with artifact bucket name.
|
||||
artifactStorage: artifactStorage
|
||||
# Placeholder: Replace with project number.
|
||||
serviceAccount: serviceAccount
|
||||
gke:
|
||||
# Placeholder: Replace with project ID, location, and cluster name.
|
||||
cluster: cluster
|
||||
|
||||
@@ -44,8 +44,20 @@ serialPipeline:
|
||||
args:
|
||||
- "-c"
|
||||
- |
|
||||
gcloud artifacts docker tags add $DEPLOYED_IMAGE \
|
||||
${BASE_IMAGE}:live-cd-${TARGET_ID}
|
||||
gcloud container images add-tag $DEPLOYED_IMAGE \
|
||||
${BASE_IMAGE}:live-cd-${TARGET_ID} --quiet
|
||||
- type: container
|
||||
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:stable
|
||||
env:
|
||||
TARGET_ID: ${{ target.id }}
|
||||
TAG_NAME: ${{ deploy_params['tag_name'] }}
|
||||
PROJECT_ID: ${{ deploy_params['project_id'] }}
|
||||
command: ["/bin/bash"]
|
||||
args:
|
||||
- "-c"
|
||||
- |
|
||||
gcloud storage cp gs://${PROJECT_ID}-deploy/${TAG_NAME}/cloudbuild-schema-deploy-${TARGET_ID}.yaml .
|
||||
gcloud builds submit --no-source --config=cloudbuild-schema-deploy-${TARGET_ID}.yaml
|
||||
analysis:
|
||||
# 10 minutes.
|
||||
duration: 600s
|
||||
@@ -60,6 +72,20 @@ serialPipeline:
|
||||
- phaseId: "canary-1"
|
||||
profiles: ["sandbox-partial-phase-1"]
|
||||
percentage: 10
|
||||
predeploy:
|
||||
tasks:
|
||||
- type: container
|
||||
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:stable
|
||||
env:
|
||||
TARGET_ID: ${{ target.id }}
|
||||
TAG_NAME: ${{ deploy_params['tag_name'] }}
|
||||
PROJECT_ID: ${{ deploy_params['project_id'] }}
|
||||
command: ["/bin/bash"]
|
||||
args:
|
||||
- "-c"
|
||||
- |
|
||||
gcloud storage cp gs://${PROJECT_ID}-deploy/${TAG_NAME}/cloudbuild-schema-verify-${TARGET_ID}.yaml .
|
||||
gcloud builds submit --no-source --config=cloudbuild-schema-verify-${TARGET_ID}.yaml
|
||||
analysis:
|
||||
# 10 minutes.
|
||||
duration: 600s
|
||||
@@ -90,11 +116,42 @@ serialPipeline:
|
||||
args:
|
||||
- "-c"
|
||||
- |
|
||||
gcloud artifacts docker tags add $DEPLOYED_IMAGE \
|
||||
${BASE_IMAGE}:live-cd-${TARGET_ID}
|
||||
gcloud container images add-tag $DEPLOYED_IMAGE \
|
||||
${BASE_IMAGE}:live-cd-${TARGET_ID} --quiet
|
||||
- type: container
|
||||
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:stable
|
||||
env:
|
||||
TARGET_ID: ${{ target.id }}
|
||||
TAG_NAME: ${{ deploy_params['tag_name'] }}
|
||||
PROJECT_ID: ${{ deploy_params['project_id'] }}
|
||||
command: ["/bin/bash"]
|
||||
args:
|
||||
- "-c"
|
||||
- |
|
||||
gcloud storage cp gs://${PROJECT_ID}-deploy/${TAG_NAME}/cloudbuild-schema-deploy-${TARGET_ID}.yaml .
|
||||
gcloud builds submit --no-source --config=cloudbuild-schema-deploy-${TARGET_ID}.yaml
|
||||
analysis:
|
||||
# 10 minutes.
|
||||
duration: 600s
|
||||
googleCloud:
|
||||
alertPolicyChecks:
|
||||
sandboxStableDeploymentAlertPolicyChecks
|
||||
---
|
||||
apiVersion: deploy.cloud.google.com/v1
|
||||
kind: Automation
|
||||
metadata:
|
||||
name: deploy-nomulus/auto-advance-canary
|
||||
description: Automatically advances rollouts through canary-1 phase after successful deployment and analysis.
|
||||
# Placeholder: Replace with project service account.
|
||||
serviceAccount: serviceAccount
|
||||
selector:
|
||||
targets:
|
||||
- id: crash
|
||||
- id: sandbox
|
||||
rules:
|
||||
- advanceRolloutRule:
|
||||
id: advance-canary-phases
|
||||
sourcePhases:
|
||||
- "canary-1"
|
||||
wait: 0m
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ metadata:
|
||||
requireApproval: true
|
||||
executionConfigs:
|
||||
- usages:
|
||||
- PREDEPLOY
|
||||
- RENDER
|
||||
- DEPLOY
|
||||
- ANALYSIS
|
||||
- POSTDEPLOY
|
||||
executionTimeout: 3600s
|
||||
@@ -15,6 +15,16 @@ executionConfigs:
|
||||
artifactStorage: artifactStorage
|
||||
# Placeholder: Replace with project number.
|
||||
serviceAccount: serviceAccount
|
||||
- usages:
|
||||
- DEPLOY
|
||||
executionTimeout: 3600s
|
||||
privatePool:
|
||||
# Placeholder: Replace with worker pool name.
|
||||
workerPool: workerPool
|
||||
# Placeholder: Replace with artifact bucket name.
|
||||
artifactStorage: artifactStorage
|
||||
# Placeholder: Replace with project number.
|
||||
serviceAccount: serviceAccount
|
||||
gke:
|
||||
# Placeholder: Replace with project ID, location, and cluster name.
|
||||
cluster: cluster
|
||||
|
||||
@@ -11,6 +11,7 @@ profiles:
|
||||
- ../../jetty/kubernetes/nomulus-crash-console.yaml
|
||||
- ../../jetty/kubernetes/nomulus-crash-frontend.yaml
|
||||
- ../../jetty/kubernetes/nomulus-crash-pubapi.yaml
|
||||
- ../../jetty/kubernetes/nomulus-crash-epp-server.yaml
|
||||
deploy:
|
||||
kubectl: { }
|
||||
- name: crash-partial-phase-1
|
||||
@@ -20,6 +21,7 @@ profiles:
|
||||
- ../../jetty/kubernetes/nomulus-crash-console-partial-phase-1.yaml
|
||||
- ../../jetty/kubernetes/nomulus-crash-frontend-partial-phase-1.yaml
|
||||
- ../../jetty/kubernetes/nomulus-crash-pubapi-partial-phase-1.yaml
|
||||
- ../../jetty/kubernetes/nomulus-crash-epp-server-partial-phase-1.yaml
|
||||
deploy:
|
||||
kubectl: { }
|
||||
- name: crash-partial-phase-5
|
||||
@@ -29,6 +31,7 @@ profiles:
|
||||
- ../../jetty/kubernetes/nomulus-crash-console-partial-phase-5.yaml
|
||||
- ../../jetty/kubernetes/nomulus-crash-frontend-partial-phase-5.yaml
|
||||
- ../../jetty/kubernetes/nomulus-crash-pubapi-partial-phase-5.yaml
|
||||
- ../../jetty/kubernetes/nomulus-crash-epp-server-partial-phase-5.yaml
|
||||
deploy:
|
||||
kubectl: { }
|
||||
- name: sandbox
|
||||
@@ -38,6 +41,7 @@ profiles:
|
||||
- ../../jetty/kubernetes/nomulus-sandbox-console.yaml
|
||||
- ../../jetty/kubernetes/nomulus-sandbox-frontend.yaml
|
||||
- ../../jetty/kubernetes/nomulus-sandbox-pubapi.yaml
|
||||
- ../../jetty/kubernetes/nomulus-sandbox-epp-server.yaml
|
||||
deploy:
|
||||
kubectl: { }
|
||||
- name: sandbox-partial-phase-1
|
||||
@@ -47,6 +51,7 @@ profiles:
|
||||
- ../../jetty/kubernetes/nomulus-sandbox-console-partial-phase-1.yaml
|
||||
- ../../jetty/kubernetes/nomulus-sandbox-frontend-partial-phase-1.yaml
|
||||
- ../../jetty/kubernetes/nomulus-sandbox-pubapi-partial-phase-1.yaml
|
||||
- ../../jetty/kubernetes/nomulus-sandbox-epp-server-partial-phase-1.yaml
|
||||
deploy:
|
||||
kubectl: { }
|
||||
- name: sandbox-partial-phase-5
|
||||
@@ -56,5 +61,6 @@ profiles:
|
||||
- ../../jetty/kubernetes/nomulus-sandbox-console-partial-phase-5.yaml
|
||||
- ../../jetty/kubernetes/nomulus-sandbox-frontend-partial-phase-5.yaml
|
||||
- ../../jetty/kubernetes/nomulus-sandbox-pubapi-partial-phase-5.yaml
|
||||
- ../../jetty/kubernetes/nomulus-sandbox-epp-server-partial-phase-5.yaml
|
||||
deploy:
|
||||
kubectl: { }
|
||||
@@ -33,14 +33,11 @@ public final class ProxyHttpHeaders {
|
||||
/** HTTP header name used to pass the Registrar Id from the proxy to Nomulus. */
|
||||
public static final String REGISTRAR_ID = "Nomulus-Registrar-Id";
|
||||
|
||||
/**
|
||||
* Fallback HTTP header name used to pass the client IP address from the proxy to Nomulus.
|
||||
*
|
||||
* <p>Note that Java 17's servlet implementation may inject some seemingly unrelated addresses
|
||||
* into this header. We only use this as a fallback so the proxy can transition to use the above
|
||||
* header that should not be interfered with.
|
||||
*/
|
||||
/** Fallback HTTP header name used to pass the client IP address from the proxy to Nomulus. */
|
||||
public static final String FALLBACK_IP_ADDRESS = HttpHeaders.X_FORWARDED_FOR;
|
||||
|
||||
/** HTTP header name used to pass the authenticated Registrar Id from Nomulus to GKE. */
|
||||
public static final String LOGGED_IN_REGISTRAR = "Nomulus-Logged-In-Registrar";
|
||||
|
||||
private ProxyHttpHeaders() {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user