Compare commits

..
Author SHA1 Message Date
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
gbrodmanandGitHub bc44e095d4 Forbid subordinate host changes when superord domain is locked (#3171)
This is not explicitly called out that I could find in the RFCs, however
it is very clear from the RFCs that hosts are subordinate to their
domains and it stands to reason that updates to a subordinate host are
updates to the superordinate domain.

Section 1.1 of RFC 5732 specifies:

```
host name "ns1.example.com" has a subordinate relationship to domain
name "example.com". EPP actions (such as object transfers) that do not
preserve this relationship MUST be explicitly disallowed.
```

Allowing host updates (e.g. renames) opens up situations where this
relationship could be severed. We add analogous prohibitions on deletion
and creation of subordinate hosts as well.

b/534930957
2026-07-24 19:27:00 +00:00
gbrodmanandGitHub 355e8ba423 Don't claim that only one abuse contact is allowed (#3173)
nowhere in the RFCs does it say that only one contact is allowed, and
indeed there are many situations where it's actually an array of
contacts. We have a bunch of registrars that already have multiple abuse
contacts. I'm not sure why the comment originally claimed this, but it's
from years and years ago.

b/534931561
2026-07-24 17:01:09 +00:00
gbrodmanandGitHub dca5a010d5 Update registrar-update auth block for allowed TLDs + rlock (#3175)
Verified in support docs 2.19 and 2.33 that agents should have the
ability to edit allowed TLDs but should not have the ability to edit
whether registry lock is allowed. This reflects that.

b/534931470
2026-07-24 15:52:51 +00:00
gbrodmanandGitHub 147e7dabcb Also skip inline style optimization in sandbox (#3177) 2026-07-24 15:52:44 +00:00
gbrodmanandGitHub 0b8025d3db Disallow adding ADMIN contacts via console (#3160)
this is a potential security issue if we give users who only have
EDIT_REGISTRAR_DETAILS access to become admin PoCs, which can have a
different set of permissions.
2026-07-24 15:11:06 +00:00
Ben McIlwainandGitHub 9c4a558d7b Censor domain authcode by default in tools (#3170)
When inspecting domain entities with tools like GetDomainCommand, displaying
plaintext authentication codes is unnecessary and presents a security risk
for credential exposure in logs and bug ticket comments (b/537293980).

This commit modifies GetDomainCommand to redact sensitive authcodes by
default using an in-memory entity builder snapshot while preserving database
records. An optional --show_authcode flag with arity 1 is introduced to
explicitly display authcodes when required for transfer authorization.

BUG= http://b/537293980
2026-07-24 14:26:38 +00:00
Ben McIlwainandGitHub 7347dbf762 Add arity to registrar_request boolean flags (#3168)
Define arity 1 on requestedByRegistrar options in BulkDomainTransferCommand
and DeleteDomainCommand to accept explicit false arguments without parsing
failures. Up-to-date tests verify space-separated and equals argument syntax.

BUG= http://b/537308816
2026-07-24 14:26:34 +00:00
gbrodmanandGitHub 53b6044ff8 Fix CSP directive in production (#3176)
In prod, when loading the console, we were failing to get some
scripts/styles with the error "Executing
  inline event handler violates the following Content Security Policy directive 'script-src
  'self''. Either the 'unsafe-inline' keyword, a hash ('sha256-...'), or a nonce ('nonce-
  ...') is required to enable inline execution. Note that hashes do not apply to event
  handlers, style attributes and javascript: navigations unless the 'unsafe-hashes' keyword
  is present. The action has been blocked."

We fix this by disabling inline-critical optimization for prod in the
angular file.

In addition, the web.xml header values are comma-separated -- we forgot
one comma before.
2026-07-23 21:00:18 +00:00
Juan CelhayandGitHub 12ab7f47e2 Fix oversized postal-address fields (#3165) 2026-07-23 14:29:54 +00:00
gbrodmanandGitHub d3a1d0709e Only grant IAP access to the console for new users (#3167)
Currently we grant users IAP_SECURED_WEB_APP_USER_ROLE access to the
entire project upon creation if there is no preexisting group set up for
them to use. These users only need to be able to access the console, so
we should restrict it to the console.

If there is a group (which we do have in our prod + sandbox
configuration) then we just add them to that group instead and that
group controls all the privileges. So this is a no-op for our setup.

We prioritize the group -- if both the group and the IAP service ID are
specified, we'll still just add them to the group

b/534931170
2026-07-22 16:53:41 +00:00
67 changed files with 1504 additions and 371 deletions
+16
View File
@@ -61,6 +61,14 @@
"with": "src/environments/environment.prod.ts"
}
],
"optimization": {
"scripts": true,
"styles": {
"minify": true,
"inlineCritical": false
},
"fonts": true
},
"outputHashing": "all"
},
"sandbox": {
@@ -82,6 +90,14 @@
"with": "src/environments/environment.sandbox.ts"
}
],
"optimization": {
"scripts": true,
"styles": {
"minify": true,
"inlineCritical": false
},
"fonts": true
},
"outputHashing": "all"
},
"crash": {
@@ -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++;
@@ -165,6 +165,12 @@ public final class RegistryConfig {
return config.misc.isEmailSendingEnabled;
}
@Provides
@Config("consoleIapServiceId")
public static Optional<String> provideConsoleIapServiceId(RegistryConfigSettings config) {
return Optional.ofNullable(config.registrarConsole.consoleIapServiceId);
}
/**
* The e-mail address for general support. Used in the "contact-us" section of the registrar
* console.
@@ -1581,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) {
@@ -1588,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
@@ -187,6 +187,7 @@ public class RegistryConfigSettings {
/** Configuration for the web-based registrar console. */
public static class RegistrarConsole {
public String consoleIapServiceId;
public String dumFileName;
public String supportPhoneNumber;
public String supportEmailAddress;
@@ -216,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;
}
@@ -390,6 +390,11 @@ rde:
sshIdentityEmailAddress: rde@example.com
registrarConsole:
# IAP service ID of the HTTP(s) load balancer that serves the console-api backend.
# Users created in the console will be bound only to this service to limit access.
# This is only applicable to non-group-based auth (if there is no consoleUserGroupEmailAddress)
consoleIapServiceId: null
# DUM download file name, excluding the extension
dumFileName: dum_file_name
@@ -452,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());
@@ -20,6 +20,7 @@ import static google.registry.flows.ResourceFlowUtils.verifyResourceDoesNotExist
import static google.registry.flows.host.HostFlowUtils.lookupSuperordinateDomain;
import static google.registry.flows.host.HostFlowUtils.validateHostName;
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainNotInPendingDelete;
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainNotServerUpdateProhibited;
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainOwnership;
import static google.registry.model.EppResourceUtils.createRepoId;
import static google.registry.model.reporting.HistoryEntry.Type.HOST_CREATE;
@@ -73,6 +74,7 @@ import java.util.Optional;
* @error {@link HostFlowUtils.HostNameNotPunyCodedException}
* @error {@link HostFlowUtils.SuperordinateDomainDoesNotExistException}
* @error {@link HostFlowUtils.SuperordinateDomainInPendingDeleteException}
* @error {@link HostFlowUtils.SuperordinateDomainServerUpdateProhibitedException}
* @error {@link SubordinateHostMustHaveIpException}
* @error {@link UnexpectedExternalHostIpException}
*/
@@ -107,6 +109,7 @@ public final class HostCreateFlow implements MutatingFlow {
Optional<Domain> superordinateDomain =
lookupSuperordinateDomain(validateHostName(targetId), now);
verifySuperordinateDomainNotInPendingDelete(superordinateDomain.orElse(null));
verifySuperordinateDomainNotServerUpdateProhibited(superordinateDomain.orElse(null));
verifySuperordinateDomainOwnership(registrarId, superordinateDomain.orElse(null));
boolean willBeSubordinate = superordinateDomain.isPresent();
boolean hasIpAddresses = !isNullOrEmpty(command.getInetAddresses());
@@ -22,6 +22,7 @@ import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence;
import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses;
import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
import static google.registry.flows.host.HostFlowUtils.validateHostName;
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainNotServerUpdateProhibited;
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
@@ -33,7 +34,7 @@ import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.MutatingFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.EppResource;
import google.registry.model.domain.Domain;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid;
@@ -62,6 +63,7 @@ import java.time.Instant;
* @error {@link HostFlowUtils.HostNameNotLowerCaseException}
* @error {@link HostFlowUtils.HostNameNotNormalizedException}
* @error {@link HostFlowUtils.HostNameNotPunyCodedException}
* @error {@link HostFlowUtils.SuperordinateDomainServerUpdateProhibitedException}
*/
@ReportingSpec(ActivityReportField.HOST_DELETE)
public final class HostDeleteFlow implements MutatingFlow {
@@ -90,12 +92,15 @@ public final class HostDeleteFlow implements MutatingFlow {
if (!isSuperuser) {
verifyNoDisallowedStatuses(existingHost, DELETE_PROHIBITED_STATUSES);
// Hosts transfer with their superordinate domains, so for hosts with a superordinate domain,
// the client id, needs to be read off of it.
EppResource owningResource =
existingHost.isSubordinate()
? tm().loadByKey(existingHost.getSuperordinateDomain()).cloneProjectedAtTime(now)
: existingHost;
verifyResourceOwnership(registrarId, owningResource);
// the client id needs to be read off of it.
if (existingHost.isSubordinate()) {
Domain superordinateDomain =
tm().loadByKey(existingHost.getSuperordinateDomain()).cloneProjectedAtTime(now);
verifyResourceOwnership(registrarId, superordinateDomain);
verifySuperordinateDomainNotServerUpdateProhibited(superordinateDomain);
} else {
verifyResourceOwnership(registrarId, existingHost);
}
}
Host newHost = existingHost.asBuilder().setStatusValues(null).setDeletionTime(now).build();
if (existingHost.isSubordinate()) {
@@ -174,6 +174,31 @@ public class HostFlowUtils {
}
}
/**
* Ensure that the superordinate domain does not have {@code serverUpdateProhibited} status.
*
* <p>Subordinate hosts publish glue A/AAAA records in the TLD zone on behalf of their
* superordinate domain. If the superordinate domain is registry-locked (carries {@link
* StatusValue#SERVER_UPDATE_PROHIBITED}), non-superuser modifications to its subordinate hosts
* must be rejected as well; otherwise a compromised sponsoring registrar could hijack DNS for a
* locked domain by swapping the glue addresses of its in-bailiwick nameservers.
*/
static void verifySuperordinateDomainNotServerUpdateProhibited(Domain superordinateDomain)
throws EppException {
if ((superordinateDomain != null)
&& superordinateDomain.getStatusValues().contains(StatusValue.SERVER_UPDATE_PROHIBITED)) {
throw new SuperordinateDomainServerUpdateProhibitedException();
}
}
/** Superordinate domain for this hostname has serverUpdateProhibited status. */
static class SuperordinateDomainServerUpdateProhibitedException
extends StatusProhibitsOperationException {
public SuperordinateDomainServerUpdateProhibitedException() {
super("Superordinate domain for this hostname has serverUpdateProhibited status");
}
}
/** Host names are limited to 253 characters. */
static class HostNameTooLongException extends ParameterValueRangeErrorException {
public HostNameTooLongException() {
@@ -28,6 +28,7 @@ import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
import static google.registry.flows.host.HostFlowUtils.lookupSuperordinateDomain;
import static google.registry.flows.host.HostFlowUtils.validateHostName;
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainNotInPendingDelete;
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainNotServerUpdateProhibited;
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainOwnership;
import static google.registry.model.reporting.HistoryEntry.Type.HOST_UPDATE;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
@@ -98,6 +99,7 @@ import java.util.Optional;
* @error {@link HostFlowUtils.InvalidHostNameException}
* @error {@link HostFlowUtils.SuperordinateDomainDoesNotExistException}
* @error {@link HostFlowUtils.SuperordinateDomainInPendingDeleteException}
* @error {@link HostFlowUtils.SuperordinateDomainServerUpdateProhibitedException}
* @error {@link CannotAddIpToExternalHostException}
* @error {@link CannotRemoveSubordinateHostLastIpException}
* @error {@link CannotRenameExternalHostException}
@@ -152,7 +154,12 @@ public final class HostUpdateFlow implements MutatingFlow {
verifySuperordinateDomainNotInPendingDelete(newSuperordinateDomain.orElse(null));
EppResource owningResource = firstNonNull(oldSuperordinateDomain, existingHost);
verifyUpdateAllowed(
command, existingHost, newSuperordinateDomain.orElse(null), owningResource, isHostRename);
command,
existingHost,
oldSuperordinateDomain,
newSuperordinateDomain.orElse(null),
owningResource,
isHostRename);
if (isHostRename && ForeignKeyUtils.loadKey(Host.class, newHostName, now).isPresent()) {
throw new HostAlreadyExistsException(newHostName);
}
@@ -211,30 +218,38 @@ public final class HostUpdateFlow implements MutatingFlow {
private void verifyUpdateAllowed(
Update command,
Host existingHost,
Domain oldSuperordinateDomain,
Domain newSuperordinateDomain,
EppResource owningResource,
boolean isHostRename)
throws EppException {
if (!isSuperuser) {
// Verify that the host belongs to this registrar, either directly or because it is currently
// subordinate to a domain owned by this registrar.
verifyResourceOwnership(registrarId, owningResource);
if (isHostRename && !existingHost.isSubordinate()) {
throw new CannotRenameExternalHostException();
}
// Verify that the new superordinate domain belongs to this registrar.
verifySuperordinateDomainOwnership(registrarId, newSuperordinateDomain);
ImmutableSet<StatusValue> statusesToAdd = command.getInnerAdd().getStatusValues();
ImmutableSet<StatusValue> statusesToRemove = command.getInnerRemove().getStatusValues();
// If the resource is marked with clientUpdateProhibited, and this update does not clear that
// status, then the update must be disallowed.
if (existingHost.getStatusValues().contains(StatusValue.CLIENT_UPDATE_PROHIBITED)
&& !statusesToRemove.contains(StatusValue.CLIENT_UPDATE_PROHIBITED)) {
throw new ResourceHasClientUpdateProhibitedException();
}
verifyAllStatusesAreClientSettable(union(statusesToAdd, statusesToRemove));
}
verifyNoDisallowedStatuses(existingHost, DISALLOWED_STATUSES);
if (isSuperuser) {
return;
}
// Verify that the host belongs to this registrar, either directly or because it is currently
// subordinate to a domain owned by this registrar.
verifyResourceOwnership(registrarId, owningResource);
if (isHostRename && !existingHost.isSubordinate()) {
throw new CannotRenameExternalHostException();
}
// Verify that the new superordinate domain belongs to this registrar.
verifySuperordinateDomainOwnership(registrarId, newSuperordinateDomain);
// Subordinate hosts inherit the registry-lock protection of their superordinate domain: if
// either the current or the target superordinate domain carries SERVER_UPDATE_PROHIBITED,
// non-superuser updates (including glue IP changes and renames) must be rejected so that a
// compromised sponsoring registrar cannot hijack DNS for a registry-locked domain.
verifySuperordinateDomainNotServerUpdateProhibited(oldSuperordinateDomain);
verifySuperordinateDomainNotServerUpdateProhibited(newSuperordinateDomain);
ImmutableSet<StatusValue> statusesToAdd = command.getInnerAdd().getStatusValues();
ImmutableSet<StatusValue> statusesToRemove = command.getInnerRemove().getStatusValues();
// If the resource is marked with clientUpdateProhibited, and this update does not clear that
// status, then the update must be disallowed.
if (existingHost.getStatusValues().contains(StatusValue.CLIENT_UPDATE_PROHIBITED)
&& !statusesToRemove.contains(StatusValue.CLIENT_UPDATE_PROHIBITED)) {
throw new ResourceHasClientUpdateProhibitedException();
}
verifyAllStatusesAreClientSettable(union(statusesToAdd, statusesToRemove));
}
private static void verifyHasIpsIffIsExternal(Update command, Host existingHost, Host newHost)
@@ -197,8 +197,7 @@ public final class OteAccountBuilder {
registrars.stream()
.collect(
toImmutableMap(
Registrar::getRegistrarId,
registrar -> RegistrarRole.ACCOUNT_MANAGER)))
Registrar::getRegistrarId, _ -> RegistrarRole.ACCOUNT_MANAGER)))
.build())
.build());
return this;
@@ -265,10 +264,18 @@ public final class OteAccountBuilder {
/** Grants the users permission to pass IAP. */
public void grantIapPermission(
Optional<String> groupEmailAddress, CloudTasksUtils cloudTasksUtils, IamClient iamClient) {
Optional<String> groupEmailAddress,
Optional<String> consoleIapServiceId,
CloudTasksUtils cloudTasksUtils,
IamClient iamClient) {
for (User user : users) {
User.grantIapPermission(
user.getEmailAddress(), groupEmailAddress, cloudTasksUtils, null, iamClient);
user.getEmailAddress(),
groupEmailAddress,
consoleIapServiceId,
cloudTasksUtils,
null,
iamClient);
}
}
@@ -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;
@@ -86,6 +86,7 @@ public class User extends UpdateAutoTimestampEntity implements Buildable {
public static void grantIapPermission(
String emailAddress,
Optional<String> groupEmailAddress,
Optional<String> consoleIapServiceId,
@Nullable CloudTasksUtils cloudTasksUtils,
@Nullable ServiceConnection connection,
IamClient iamClient) {
@@ -93,14 +94,14 @@ public class User extends UpdateAutoTimestampEntity implements Buildable {
return;
}
checkArgument(
cloudTasksUtils != null || connection != null,
"At least one of cloudTasksUtils or connection must be set");
cloudTasksUtils == null ^ connection == null,
"Precisely one of cloudTasksUtils or connection can be set");
checkArgument(
cloudTasksUtils == null || connection == null,
"Only one of cloudTasksUtils or connection can be set");
groupEmailAddress.isPresent() || consoleIapServiceId.isPresent(),
"At least one of groupEmailAddress or consoleIapServiceId must be present");
if (groupEmailAddress.isEmpty()) {
logger.atInfo().log("Granting IAP role to user %s", emailAddress);
iamClient.addBinding(emailAddress, IAP_SECURED_WEB_APP_USER_ROLE);
iamClient.addBinding(emailAddress, IAP_SECURED_WEB_APP_USER_ROLE, consoleIapServiceId.get());
} else {
logger.atInfo().log("Adding %s to group %s", emailAddress, groupEmailAddress.get());
if (cloudTasksUtils != null) {
@@ -129,11 +130,8 @@ public class User extends UpdateAutoTimestampEntity implements Buildable {
return;
}
checkArgument(
cloudTasksUtils != null || connection != null,
"At least one of cloudTasksUtils or connection must be set");
checkArgument(
cloudTasksUtils == null || connection == null,
"Only one of cloudTasksUtils or connection can be set");
cloudTasksUtils == null ^ connection == null,
"Precisely one of cloudTasksUtils or connection can be set");
if (groupEmailAddress.isEmpty()) {
logger.atInfo().log("Removing IAP role from user %s", emailAddress);
iamClient.removeBinding(emailAddress, IAP_SECURED_WEB_APP_USER_ROLE);
@@ -49,6 +49,10 @@ import java.util.stream.Stream;
* also matches the "addrType" type from <a
* href="http://tools.ietf.org/html/draft-lozano-tmch-smd">Mark and Signed Mark Objects Mapping</a>.
*
* <p>The lengths of the fields are limited to match the constraints defined in XSD schemas like
* {@code rde-registrar.xsd}. Specifically, {@code zip} is limited to 16 characters and {@code
* city}, {@code state}, and each {@code street} line are limited to 255 characters.
*
* @see google.registry.model.mark.MarkAddress
* @see google.registry.model.registrar.RegistrarAddress
*/
@@ -169,11 +173,20 @@ public class Address extends ImmutableObject
street == null || (!street.isEmpty() && street.size() <= 3),
"Street address must have [1-3] lines: %s",
street);
//noinspection ConstantConditions
if (street != null) {
checkArgument(
street.stream().noneMatch(String::isEmpty),
"Street address cannot contain empty string: %s",
street);
checkArgument(
street.stream().allMatch(s -> s.length() <= 255),
"Street address lines cannot be longer than 255 characters");
}
checkArgument(
street == null || street.stream().noneMatch(String::isEmpty),
"Street address cannot contain empty string: %s",
street);
city == null || city.length() <= 255, "City cannot be longer than 255 characters");
checkArgument(
state == null || state.length() <= 255, "State cannot be longer than 255 characters");
checkArgument(zip == null || zip.length() <= 16, "Zip cannot be longer than 16 characters");
checkArgument(
countryCode == null || countryCode.length() == 2,
"Country code should be a 2 character string");
@@ -196,9 +209,9 @@ public class Address extends ImmutableObject
public Builder<T> setStreet(ImmutableList<String> street) {
getInstance().street = street;
getInstance().streetLine1 = street.get(0);
getInstance().streetLine2 = street.size() >= 2 ? street.get(1) : null;
getInstance().streetLine3 = street.size() == 3 ? street.get(2) : null;
getInstance().streetLine1 = (street != null && street.size() >= 1) ? street.get(0) : null;
getInstance().streetLine2 = (street != null && street.size() >= 2) ? street.get(1) : null;
getInstance().streetLine3 = (street != null && street.size() == 3) ? street.get(2) : null;
return this;
}
@@ -631,7 +631,7 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
}
/**
* Returns the {@link RegistrarPoc} that is the RDAP abuse contact for this registrar, or empty if
* Returns a {@link RegistrarPoc} that is the RDAP abuse contact for this registrar, or empty if
* one does not exist.
*/
public Optional<RegistrarPoc> getRdapAbuseContact() {
@@ -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(
@@ -88,7 +88,8 @@ public class BulkDomainTransferCommand extends ConfirmingCommand implements Comm
@Parameter(
names = {"--registrar_request"},
description = "Whether the change was requested by a registrar.")
description = "Whether the change was requested by a registrar.",
arity = 1)
private boolean requestedByRegistrar = false;
@Parameter(
@@ -118,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) {
@@ -38,6 +38,10 @@ public class CreateUserCommand extends CreateOrUpdateUserCommand implements Comm
@Config("gSuiteConsoleUserGroupEmailAddress")
Optional<String> maybeGroupEmailAddress;
@Inject
@Config("consoleIapServiceId")
Optional<String> consoleIapServiceId;
@Nullable
@Override
User getExistingUser(String email) {
@@ -49,7 +53,8 @@ public class CreateUserCommand extends CreateOrUpdateUserCommand implements Comm
@Override
protected String execute() throws Exception {
String ret = super.execute();
grantIapPermission(email, maybeGroupEmailAddress, null, connection, iamClient);
grantIapPermission(
email, maybeGroupEmailAddress, consoleIapServiceId, null, connection, iamClient);
return ret;
}
@@ -52,7 +52,8 @@ final class DeleteDomainCommand extends MutatingEppToolCommand {
@Parameter(
names = {"--registrar_request"},
description = "Whether the change was requested by a registrar.")
description = "Whether the change was requested by a registrar.",
arity = 1)
private boolean requestedByRegistrar = false;
@Override
@@ -20,6 +20,8 @@ import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.persistence.transaction.QueryComposer.Comparator;
import google.registry.util.DomainNameUtils;
import java.util.List;
@@ -32,6 +34,12 @@ final class GetDomainCommand extends GetEppResourceCommand {
@Parameter(names = "--show_deleted", description = "Include deleted domains in the print out")
private boolean showDeleted = false;
@Parameter(
names = "--show_authcode",
description = "Include domain authentication code in output",
arity = 1)
private boolean showAuthcode = false;
@Parameter(
description = "Fully qualified domain name(s)",
required = true)
@@ -51,14 +59,26 @@ final class GetDomainCommand extends GetEppResourceCommand {
.stream()
.forEach(
d -> {
printResource("Domain", canonicalDomain, Optional.of(d));
printResource(
"Domain", canonicalDomain, Optional.of(censorAuthcode(d)));
}));
} else {
printResource(
"Domain",
canonicalDomain,
ForeignKeyUtils.loadResource(Domain.class, canonicalDomain, readTimestamp));
ForeignKeyUtils.loadResource(Domain.class, canonicalDomain, readTimestamp)
.map(this::censorAuthcode));
}
}
}
private Domain censorAuthcode(Domain domain) {
if (showAuthcode || domain.getAuthInfo() == null) {
return domain;
}
return domain
.asBuilder()
.setAuthInfo(DomainAuthInfo.create(PasswordAuth.create("[REDACTED]")))
.build();
}
}
@@ -16,6 +16,7 @@ package google.registry.tools;
import com.google.api.services.cloudresourcemanager.CloudResourceManager;
import com.google.api.services.cloudresourcemanager.model.Binding;
import com.google.api.services.cloudresourcemanager.model.Expr;
import com.google.api.services.cloudresourcemanager.model.GetIamPolicyRequest;
import com.google.api.services.cloudresourcemanager.model.Policy;
import com.google.api.services.cloudresourcemanager.model.SetIamPolicyRequest;
@@ -31,7 +32,10 @@ import java.util.Optional;
@Singleton
public class IamClient {
private static final String MEMBER_FORMAT = "user:%s";
private static final String IAP_ACCESS_EXPRESSION_FORMAT =
"resource.name == 'projects/%s/iap_web/compute/services/%s'";
private final CloudResourceManager resourceManager;
private final String projectId;
@@ -60,7 +64,7 @@ public class IamClient {
*
* <p>No-op if the role is already bound to the account.
*/
public void addBinding(String account, String role) {
public void addBinding(String account, String role, String consoleIapServiceId) {
String member = String.format(MEMBER_FORMAT, account);
Policy policy = getPolicy();
Binding binding =
@@ -69,7 +73,21 @@ public class IamClient {
.findFirst()
.orElseGet(
() -> {
Binding newBinding = new Binding().setRole(role).setMembers(new ArrayList<>());
Binding newBinding =
new Binding()
.setRole(role)
.setMembers(new ArrayList<>())
.setCondition(
new Expr()
.setTitle("Registrar Console IAP access")
.setDescription(
"Restrict IAP access only to the Registrar Console HTTP(s)"
+ " load balancer")
.setExpression(
String.format(
IAP_ACCESS_EXPRESSION_FORMAT,
projectId,
consoleIapServiceId)));
policy.getBindings().add(newBinding);
return newBinding;
});
@@ -86,6 +86,10 @@ final class SetupOteCommand extends ConfirmingCommand {
@Config("gSuiteConsoleUserGroupEmailAddress")
Optional<String> maybeGroupEmailAddress;
@Inject
@Config("consoleIapServiceId")
Optional<String> consoleIapServiceId;
OteAccountBuilder oteAccountBuilder;
String password;
@@ -138,7 +142,8 @@ you sure you didn't mean to run this against sandbox (e.g. "-e SANDBOX")?\
@Override
public String execute() {
ImmutableMap<String, String> clientIdToTld = oteAccountBuilder.buildAndPersist();
oteAccountBuilder.grantIapPermission(maybeGroupEmailAddress, cloudTasksUtils, iamClient);
oteAccountBuilder.grantIapPermission(
maybeGroupEmailAddress, consoleIapServiceId, cloudTasksUtils, iamClient);
StringBuilder output = new StringBuilder();
@@ -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(
@@ -127,6 +126,14 @@ public abstract class ConsoleApiAction implements Runnable {
}
}
protected static void checkGlobalPermission(User user, ConsolePermission permission) {
if (!user.getUserRoles().hasGlobalPermission(permission)) {
throw new ConsolePermissionForbiddenException(
String.format(
"User %s does not have global permission %s", user.getEmailAddress(), permission));
}
}
protected void postHandler(User user) {
throw new UnsupportedOperationException("Console API POST handler not implemented");
}
@@ -59,25 +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 IamClient iamClient;
private final Optional<String> consoleIapServiceId;
private final String gSuiteDomainName;
private final String registrarId;
@Inject
public ConsoleOteAction(
ConsoleApiParams consoleApiParams,
IamClient iamClient,
@Parameter("registrarId") String registrarId, // Get request param
@Config("gSuiteConsoleUserGroupEmailAddress") Optional<String> maybeGroupEmailAddress,
@Named("base58StringGenerator") StringGenerator passwordGenerator,
@Parameter("oteCreateData") Optional<OteCreateData> oteCreateData) {
@Parameter("oteCreateData") Optional<OteCreateData> oteCreateData,
@Config("gSuiteConsoleUserGroupEmailAddress") Optional<String> maybeGroupEmailAddress,
@Config("consoleIapServiceId") Optional<String> consoleIapServiceId,
@Config("gSuiteDomainName") String gSuiteDomainName,
@Parameter("registrarId") String registrarId) {
super(consoleApiParams);
this.iamClient = iamClient;
this.passwordGenerator = passwordGenerator;
this.oteCreateData = oteCreateData;
this.maybeGroupEmailAddress = maybeGroupEmailAddress;
this.iamClient = iamClient;
this.consoleIapServiceId = consoleIapServiceId;
this.gSuiteDomainName = gSuiteDomainName;
this.registrarId = registrarId;
}
@@ -94,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);
@@ -105,7 +114,8 @@ public class ConsoleOteAction extends ConsoleApiAction {
.setPassword(password);
ImmutableMap<String, String> registrarIdToTld = oteAccountBuilder.buildAndPersist();
oteAccountBuilder.grantIapPermission(maybeGroupEmailAddress, cloudTasksUtils, iamClient);
oteAccountBuilder.grantIapPermission(
maybeGroupEmailAddress, consoleIapServiceId, cloudTasksUtils, iamClient);
consoleApiParams.response().setStatus(SC_OK);
consoleApiParams
.response()
@@ -15,6 +15,7 @@
package google.registry.ui.server.console;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.POST;
import static google.registry.util.DateTimeUtils.START_INSTANT;
@@ -38,7 +39,6 @@ import google.registry.util.RegistryEnvironment;
import jakarta.inject.Inject;
import java.time.Instant;
import java.util.Optional;
import java.util.stream.Collectors;
@Action(
service = Service.CONSOLE,
@@ -69,22 +69,23 @@ public class ConsoleUpdateRegistrarAction extends ConsoleApiAction {
tm().transact(
() -> {
Optional<Registrar> existingRegistrar =
Registrar.loadByRegistrarId(registrarParam.getRegistrarId());
checkArgument(
!existingRegistrar.isEmpty(),
"Registrar with registrarId %s doesn't exists",
registrarParam.getRegistrarId());
Registrar existingRegistrar =
Registrar.loadByRegistrarId(registrarParam.getRegistrarId())
.orElseThrow(
() ->
new IllegalArgumentException(
String.format(
"Registrar %s does not exist",
registrarParam.getRegistrarId())));
// Only allow modifying allowed TLDs if we're in a non-PRODUCTION environment, if the
// registrar is not REAL, or the registrar has a RDAP abuse contact set.
if (!registrarParam.getAllowedTlds().isEmpty()) {
boolean isRealRegistrar =
Registrar.Type.REAL.equals(existingRegistrar.get().getType());
boolean isRealRegistrar = Registrar.Type.REAL.equals(existingRegistrar.getType());
if (RegistryEnvironment.PRODUCTION.equals(RegistryEnvironment.get())
&& isRealRegistrar) {
checkArgumentPresent(
existingRegistrar.get().getRdapAbuseContact(),
existingRegistrar.getRdapAbuseContact(),
"Cannot modify allowed TLDs if there is no RDAP abuse contact set. Please"
+ " use the \"nomulus registrar_contact\" command on this registrar to"
+ " set a RDAP abuse contact.");
@@ -103,20 +104,27 @@ public class ConsoleUpdateRegistrarAction extends ConsoleApiAction {
var updatedRegistrarBuilder =
existingRegistrar
.get()
.asBuilder()
.setLastPocVerificationDate(newLastPocVerificationDate);
if (user.getUserRoles()
.hasGlobalPermission(ConsolePermission.EDIT_REGISTRAR_DETAILS)) {
updatedRegistrarBuilder =
updatedRegistrarBuilder
.setAllowedTlds(
registrarParam.getAllowedTlds().stream()
.map(DomainNameUtils::canonicalizeHostname)
.collect(Collectors.toSet()))
.setRegistryLockAllowed(registrarParam.isRegistryLockAllowed())
.setLastPocVerificationDate(newLastPocVerificationDate);
if (!registrarParam.getAllowedTlds().equals(existingRegistrar.getAllowedTlds())) {
// The global permission EDIT_REGISTRAR_DETAILS signifies a support agent who *does*
// have the ability to edit allowed TLDs. See support docs 2.19: "Enabling TLD
// Access in Production"
checkGlobalPermission(user, ConsolePermission.EDIT_REGISTRAR_DETAILS);
updatedRegistrarBuilder.setAllowedTlds(
registrarParam.getAllowedTlds().stream()
.map(DomainNameUtils::canonicalizeHostname)
.collect(toImmutableSet()));
}
if (registrarParam.isRegistryLockAllowed()
!= existingRegistrar.isRegistryLockAllowed()) {
// Enabling registry lock requires a support lead or FTE, which maps to
// MANAGE_REGISTRARS. See support docs 2.33: "Registry Lock Onboarding Process"
checkGlobalPermission(user, ConsolePermission.MANAGE_REGISTRARS);
updatedRegistrarBuilder.setRegistryLockAllowed(
registrarParam.isRegistryLockAllowed());
}
var updatedRegistrar = updatedRegistrarBuilder.build();
@@ -126,14 +134,11 @@ public class ConsoleUpdateRegistrarAction extends ConsoleApiAction {
.setType(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE)
.setDescription(updatedRegistrar.getRegistrarId()));
logConsoleChangesIfNecessary(updatedRegistrar, existingRegistrar.get());
logConsoleChangesIfNecessary(updatedRegistrar, existingRegistrar);
sendExternalUpdatesIfNecessary(
EmailInfo.create(
existingRegistrar.get(),
updatedRegistrar,
ImmutableSet.of(),
ImmutableSet.of()));
existingRegistrar, updatedRegistrar, ImmutableSet.of(), ImmutableSet.of()));
});
consoleApiParams.response().setStatus(SC_OK);
@@ -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;
@@ -79,6 +78,7 @@ public class ConsoleUsersAction extends ConsoleApiAction {
private final StringGenerator passwordGenerator;
private final Optional<UserData> userData;
private final Optional<String> maybeGroupEmailAddress;
private final Optional<String> consoleIapServiceId;
private final IamClient iamClient;
private final String gSuiteDomainName;
@@ -89,6 +89,7 @@ public class ConsoleUsersAction extends ConsoleApiAction {
IamClient iamClient,
@Config("gSuiteDomainName") String gSuiteDomainName,
@Config("gSuiteConsoleUserGroupEmailAddress") Optional<String> maybeGroupEmailAddress,
@Config("consoleIapServiceId") Optional<String> consoleIapServiceId,
@Named("base58StringGenerator") StringGenerator passwordGenerator,
@Parameter("userData") Optional<UserData> userData,
@Parameter("registrarId") String registrarId) {
@@ -98,6 +99,7 @@ public class ConsoleUsersAction extends ConsoleApiAction {
this.passwordGenerator = passwordGenerator;
this.userData = userData;
this.maybeGroupEmailAddress = maybeGroupEmailAddress;
this.consoleIapServiceId = consoleIapServiceId;
this.iamClient = iamClient;
this.gSuiteDomainName = gSuiteDomainName;
}
@@ -256,7 +258,8 @@ public class ConsoleUsersAction extends ConsoleApiAction {
User.Builder builder = new User.Builder().setUserRoles(userRoles).setEmailAddress(newEmail);
tm().put(builder.build());
User.grantIapPermission(newEmail, maybeGroupEmailAddress, cloudTasksUtils, null, iamClient);
User.grantIapPermission(
newEmail, maybeGroupEmailAddress, consoleIapServiceId, cloudTasksUtils, null, iamClient);
sendConfirmationEmail(registrarId, newEmail, "Created user");
consoleApiParams.response().setStatus(SC_CREATED);
consoleApiParams
@@ -353,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. */
@@ -228,17 +228,23 @@ public class ContactAction extends ConsoleApiAction {
enforcePrimaryContactRestrictions(oldContactsByType, newContactsByType);
ensurePhoneNumberNotRemovedForContactTypes(oldContactsByType, newContactsByType, Type.TECH);
Optional<RegistrarPoc> domainRdapAbuseContact =
getDomainRdapVisibleAbuseContact(updatedContacts);
// If the new set has a domain RDAP abuse contact, it must have a phone number.
if (domainRdapAbuseContact.isPresent()
&& domainRdapAbuseContact.get().getPhoneNumber() == null) {
throw new ContactRequirementException(
"The abuse contact visible in domain RDAP query must have a phone number");
}
ImmutableSet<RegistrarPoc> abusePocs =
updatedContacts.stream()
.filter(RegistrarPoc::getVisibleInDomainRdapAsAbuse)
.collect(toImmutableSet());
// All abuse POCs must have a phone number attached
abusePocs.forEach(
poc -> {
if (poc.getPhoneNumber() == null) {
throw new ContactRequirementException(
"The abuse contact visible in domain RDAP query must have a phone number");
}
});
// If there was a domain RDAP abuse contact in the old set, the new set must have one.
if (getDomainRdapVisibleAbuseContact(existingContacts).isPresent()
&& domainRdapAbuseContact.isEmpty()) {
if (existingContacts.stream().anyMatch(RegistrarPoc::getVisibleInDomainRdapAsAbuse)
&& abusePocs.isEmpty()) {
throw new ContactRequirementException(
"An abuse contact visible in domain RDAP query must be designated");
}
@@ -255,26 +261,14 @@ public class ContactAction extends ConsoleApiAction {
newContactsByType.get(Type.ADMIN).stream()
.map(RegistrarPoc::getEmailAddress)
.collect(toImmutableSet());
if (!newAdminEmails.containsAll(oldAdminEmails)) {
throw new ContactRequirementException(
"Cannot remove or change the email address of primary contacts");
// ADMIN (primary) PoC emails are used for EPP password resets and potentially other
// security-sensitive tasks. A user with only EDIT_REGISTRAR_DETAILS should not be able to
// change this.
if (!newAdminEmails.equals(oldAdminEmails)) {
throw new ContactRequirementException("Cannot alter the set of primary contacts");
}
}
/**
* Retrieves the registrar contact whose phone number and email address is visible in domain RDAP
* query as abuse contact (if any).
*
* <p>Frontend processing ensures that only one contact can be set as abuse contact in domain RDAP
* record.
*
* <p>Therefore, it is possible to return inside the loop once one such contact is found.
*/
private static Optional<RegistrarPoc> getDomainRdapVisibleAbuseContact(
Set<RegistrarPoc> contacts) {
return contacts.stream().filter(RegistrarPoc::getVisibleInDomainRdapAsAbuse).findFirst();
}
/**
* Ensure that for each given registrar type, a phone number is present after update, if there was
* one before.
@@ -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");
@@ -51,6 +51,7 @@ import google.registry.flows.host.HostFlowUtils.InvalidHostNameException;
import google.registry.flows.host.HostFlowUtils.IpAddressNotRoutableException;
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainDoesNotExistException;
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainInPendingDeleteException;
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainServerUpdateProhibitedException;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.domain.Domain;
import google.registry.model.eppcommon.StatusValue;
@@ -413,6 +414,22 @@ class HostCreateFlowTest extends ResourceFlowTestCase<HostCreateFlow, Host> {
.marshalsToXml();
}
@Test
void testFailure_superordinateDomainIsServerUpdateProhibited() {
createTld("tld");
persistResource(
DatabaseHelper.newDomain("example.tld")
.asBuilder()
.setStatusValues(ImmutableSet.of(StatusValue.SERVER_UPDATE_PROHIBITED))
.build());
setEppHostCreateInputWithIps("ns1.example.tld");
SuperordinateDomainServerUpdateProhibitedException thrown =
assertThrows(SuperordinateDomainServerUpdateProhibitedException.class, this::runFlow);
assertThat(thrown)
.hasMessageThat()
.contains("Superordinate domain for this hostname has serverUpdateProhibited status");
}
@Test
void testIcannActivityReportField_getsLogged() throws Exception {
runFlow();
@@ -42,6 +42,7 @@ import google.registry.flows.exceptions.ResourceToDeleteIsReferencedException;
import google.registry.flows.host.HostFlowUtils.HostNameNotLowerCaseException;
import google.registry.flows.host.HostFlowUtils.HostNameNotNormalizedException;
import google.registry.flows.host.HostFlowUtils.HostNameNotPunyCodedException;
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainServerUpdateProhibitedException;
import google.registry.model.domain.Domain;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.rgp.GracePeriodStatus;
@@ -360,6 +361,25 @@ class HostDeleteFlowTest extends ResourceFlowTestCase<HostDeleteFlow, Host> {
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_superordinateDomainIsServerUpdateProhibited() {
createTld("tld");
Domain domain =
persistResource(
DatabaseHelper.newDomain("example.tld")
.asBuilder()
.setStatusValues(ImmutableSet.of(StatusValue.SERVER_UPDATE_PROHIBITED))
.build());
persistResource(
newHost("ns1.example.tld").asBuilder().setSuperordinateDomain(domain.createVKey()).build());
clock.advanceOneMilli();
SuperordinateDomainServerUpdateProhibitedException thrown =
assertThrows(SuperordinateDomainServerUpdateProhibitedException.class, this::runFlow);
assertThat(thrown)
.hasMessageThat()
.contains("Superordinate domain for this hostname has serverUpdateProhibited status");
}
@Test
void testIcannActivityReportField_getsLogged() throws Exception {
persistActiveHost("ns1.example.tld");
@@ -68,6 +68,7 @@ import google.registry.flows.host.HostFlowUtils.HostNameTooShallowException;
import google.registry.flows.host.HostFlowUtils.IpAddressNotRoutableException;
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainDoesNotExistException;
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainInPendingDeleteException;
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainServerUpdateProhibitedException;
import google.registry.flows.host.HostUpdateFlow.CannotAddIpToExternalHostException;
import google.registry.flows.host.HostUpdateFlow.CannotRemoveSubordinateHostLastIpException;
import google.registry.flows.host.HostUpdateFlow.CannotRenameExternalHostException;
@@ -889,6 +890,30 @@ class HostUpdateFlowTest extends ResourceFlowTestCase<HostUpdateFlow, Host> {
.contains("Superordinate domain for this hostname is in pending delete");
}
@Test
void testFailure_superordinateDomainIsServerUpdateProhibited() throws Exception {
setEppHostUpdateInput(
"ns1.example.tld",
"ns2.example.tld",
"<host:addr ip=\"v4\">192.0.2.22</host:addr>",
"<host:addr ip=\"v6\">1080:0:0:0:8:800:200C:417A</host:addr>");
createTld("tld");
Domain domain =
persistResource(
DatabaseHelper.newDomain("example.tld")
.asBuilder()
.setSubordinateHosts(ImmutableSet.of(oldHostName()))
.setStatusValues(ImmutableSet.of(StatusValue.SERVER_UPDATE_PROHIBITED))
.build());
persistActiveSubordinateHost(oldHostName(), domain);
clock.advanceOneMilli();
SuperordinateDomainServerUpdateProhibitedException thrown =
assertThrows(SuperordinateDomainServerUpdateProhibitedException.class, this::runFlow);
assertThat(thrown)
.hasMessageThat()
.contains("Superordinate domain for this hostname has serverUpdateProhibited status");
}
@Test
void testFailure_neverExisted() throws Exception {
ResourceDoesNotExistException thrown =
@@ -116,6 +116,7 @@ public final class OteAccountBuilderTest {
public static void verifyIapPermission(
@Nullable String emailAddress,
Optional<String> maybeGroupEmailAddress,
Optional<String> consoleIapServiceId,
CloudTasksHelper cloudTasksHelper,
IamClient iamClient) {
if (emailAddress == null) {
@@ -125,7 +126,8 @@ public final class OteAccountBuilderTest {
String groupEmailAddress = maybeGroupEmailAddress.orElse(null);
if (groupEmailAddress == null) {
cloudTasksHelper.assertNoTasksEnqueued("console-user-group-update");
verify(iamClient).addBinding(emailAddress, IAP_SECURED_WEB_APP_USER_ROLE);
verify(iamClient)
.addBinding(emailAddress, IAP_SECURED_WEB_APP_USER_ROLE, consoleIapServiceId.get());
} else {
cloudTasksHelper.assertTasksEnqueued(
"console-user-group-update",
@@ -146,9 +148,32 @@ public final class OteAccountBuilderTest {
CloudTasksUtils cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
OteAccountBuilder.forRegistrarId("myclientid")
.addUser("email@example.com")
.grantIapPermission(Optional.of("console@example.com"), cloudTasksUtils, iamClient);
.grantIapPermission(
Optional.of("console@example.com"),
Optional.of("consoleIapServiceId"),
cloudTasksUtils,
iamClient);
verifyIapPermission(
"email@example.com", Optional.of("console@example.com"), cloudTasksHelper, iamClient);
"email@example.com",
Optional.of("console@example.com"),
Optional.of("consoleIapServiceId"),
cloudTasksHelper,
iamClient);
}
@Test
void testUpdateUserGroup_worksWithoutIapServiceId() {
CloudTasksUtils cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
OteAccountBuilder.forRegistrarId("myclientid")
.addUser("email@example.com")
.grantIapPermission(
Optional.of("console@example.com"), Optional.empty(), cloudTasksUtils, iamClient);
verifyIapPermission(
"email@example.com",
Optional.of("console@example.com"),
Optional.empty(),
cloudTasksHelper,
iamClient);
}
@Test
@@ -156,8 +181,14 @@ public final class OteAccountBuilderTest {
CloudTasksUtils cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
OteAccountBuilder.forRegistrarId("myclientid")
.addUser("email@example.com")
.grantIapPermission(Optional.empty(), cloudTasksUtils, iamClient);
verifyIapPermission("email@example.com", Optional.empty(), cloudTasksHelper, iamClient);
.grantIapPermission(
Optional.empty(), Optional.of("consoleIapServiceId"), cloudTasksUtils, iamClient);
verifyIapPermission(
"email@example.com",
Optional.empty(),
Optional.of("consoleIapServiceId"),
cloudTasksHelper,
iamClient);
}
@Test
@@ -66,13 +66,21 @@ public class UserTest extends EntityTestCase {
void testFailure_asyncAndSyncModeConflict() {
assertThrows(
IllegalArgumentException.class,
() -> User.grantIapPermission("email@example.com", Optional.empty(), null, null, null));
() ->
User.grantIapPermission(
"email@example.com",
Optional.empty(),
Optional.of("consoleIapServiceId"),
null,
null,
null));
assertThrows(
IllegalArgumentException.class,
() ->
User.grantIapPermission(
"email@example.com",
Optional.empty(),
Optional.of("consoleIapServiceId"),
mock(CloudTasksUtils.class),
mock(ServiceConnection.class),
null));
@@ -90,6 +98,24 @@ public class UserTest extends EntityTestCase {
null));
}
@Test
void testGrantIapPermission_neitherGroupNorIapServiceId_fails() {
IllegalArgumentException e =
assertThrows(
IllegalArgumentException.class,
() ->
User.grantIapPermission(
"email@example.com",
Optional.empty(),
Optional.empty(),
mock(CloudTasksUtils.class),
null,
mock(IamClient.class)));
assertThat(e)
.hasMessageThat()
.isEqualTo("At least one of groupEmailAddress or consoleIapServiceId must be present");
}
@Test
void testFailure_badInputs() {
User.Builder builder = new User.Builder();
@@ -156,13 +182,33 @@ public class UserTest extends EntityTestCase {
// Individual permission.
User.grantIapPermission(
"email@example.com", Optional.empty(), cloudTasksUtils, null, iamClient);
"email@example.com",
Optional.empty(),
Optional.of("consoleIapServiceId"),
cloudTasksUtils,
null,
iamClient);
cloudTasksHelper.assertNoTasksEnqueued();
verify(iamClient).addBinding("email@example.com", IAP_SECURED_WEB_APP_USER_ROLE);
verify(iamClient)
.addBinding("email@example.com", IAP_SECURED_WEB_APP_USER_ROLE, "consoleIapServiceId");
// Group membership.
User.grantIapPermission(
"email@example.com", Optional.of("console@example.com"), cloudTasksUtils, null, iamClient);
"email@example.com",
Optional.of("console@example.com"),
Optional.empty(),
cloudTasksUtils,
null,
iamClient);
// Group membership takes precedence over individual membership
User.grantIapPermission(
"groupemail@example.com",
Optional.of("console@example.com"),
Optional.of("consoleIapServiceId"),
cloudTasksUtils,
null,
iamClient);
cloudTasksHelper.assertTasksEnqueued(
"console-user-group-update",
new TaskMatcher()
@@ -171,6 +217,13 @@ public class UserTest extends EntityTestCase {
.path("/_dr/admin/updateUserGroup")
.param("userEmailAddress", "email@example.com")
.param("groupEmailAddress", "console@example.com")
.param("groupUpdateMode", "ADD"),
new TaskMatcher()
.service("BACKEND")
.method(HttpMethod.POST)
.path("/_dr/admin/updateUserGroup")
.param("userEmailAddress", "groupemail@example.com")
.param("groupEmailAddress", "console@example.com")
.param("groupUpdateMode", "ADD"));
verifyNoMoreInteractions(iamClient);
}
@@ -181,13 +234,25 @@ public class UserTest extends EntityTestCase {
IamClient iamClient = mock(IamClient.class);
// Individual permission.
User.grantIapPermission("email@example.com", Optional.empty(), null, connection, iamClient);
User.grantIapPermission(
"email@example.com",
Optional.empty(),
Optional.of("consoleIapServiceId"),
null,
connection,
iamClient);
verifyNoInteractions(connection);
verify(iamClient).addBinding("email@example.com", IAP_SECURED_WEB_APP_USER_ROLE);
verify(iamClient)
.addBinding("email@example.com", IAP_SECURED_WEB_APP_USER_ROLE, "consoleIapServiceId");
// Group membership.
User.grantIapPermission(
"email@example.com", Optional.of("console@example.com"), null, connection, iamClient);
"email@example.com",
Optional.of("console@example.com"),
Optional.empty(),
null,
connection,
iamClient);
verify(connection)
.sendPostRequest(
UpdateUserGroupAction.PATH,
@@ -109,6 +109,32 @@ class AddressTest {
assertThrows(IllegalArgumentException.class, () -> createAddress("line1", "", "line3"));
}
@Test
void testFailure_streetLineTooLong() {
assertThrows(IllegalArgumentException.class, () -> createAddress("a".repeat(256)));
}
@Test
void testFailure_cityTooLong() {
assertThrows(
IllegalArgumentException.class,
() -> createAddress("line1").asBuilder().setCity("a".repeat(256)).build());
}
@Test
void testFailure_stateTooLong() {
assertThrows(
IllegalArgumentException.class,
() -> createAddress("line1").asBuilder().setState("a".repeat(256)).build());
}
@Test
void testFailure_zipTooLong() {
assertThrows(
IllegalArgumentException.class,
() -> createAddress("line1").asBuilder().setZip("12345678901234567").build());
}
@Test
void testSuccess_pojoToAndFromXml() throws Exception {
JAXBContext jaxbContext = JAXBContext.newInstance(TestEntity.class);
@@ -165,4 +165,55 @@ public class BulkDomainTransferCommandTest extends CommandTestCase<BulkDomainTra
.isEqualTo(
"Must specify exactly one input method, either --domains or --domain_names_file");
}
@Test
void testSuccess_registrarRequestExplicit() throws Exception {
runCommandForced(
"--gaining_registrar_id",
"NewRegistrar",
"--losing_registrar_id",
"TheRegistrar",
"--reason",
"someReason",
"--domains",
"foo.tld,bar.tld",
"--registrar_request=false");
verify(connection)
.sendPostRequest(
"/_dr/task/bulkDomainTransfer",
ImmutableMap.of(
"gainingRegistrarId",
"NewRegistrar",
"losingRegistrarId",
"TheRegistrar",
"requestedByRegistrar",
false,
"reason",
"someReason"),
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));
}
}
@@ -45,6 +45,7 @@ public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
void beforeEach() {
command.iamClient = iamClient;
command.maybeGroupEmailAddress = Optional.empty();
command.consoleIapServiceId = Optional.of("consoleIapServiceId");
command.setConnection(connection);
}
@@ -56,7 +57,8 @@ public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
assertThat(onlyUser.getUserRoles().isAdmin()).isFalse();
assertThat(onlyUser.getUserRoles().getGlobalRole()).isEqualTo(GlobalRole.NONE);
assertThat(onlyUser.getUserRoles().getRegistrarRoles()).isEmpty();
verify(iamClient).addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE);
verify(iamClient)
.addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE, "consoleIapServiceId");
verifyNoMoreInteractions(iamClient);
verifyNoInteractions(connection);
}
@@ -86,6 +88,41 @@ public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
verifyNoMoreInteractions(connection);
}
@Test
void testSuccess_addToGroup_noIapServiceId() throws Exception {
command.maybeGroupEmailAddress = Optional.of("group@example.test");
command.consoleIapServiceId = Optional.empty();
runCommandForced("--email", "user@example.test");
User onlyUser = Iterables.getOnlyElement(DatabaseHelper.loadAllOf(User.class));
assertThat(onlyUser.getEmailAddress()).isEqualTo("user@example.test");
verify(connection)
.sendPostRequest(
UpdateUserGroupAction.PATH,
ImmutableMap.of(
"userEmailAddress",
"user@example.test",
"groupEmailAddress",
"group@example.test",
"groupUpdateMode",
"ADD"),
MediaType.PLAIN_TEXT_UTF_8,
new byte[0]);
verifyNoInteractions(iamClient);
verifyNoMoreInteractions(connection);
}
@Test
void testFailure_neitherGroupNorIapServiceId() {
command.maybeGroupEmailAddress = Optional.empty();
command.consoleIapServiceId = Optional.empty();
IllegalArgumentException e =
assertThrows(
IllegalArgumentException.class, () -> runCommandForced("--email", "user@example.test"));
assertThat(e)
.hasMessageThat()
.isEqualTo("At least one of groupEmailAddress or consoleIapServiceId must be present");
}
@Test
void testSuccess_registryLock() throws Exception {
runCommandForced(
@@ -107,7 +144,8 @@ public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
void testSuccess_admin() throws Exception {
runCommandForced("--email", "user@example.test", "--admin", "true");
assertThat(loadExistingUser("user@example.test").getUserRoles().isAdmin()).isTrue();
verify(iamClient).addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE);
verify(iamClient)
.addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE, "consoleIapServiceId");
verifyNoMoreInteractions(iamClient);
verifyNoInteractions(connection);
}
@@ -117,7 +155,8 @@ public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
runCommandForced("--email", "user@example.test", "--global_role", "FTE");
assertThat(loadExistingUser("user@example.test").getUserRoles().getGlobalRole())
.isEqualTo(GlobalRole.FTE);
verify(iamClient).addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE);
verify(iamClient)
.addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE, "consoleIapServiceId");
verifyNoMoreInteractions(iamClient);
verifyNoInteractions(connection);
}
@@ -136,7 +175,8 @@ public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
RegistrarRole.ACCOUNT_MANAGER,
"NewRegistrar",
RegistrarRole.PRIMARY_CONTACT));
verify(iamClient).addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE);
verify(iamClient)
.addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE, "consoleIapServiceId");
verifyNoMoreInteractions(iamClient);
verifyNoInteractions(connection);
}
@@ -144,7 +184,8 @@ public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
@Test
void testFailure_alreadyExists() throws Exception {
runCommandForced("--email", "user@example.test");
verify(iamClient).addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE);
verify(iamClient)
.addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE, "consoleIapServiceId");
verifyNoMoreInteractions(iamClient);
assertThat(
assertThrows(
@@ -53,10 +53,20 @@ class DeleteDomainCommandTest extends EppToolCommandTestCase<DeleteDomainCommand
"--client=NewRegistrar",
"--domain_name=example.tld",
"--reason=Test",
"--registrar_request");
"--registrar_request=true");
eppVerifier.verifySent("domain_delete_by_registrar.xml");
}
@Test
void testSuccess_requestedByRegistrarExplicitFalse() throws Exception {
runCommandForced(
"--client=NewRegistrar",
"--domain_name=example.tld",
"--reason=Test",
"--registrar_request=false");
eppVerifier.verifySent("domain_delete.xml");
}
@Test
void testFailure_noReason() {
assertThrows(
@@ -24,6 +24,8 @@ import static google.registry.util.DateTimeUtils.plusMonths;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.beust.jcommander.ParameterException;
import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.testing.DatabaseHelper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -136,4 +138,28 @@ class GetDomainCommandTest extends CommandTestCase<GetDomainCommand> {
// Deleted
assertInStdout("Websafe key: kind:Domain@sql:rO0ABXQABTMtVExE");
}
@Test
void testSuccess_censorsAuthcodeByDefault() throws Exception {
persistResource(
DatabaseHelper.newDomain("example.tld")
.asBuilder()
.setAuthInfo(DomainAuthInfo.create(PasswordAuth.create("secret123")))
.build());
runCommand("example.tld");
assertInStdout("value=[REDACTED]");
assertNotInStdout("secret123");
}
@Test
void testSuccess_showAuthcode() throws Exception {
persistResource(
DatabaseHelper.newDomain("example.tld")
.asBuilder()
.setAuthInfo(DomainAuthInfo.create(PasswordAuth.create("secret123")))
.build());
runCommand("example.tld", "--show_authcode=true");
assertInStdout("value=secret123");
assertNotInStdout("[REDACTED]");
}
}
@@ -38,6 +38,7 @@ import org.mockito.ArgumentCaptor;
/** Unit tests for {@link IamClient}. */
public class IamClientTest {
private final CloudResourceManager resourceManager = mock(CloudResourceManager.class);
private final String consoleIapServiceId = "123456789";
private final String projectId = "my-project";
private final String account = "test@example.test";
private final String role = "roles/fakeRole";
@@ -89,11 +90,17 @@ public class IamClientTest {
void testSuccess_addBinding_noMatchedBindingExists() throws Exception {
setupRequests();
assertThat(bindings.size()).isEqualTo(1);
client.addBinding(account, role);
client.addBinding(account, role, consoleIapServiceId);
assertThat(bindings.size()).isEqualTo(2);
Binding binding = bindings.get(1);
assertThat(binding.getRole()).isEqualTo(role);
assertThat(binding.getMembers()).containsExactly("user:" + account);
assertThat(binding.getCondition()).isNotNull();
assertThat(binding.getCondition().getTitle()).isEqualTo("Registrar Console IAP access");
assertThat(binding.getCondition().getDescription())
.isEqualTo("Restrict IAP access only to the Registrar Console HTTP(s) load balancer");
assertThat(binding.getCondition().getExpression())
.isEqualTo("resource.name == 'projects/my-project/iap_web/compute/services/123456789'");
verifySetPolicyRequest();
}
@@ -107,7 +114,7 @@ public class IamClientTest {
when(matchedBinding.getMembers()).thenReturn(existingMembers);
bindings.add(matchedBinding);
assertThat(bindings.size()).isEqualTo(2);
client.addBinding(account, role);
client.addBinding(account, role, consoleIapServiceId);
assertThat(bindings.size()).isEqualTo(2);
assertThat(existingMembers)
.containsExactly("serviceAccount:service@example.test", "user:" + account);
@@ -125,7 +132,7 @@ public class IamClientTest {
when(matchedBinding.getMembers()).thenReturn(existingMembers);
bindings.add(matchedBinding);
assertThat(bindings.size()).isEqualTo(2);
client.addBinding(account, role);
client.addBinding(account, role, consoleIapServiceId);
assertThat(bindings.size()).isEqualTo(2);
assertThat(existingMembers)
.containsExactly("serviceAccount:service@example.test", "user:" + account);
@@ -73,6 +73,7 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
command.clock = fakeClock;
command.passwordGenerator = passwordGenerator;
command.maybeGroupEmailAddress = Optional.of("group@example.com");
command.consoleIapServiceId = Optional.of("consoleIapServiceId");
command.cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
command.iamClient = iamClient;
persistPremiumList("default_sandbox_list", USD, "sandbox,USD 1000");
@@ -129,7 +130,9 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
String groupEmailAddress = command.maybeGroupEmailAddress.orElse(null);
if (groupEmailAddress == null) {
cloudTasksHelper.assertNoTasksEnqueued("console-user-group-update");
verify(iamClient).addBinding(emailAddress, IAP_SECURED_WEB_APP_USER_ROLE);
verify(iamClient)
.addBinding(
emailAddress, IAP_SECURED_WEB_APP_USER_ROLE, command.consoleIapServiceId.get());
} else {
cloudTasksHelper.assertTasksEnqueued(
"console-user-group-update",
@@ -144,6 +144,7 @@ class ConsoleOteActionTest extends ConsoleActionBaseTestCase {
verifyIapPermission(
"contact@registry.example",
Optional.of("someRandomString@email.test"),
Optional.of("consoleIapServiceId"),
cloudTasksHelper,
iamClient);
}
@@ -202,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,
@@ -214,9 +233,11 @@ class ConsoleOteActionTest extends ConsoleActionBaseTestCase {
return new ConsoleOteAction(
consoleApiParams,
iamClient,
registrarId,
maybeGroupEmailAddress,
passwordGenerator,
oteCreateData);
oteCreateData,
maybeGroupEmailAddress,
Optional.of("consoleIapServiceId"),
"registry.example",
registrarId);
}
}
@@ -20,6 +20,7 @@ import static google.registry.testing.DatabaseHelper.createTlds;
import static google.registry.testing.DatabaseHelper.loadSingleton;
import static google.registry.testing.DatabaseHelper.persistResource;
import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -37,6 +38,7 @@ import google.registry.request.Action;
import google.registry.request.RequestModule;
import google.registry.request.auth.AuthResult;
import google.registry.testing.ConsoleApiParamsUtils;
import google.registry.testing.FakeResponse;
import google.registry.testing.SystemPropertyExtension;
import google.registry.util.EmailMessage;
import google.registry.util.RegistryEnvironment;
@@ -55,8 +57,6 @@ class ConsoleUpdateRegistrarActionTest extends ConsoleActionBaseTestCase {
private Registrar registrar;
private User user;
private static String registrarPostData =
"{\"registrarId\":\"%s\",\"allowedTlds\":[%s],\"registryLockAllowed\":%s,"
+ " \"lastPocVerificationDate\":%s }";
@@ -76,12 +76,6 @@ class ConsoleUpdateRegistrarActionTest extends ConsoleActionBaseTestCase {
.setAllowedTlds(ImmutableSet.of())
.setRegistryLockAllowed(false)
.build());
user =
persistResource(
new User.Builder()
.setEmailAddress("user@registrarId.com")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build())
.build());
}
@Test
@@ -209,15 +203,123 @@ class ConsoleUpdateRegistrarActionTest extends ConsoleActionBaseTestCase {
.build());
}
private ConsoleApiParams createParams() {
AuthResult authResult = AuthResult.createUser(user);
return ConsoleApiParamsUtils.createFake(authResult);
@Test
void testFailure_registrarNotFound() throws IOException {
var action =
createAction(
String.format(
registrarPostData,
"nonexistent",
"app, dev",
false,
"\"2023-12-12T00:00:00.000Z\""));
action.run();
assertThat(response.getStatus()).isEqualTo(SC_FORBIDDEN);
}
private ConsoleUpdateRegistrarAction createAction(String requestData) throws IOException {
@Test
void testSuccess_supportAgentCanUpdateAllowedTlds() throws IOException {
User supportAgentUser =
persistResource(
new User.Builder()
.setEmailAddress("agent@registrarId.com")
.setUserRoles(
new UserRoles.Builder().setGlobalRole(GlobalRole.SUPPORT_AGENT).build())
.build());
var action =
createAction(
supportAgentUser,
String.format(
registrarPostData,
"TheRegistrar",
"app, dev",
false,
"\"2023-12-12T00:00:00.000Z\""));
action.run();
Registrar newRegistrar = Registrar.loadByRegistrarId("TheRegistrar").get();
assertThat(newRegistrar.getAllowedTlds()).containsExactly("app", "dev");
assertThat(response.getStatus()).isEqualTo(SC_OK);
}
@Test
void testFailure_supportAgentCannotUpdateRegistryLock() throws IOException {
User supportAgentUser =
persistResource(
new User.Builder()
.setEmailAddress("agent@registrarId.com")
.setUserRoles(
new UserRoles.Builder().setGlobalRole(GlobalRole.SUPPORT_AGENT).build())
.build());
var action =
createAction(
supportAgentUser,
String.format(
registrarPostData,
"TheRegistrar",
"app, dev",
true,
"\"2023-12-12T00:00:00.000Z\""));
action.run();
assertThat(response.getStatus()).isEqualTo(SC_FORBIDDEN);
Registrar unupdatedRegistrar = Registrar.loadByRegistrarId("TheRegistrar").get();
assertThat(unupdatedRegistrar.isRegistryLockAllowed()).isFalse();
}
@Test
void testFailure_userWithoutEditRegistrarDetailsCannotUpdateAllowedTlds() throws IOException {
User normalUser =
persistResource(
new User.Builder()
.setEmailAddress("normal@registrarId.com")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.NONE).build())
.build());
var action =
createAction(
normalUser,
String.format(
registrarPostData,
"TheRegistrar",
"app, dev",
false,
"\"2023-12-12T00:00:00.000Z\""));
action.run();
assertThat(response.getStatus()).isEqualTo(SC_FORBIDDEN);
Registrar unupdatedRegistrar = Registrar.loadByRegistrarId("TheRegistrar").get();
assertThat(unupdatedRegistrar.getAllowedTlds()).isEmpty();
}
@Test
void testSuccess_supportLeadCanUpdateRegistryLock() throws IOException {
User supportLeadUser =
persistResource(
new User.Builder()
.setEmailAddress("lead@registrarId.com")
.setUserRoles(
new UserRoles.Builder().setGlobalRole(GlobalRole.SUPPORT_LEAD).build())
.build());
var action =
createAction(
supportLeadUser,
String.format(
registrarPostData, "TheRegistrar", "", true, "\"2023-12-12T00:00:00.000Z\""));
action.run();
Registrar newRegistrar = Registrar.loadByRegistrarId("TheRegistrar").get();
assertThat(newRegistrar.isRegistryLockAllowed()).isTrue();
assertThat(response.getStatus()).isEqualTo(SC_OK);
}
private ConsoleUpdateRegistrarAction createAction(User actionUser, String requestData)
throws IOException {
AuthResult authResult = AuthResult.createUser(actionUser);
consoleApiParams = ConsoleApiParamsUtils.createFake(authResult);
response = (FakeResponse) consoleApiParams.response();
when(consoleApiParams.request().getMethod()).thenReturn(Action.Method.POST.toString());
Optional<Registrar> maybeRegistrarUpdateData =
ConsoleModule.provideRegistrar(GSON, RequestModule.provideJsonBody(requestData, GSON));
return new ConsoleUpdateRegistrarAction(consoleApiParams, maybeRegistrarUpdateData);
}
private ConsoleUpdateRegistrarAction createAction(String requestData) throws IOException {
return createAction(fteUser, requestData);
}
}
@@ -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(
@@ -663,6 +684,7 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase {
iamClient,
"email.com",
Optional.of("someRandomString"),
Optional.of("consoleIapServiceId"),
passwordGenerator,
userData,
"TheRegistrar");
@@ -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 =
@@ -343,10 +343,23 @@ visibleInRdapAsTech=true, visibleInDomainRdapAsAbuse=false}
testRegistrar.getRegistrarId(),
adminPoc.asBuilder().setEmailAddress("testemail@example.com").build());
action.run();
FakeResponse fakeResponse = response;
assertThat(fakeResponse.getStatus()).isEqualTo(400);
assertThat(fakeResponse.getPayload())
.isEqualTo("Cannot remove or change the email address of primary contacts");
assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST);
assertThat(response.getPayload()).isEqualTo("Cannot alter the set of primary contacts");
}
@Test
void testFailure_addsAdminEmail() throws Exception {
RegistrarPoc newAdminPoc =
adminPoc
.asBuilder()
.setName("New Admin Contact")
.setEmailAddress("new.admin@example.com")
.build();
ContactAction action =
createAction(Action.Method.POST, fteUser, testRegistrar.getRegistrarId(), newAdminPoc);
action.run();
assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST);
assertThat(response.getPayload()).isEqualTo("Cannot alter the set of primary contacts");
}
private ContactAction createAction(
@@ -19,6 +19,7 @@ import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableO
import static google.registry.testing.DatabaseHelper.loadSingleton;
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableList;
@@ -67,7 +68,8 @@ public class RdapRegistrarFieldsActionTest extends ConsoleActionBaseTestCase {
"url",
"\"http://my.fake.url\"",
"localizedAddress",
"{\"street\": [\"123 Example Boulevard\"], \"city\": \"Williamsburg\", \"state\":"
"{\"street\": [\"123 Example Boulevard\"], \"city\":"
+ " \"Williamsburg\", \"state\":"
+ " \"NY\", \"zip\": \"11201\", \"countryCode\": \"US\"}"));
@Test
@@ -132,6 +134,17 @@ public class RdapRegistrarFieldsActionTest extends ConsoleActionBaseTestCase {
assertThat(DatabaseHelper.loadByEntity(newRegistrar)).isEqualTo(newRegistrar);
}
@Test
void testFailure_zipTooLong() throws Exception {
uiRegistrarMap.put(
"localizedAddress",
"{\"street\": [\"123 Fake St\"], \"city\": \"Fakeville\", \"state\":"
+ " \"NL\", \"zip\": \"12345678901234567\", \"countryCode\": \"CA\"}");
IllegalArgumentException exception =
assertThrows(IllegalArgumentException.class, this::createAction);
assertThat(exception).hasMessageThat().contains("Zip cannot be longer than 16 characters");
}
private RdapRegistrarFieldsAction createAction() throws IOException {
return createAction(fteUser);
}
@@ -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)
);
@@ -9,8 +9,8 @@
<init-param>
<param-name>headerConfig</param-name>
<param-value>
set Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; object-src 'none'; base-uri 'self';
set X-Content-Type-Options: nosniff
set Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; object-src 'none'; base-uri 'self';,
set X-Content-Type-Options: nosniff,
set X-Frame-Options: DENY
</param-value>
</init-param>
@@ -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"
+21
View File
@@ -253,6 +253,27 @@ 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)
for env in alpha crash qa sandbox production
do
if [ ${env} == production ]
then
project="domain-registry"
else
project="domain-registry-${env}"
fi
# non-canary
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
# canary
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-server/epp-server-canary/g | \
sed s/EPP-v2-ipv4-main/EPP-v2-ipv4-canary/g | \
sed s/EPP-v2-ipv6-main/EPP-v2-ipv6-canary/g > ./jetty/kubernetes/nomulus-${env}-epp-server-canary.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'
+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() {}
}