Compare commits

..
Author SHA1 Message Date
Pavlo TkachandGitHub 72c610688a Remove canary release build for epp server (#3193) 2026-08-01 02:02:07 +00:00
Ben McIlwainandGitHub b5ae51a036 Do not apply reserved list to domain restore (#3191)
Per ICANN's Expired Registration Recovery Policy, all gTLD registries must
offer a Redemption Grace Period (RGP) of 30 days during which deleted
domains may be restored. Registry reservation lists should not block
domain restore commands during the RGP.

This change removes the reserved list check in DomainRestoreRequestFlow,
reverting the behavior originally added in CL 72341125 (July 2014) that
explicitly disallowed restoring reserved domains. Unit tests have been
updated to confirm restoring reserved domains succeeds for standard
registrar accounts.

BUG=b/539548743
TAG=agy
CONV=d5dff534-f924-4bba-a58e-74091dc5f496
2026-07-31 17:23:37 +00:00
gbrodmanandGitHub 92b684d7ec Only forbid DISABLED registrars in the console (#3188)
PENDING registrars may just need to add contacts etc in order to be
enabled, and they should be able to use the console to do so.
2026-07-30 20:57:43 +00:00
Pavlo TkachandGitHub 49cecf6776 Update cloud build release with new epp service (#3189) 2026-07-30 19:49:40 +00:00
gbrodmanandGitHub 4cc3fc9cd2 Use native query for registrar-users console user query (#3182)
This means we don't have to load all users and filter them out later. In
practice this doesn't matter because the user table is relatively small
(a few hundred) but 1. who knows what can happen in the future? 2. this
makes the code analysis tools happier
2026-07-29 22:30:07 +00:00
Juan CelhayandGitHub 74f441765e Fix image replacement in cd (#3186) 2026-07-29 19:37:12 +00:00
gbrodmanandGitHub 0ce83c8b2d Change syncRemoteTask scheduled task to be a POST (#3185)
Turns out before we were using GET for everything. The action is, and
should remain, a POST on the back end so let's call that.
2026-07-29 18:52:59 +00:00
Juan CelhayandGitHub fc4246ea95 Add pre- and post- deploy tasks for SQL schema verification and deployment (#3184) 2026-07-29 02:00:51 +00:00
Ben McIlwainandGitHub 29def8d78d Make losing client ID optional in bulk transfers (#3169)
When executing bulk domain transfers with an explicit list of domain names or
a domain names file, enforcing by losing registrar ID is often unnecessary
and redundant (b/537294004).

This commit makes --losing_registrar_id an optional command-line parameter
and updates BulkDomainTransferAction and BatchModule to handle an optional
losing sponsor ID. Existing behavior is preserved when the parameter is
explicitly supplied.

BUG= http://b/537294004
2026-07-29 02:00:39 +00:00
Pavlo TkachandGitHub 0c79414a31 Harden EPP connection limits and idle timeouts (#3179)
This change hardens the EPP GKE entry point against a connection hoarding Denial of Service (DoS) vulnerability (b/534930905).
We resolve this by restricting pre-login connections to a short idle timeout and enforcing pod-local connection caps:
1. Removed certificate-based connection quota tracking. IP limits are now enforced pre-login, and authenticated Registrar ID limits are enforced post-login.
2. Implemented a 10-second scheduled timeout task during the pre-login phase. If the client does not successfully authenticate within 10 seconds of TLS handshake completion, they are disconnected.
3. Added a new response header 'Nomulus-Logged-In-Registrar' set by the backend EppRequestHandler upon successful login. EppServiceHandler monitors this header inline to perform registrar quota upgrades and cancel the pre-login timeout task.
4. Hardened EppProxyProtocolHandler to validate incoming IPs from the PROXY protocol header to prevent IP spoofing and smuggling, falling back to the TCP source IP on validation failures.
2026-07-28 19:59:51 +00:00
gbrodmanandGitHub ea7d5d4a5e Enforce OT&E accounts existing in the console gSuite domain (#3178)
This is non-production so it's not a huge deal but in general, we should
restrict the OT&E users so that they only exist within the workspace
that we control. Other users that are created using the console already
follow this format.

b/534932209 for more info
2026-07-28 19:24:09 +00:00
Juan CelhayandGitHub 553fa1dc14 Use environment custom worker pool for deploy step in cloud deploy (#3180) 2026-07-28 19:08:26 +00:00
Juan CelhayandGitHub c36087dc93 generate servertrids using securerandom behind feature flag (#3163) 2026-07-28 16:00:55 +00:00
36 changed files with 745 additions and 218 deletions
@@ -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++;
@@ -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"]
@@ -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>
@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkState;
import static java.nio.charset.StandardCharsets.US_ASCII;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.InetAddresses;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
@@ -78,9 +79,17 @@ public class EppProxyProtocolHandler extends ByteToMessageDecoder {
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());
String parsedIP = headerArray[2];
if (InetAddresses.isInetAddress(parsedIP)) {
remoteIP = parsedIP;
logger.atFine().log(
"Header parsed, using %s as remote IP for channel %s", remoteIP, ctx.channel());
} else {
logger.atWarning().log(
"Invalid IP address in PROXY header: %s, falling back to source IP for channel %s",
parsedIP, ctx.channel());
remoteIP = getSourceIP(ctx);
}
// 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
@@ -42,10 +42,12 @@ 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.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;
@@ -73,14 +75,17 @@ public class EppServiceHandler extends SimpleChannelInboundHandler<ByteBuf> {
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 +96,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 +117,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 +125,36 @@ public class EppServiceHandler extends SimpleChannelInboundHandler<ByteBuf> {
}
private void onSslHandshakeComplete(ChannelHandlerContext ctx, X509Certificate cert) {
if (!ctx.channel().isActive()) {
return;
}
sslClientCertificateHash = getCertificateHash(cert);
clientAddress = ctx.channel().attr(REMOTE_ADDRESS_KEY).get();
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 +170,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 +203,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 +231,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());
@@ -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;
@@ -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) {
@@ -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);
}
}
@@ -19,11 +19,23 @@ 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.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;
class EppProxyProtocolHandlerTest {
private EmbeddedChannel createChannel(EppProxyProtocolHandler handler) {
return new EmbeddedChannel(handler) {
@Override
public SocketAddress remoteAddress() {
return new InetSocketAddress(InetAddress.getLoopbackAddress(), 12345);
}
};
}
@Test
void testProxyProtocol_parsesValidHeader() {
EppProxyProtocolHandler handler = new EppProxyProtocolHandler();
@@ -39,6 +51,21 @@ class EppProxyProtocolHandlerTest {
assertThat(channel.pipeline().get(EppProxyProtocolHandler.class)).isNull();
}
@Test
void testProxyProtocol_invalidIP_fallsBackToSource() {
EppProxyProtocolHandler handler = new EppProxyProtocolHandler();
EmbeddedChannel channel = createChannel(handler);
String proxyHeader = "PROXY TCP4 invalid_ip_address 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("127.0.0.1"); // Falls back to mocked remoteAddress
assertThat(channel.pipeline().get(EppProxyProtocolHandler.class)).isNull();
}
@Test
void testProxyProtocol_unknownHeader() {
EppProxyProtocolHandler handler = new EppProxyProtocolHandler();
@@ -57,7 +84,7 @@ class EppProxyProtocolHandlerTest {
@Test
void testProxyProtocol_noHeader_notProxied() {
EppProxyProtocolHandler handler = new EppProxyProtocolHandler();
EmbeddedChannel channel = new EmbeddedChannel(handler);
EmbeddedChannel channel = createChannel(handler);
String normalData = "NOT_A_PROXY_HEADER";
ByteBuf buffer = Unpooled.wrappedBuffer(normalData.getBytes(StandardCharsets.US_ASCII));
@@ -65,8 +92,7 @@ class EppProxyProtocolHandlerTest {
channel.writeInbound(buffer);
String remoteAddress = channel.attr(EppProxyProtocolHandler.REMOTE_ADDRESS_KEY).get();
// In EmbeddedChannel without remoteAddress mock, getSourceIP returns null
assertThat(remoteAddress).isNull();
assertThat(remoteAddress).isEqualTo("127.0.0.1"); // Falls back to mocked remoteAddress
assertThat(channel.pipeline().get(EppProxyProtocolHandler.class)).isNull();
ByteBuf passedOn = channel.readInbound();
@@ -17,11 +17,15 @@ 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 java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertNotNull;
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,17 @@ 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.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 +70,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 +91,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 +132,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 +186,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 +231,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 +267,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 +280,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 +332,64 @@ 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();
assertNotNull(timeoutTask);
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();
}
}
@@ -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");
@@ -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(
@@ -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 =
@@ -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)
);
@@ -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,
}
+2
View File
@@ -80,10 +80,12 @@ 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|cluster: cluster|cluster: $cluster_val|" "$target_file"
sed -i "s|workerPool: workerPool|workerPool: $worker_pool|" "$target_file"
fi
fi
done
+1 -1
View File
@@ -200,7 +200,7 @@ steps:
--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}" \
--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"
+4
View File
@@ -253,6 +253,10 @@ 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)
sed s/GCP_PROJECT/${PROJECT_ID}/g ./jetty/kubernetes/nomulus-epp-server.yaml | \
sed s/latest/${TAG_NAME}/g | \
sed s/ENVIRONMENT/${env}/g > ./jetty/kubernetes/nomulus-${env}-epp-server.yaml
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'
+11 -1
View File
@@ -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
@@ -14,6 +14,17 @@ serialPipeline:
- phaseId: "canary-1"
profiles: ["crash-partial-phase-1"]
percentage: 10
predeploy:
tasks:
- type: container
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:stable
env:
TARGET_ID: ${{ target.id }}
command: ["/bin/bash"]
args:
- "-c"
- |
gcloud builds submit --config=release/cloudbuild-schema-verify-${TARGET_ID}.yaml
analysis:
# 10 minutes.
duration: 600s
@@ -46,6 +57,15 @@ serialPipeline:
- |
gcloud artifacts docker tags add $DEPLOYED_IMAGE \
${BASE_IMAGE}:live-cd-${TARGET_ID}
- type: container
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:stable
env:
TARGET_ID: ${{ target.id }}
command: ["/bin/bash"]
args:
- "-c"
- |
gcloud builds submit --config=release/cloudbuild-schema-deploy-${TARGET_ID}.yaml
analysis:
# 10 minutes.
duration: 600s
@@ -60,6 +80,17 @@ 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 }}
command: ["/bin/bash"]
args:
- "-c"
- |
gcloud builds submit --config=release/cloudbuild-schema-verify-${TARGET_ID}.yaml
analysis:
# 10 minutes.
duration: 600s
@@ -92,6 +123,15 @@ serialPipeline:
- |
gcloud artifacts docker tags add $DEPLOYED_IMAGE \
${BASE_IMAGE}:live-cd-${TARGET_ID}
- type: container
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:stable
env:
TARGET_ID: ${{ target.id }}
command: ["/bin/bash"]
args:
- "-c"
- |
gcloud builds submit --config=release/cloudbuild-schema-deploy-${TARGET_ID}.yaml
analysis:
# 10 minutes.
duration: 600s
+11 -1
View File
@@ -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
@@ -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() {}
}