mirror of
https://github.com/google/nomulus
synced 2026-08-02 05:16:08 +00:00
Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5ae51a036 | ||
|
|
92b684d7ec | ||
|
|
49cecf6776 | ||
|
|
4cc3fc9cd2 | ||
|
|
74f441765e | ||
|
|
0ce83c8b2d | ||
|
|
fc4246ea95 | ||
|
|
29def8d78d | ||
|
|
0c79414a31 | ||
|
|
ea7d5d4a5e | ||
|
|
553fa1dc14 | ||
|
|
c36087dc93 | ||
|
|
bc44e095d4 | ||
|
|
355e8ba423 | ||
|
|
dca5a010d5 | ||
|
|
147e7dabcb | ||
|
|
0b8025d3db | ||
|
|
9c4a558d7b | ||
|
|
7347dbf762 | ||
|
|
53b6044ff8 | ||
|
|
12ab7f47e2 | ||
|
|
d3a1d0709e |
@@ -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"]
|
||||
|
||||
+1
@@ -326,6 +326,7 @@
|
||||
<task>
|
||||
<url><![CDATA[/_dr/task/syncRemoteCache]]></url>
|
||||
<name>syncRemoteCache</name>
|
||||
<method>POST</method>
|
||||
<description>
|
||||
Syncs remote (Valkey/Redis) EPP resource caches with changes made recently.
|
||||
</description>
|
||||
|
||||
@@ -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()
|
||||
|
||||
+31
-26
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+29
-3
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+20
-20
@@ -65,37 +65,37 @@ class LocalConnectionLimiterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAcquireCert_successUpToLimit() {
|
||||
assertThat(limiter.acquireCert("cert_hash_1")).isTrue();
|
||||
assertThat(limiter.acquireCert("cert_hash_1")).isTrue();
|
||||
void testAcquireRegistrar_successUpToLimit() {
|
||||
assertThat(limiter.acquireRegistrar("registrar_1")).isTrue();
|
||||
assertThat(limiter.acquireRegistrar("registrar_1")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAcquireCert_rejectsOverLimit() {
|
||||
assertThat(limiter.acquireCert("cert_hash_1")).isTrue();
|
||||
assertThat(limiter.acquireCert("cert_hash_1")).isTrue();
|
||||
// 3rd attempt from same cert should be rejected
|
||||
assertThat(limiter.acquireCert("cert_hash_1")).isFalse();
|
||||
void testAcquireRegistrar_rejectsOverLimit() {
|
||||
assertThat(limiter.acquireRegistrar("registrar_1")).isTrue();
|
||||
assertThat(limiter.acquireRegistrar("registrar_1")).isTrue();
|
||||
// 3rd attempt from same registrar should be rejected
|
||||
assertThat(limiter.acquireRegistrar("registrar_1")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAcquireCert_independentAcrossCerts() {
|
||||
assertThat(limiter.acquireCert("cert_hash_1")).isTrue();
|
||||
assertThat(limiter.acquireCert("cert_hash_1")).isTrue();
|
||||
assertThat(limiter.acquireCert("cert_hash_1")).isFalse();
|
||||
void testAcquireRegistrar_independentAcrossRegistrars() {
|
||||
assertThat(limiter.acquireRegistrar("registrar_1")).isTrue();
|
||||
assertThat(limiter.acquireRegistrar("registrar_1")).isTrue();
|
||||
assertThat(limiter.acquireRegistrar("registrar_1")).isFalse();
|
||||
|
||||
// A different cert should still be allowed
|
||||
assertThat(limiter.acquireCert("cert_hash_2")).isTrue();
|
||||
// A different registrar should still be allowed
|
||||
assertThat(limiter.acquireRegistrar("registrar_2")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReleaseCert_freesSlot() {
|
||||
assertThat(limiter.acquireCert("cert_hash_1")).isTrue();
|
||||
assertThat(limiter.acquireCert("cert_hash_1")).isTrue();
|
||||
assertThat(limiter.acquireCert("cert_hash_1")).isFalse();
|
||||
void testReleaseRegistrar_freesSlot() {
|
||||
assertThat(limiter.acquireRegistrar("registrar_1")).isTrue();
|
||||
assertThat(limiter.acquireRegistrar("registrar_1")).isTrue();
|
||||
assertThat(limiter.acquireRegistrar("registrar_1")).isFalse();
|
||||
|
||||
limiter.releaseCert("cert_hash_1");
|
||||
limiter.releaseRegistrar("registrar_1");
|
||||
// Now we should be able to acquire again
|
||||
assertThat(limiter.acquireCert("cert_hash_1")).isTrue();
|
||||
assertThat(limiter.acquireRegistrar("registrar_1")).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
// Copyright 2026 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.flows;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.USE_RANDOM_SERVER_TRID;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureStatus.ACTIVE;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static google.registry.util.DateTimeUtils.START_INSTANT;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import google.registry.model.common.FeatureFlag;
|
||||
import google.registry.model.common.FeatureFlag.FeatureStatus;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Instant;
|
||||
import java.util.regex.Pattern;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
/** Unit tests for {@link ServerTridProviderImpl}. */
|
||||
class ServerTridProviderImplTest {
|
||||
|
||||
@RegisterExtension
|
||||
final JpaIntegrationTestExtension jpa =
|
||||
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
ServerTridProviderImpl.secureRandom.remove();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateServerTrid_flagInactive_generatesLegacyFormat() {
|
||||
ServerTridProviderImpl provider = new ServerTridProviderImpl();
|
||||
String trid1 = provider.createServerTrid();
|
||||
String trid2 = provider.createServerTrid();
|
||||
|
||||
assertThat(trid1).contains("-");
|
||||
assertThat(trid2).contains("-");
|
||||
assertThat(trid1).isNotEqualTo(trid2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateServerTrid_flagActive_generatesCorrectFormat() {
|
||||
persistResource(
|
||||
new FeatureFlag.Builder()
|
||||
.setFeatureName(USE_RANDOM_SERVER_TRID)
|
||||
.setStatusMap(
|
||||
ImmutableSortedMap.<Instant, FeatureStatus>naturalOrder()
|
||||
.put(START_INSTANT, ACTIVE)
|
||||
.build())
|
||||
.build());
|
||||
|
||||
SecureRandom mockSecureRandom = mock(SecureRandom.class);
|
||||
|
||||
// Mock secureRandom to return a deterministic sequence of bytes: 0, 1, 2, ..., 23
|
||||
doAnswer(
|
||||
invocation -> {
|
||||
byte[] bytes = invocation.getArgument(0);
|
||||
for (int i = 0; i < bytes.length; i++) {
|
||||
bytes[i] = (byte) i;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.when(mockSecureRandom)
|
||||
.nextBytes(any(byte[].class));
|
||||
|
||||
ServerTridProviderImpl.secureRandom.set(mockSecureRandom);
|
||||
ServerTridProviderImpl provider = new ServerTridProviderImpl();
|
||||
String trid = provider.createServerTrid();
|
||||
|
||||
String expectedTrid = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYX";
|
||||
|
||||
Pattern tridPattern = Pattern.compile("^[A-Za-z0-9_-]{32}$");
|
||||
assertThat(trid).matches(tridPattern);
|
||||
assertThat(trid.length()).isAtMost(64);
|
||||
assertThat(trid).isEqualTo(expectedTrid);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateServerTrid_flagActive_withMaxByteValues() {
|
||||
persistResource(
|
||||
new FeatureFlag.Builder()
|
||||
.setFeatureName(USE_RANDOM_SERVER_TRID)
|
||||
.setStatusMap(
|
||||
ImmutableSortedMap.<Instant, FeatureStatus>naturalOrder()
|
||||
.put(START_INSTANT, ACTIVE)
|
||||
.build())
|
||||
.build());
|
||||
|
||||
SecureRandom mockSecureRandom = mock(SecureRandom.class);
|
||||
|
||||
// Mock secureRandom to return all 0xFF bytes
|
||||
doAnswer(
|
||||
invocation -> {
|
||||
byte[] bytes = invocation.getArgument(0);
|
||||
for (int i = 0; i < bytes.length; i++) {
|
||||
bytes[i] = (byte) 0xFF;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.when(mockSecureRandom)
|
||||
.nextBytes(any(byte[].class));
|
||||
|
||||
ServerTridProviderImpl.secureRandom.set(mockSecureRandom);
|
||||
ServerTridProviderImpl provider = new ServerTridProviderImpl();
|
||||
String trid = provider.createServerTrid();
|
||||
|
||||
String expectedTrid = "________________________________";
|
||||
assertThat(trid).isEqualTo(expectedTrid);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateServerTrid_flagActive_realInitializationWorks() {
|
||||
persistResource(
|
||||
new FeatureFlag.Builder()
|
||||
.setFeatureName(USE_RANDOM_SERVER_TRID)
|
||||
.setStatusMap(
|
||||
ImmutableSortedMap.<Instant, FeatureStatus>naturalOrder()
|
||||
.put(START_INSTANT, ACTIVE)
|
||||
.build())
|
||||
.build());
|
||||
|
||||
ServerTridProviderImpl provider = new ServerTridProviderImpl();
|
||||
String trid1 = provider.createServerTrid();
|
||||
String trid2 = provider.createServerTrid();
|
||||
|
||||
Pattern tridPattern = Pattern.compile("^[A-Za-z0-9_-]{32}$");
|
||||
assertThat(trid1).matches(tridPattern);
|
||||
assertThat(trid2).matches(tridPattern);
|
||||
assertThat(trid1).isNotEqualTo(trid2);
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,6 @@ import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
|
||||
import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.CurrencyUnitMismatchException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.CurrencyValueScaleException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.DomainReservedException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.FeesMismatchException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.FeesRequiredForPremiumNameException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.MissingBillingAccountMapException;
|
||||
@@ -397,15 +396,14 @@ class DomainRestoreRequestFlowTest extends ResourceFlowTestCase<DomainRestoreReq
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_superuserOverridesReservedList() throws Exception {
|
||||
void testSuccess_reservedDomain() throws Exception {
|
||||
persistResource(
|
||||
Tld.get("tld")
|
||||
.asBuilder()
|
||||
.setReservedLists(persistReservedList("tld-reserved", "example,FULLY_BLOCKED"))
|
||||
.build());
|
||||
persistPendingDeleteDomain();
|
||||
runFlowAssertResponse(
|
||||
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("generic_success_response.xml"));
|
||||
runFlowAssertResponse(loadFile("generic_success_response.xml"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -631,19 +629,6 @@ class DomainRestoreRequestFlowTest extends ResourceFlowTestCase<DomainRestoreReq
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_reservedBlocked() throws Exception {
|
||||
createTld("tld");
|
||||
persistResource(
|
||||
Tld.get("tld")
|
||||
.asBuilder()
|
||||
.setReservedLists(persistReservedList("tld-reserved", "example,FULLY_BLOCKED"))
|
||||
.build());
|
||||
persistPendingDeleteDomain();
|
||||
EppException thrown = assertThrows(DomainReservedException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_premiumNotAcked() throws Exception {
|
||||
createTld("example");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+114
-12
@@ -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");
|
||||
|
||||
-12
@@ -251,18 +251,6 @@ public class ConsoleBulkDomainActionTest extends ConsoleActionBaseTestCase {
|
||||
assertThat(response.getStatus()).isEqualTo(SC_FORBIDDEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_pendingRegistrar() {
|
||||
persistResource(
|
||||
loadRegistrar("TheRegistrar").asBuilder().setState(Registrar.State.PENDING).build());
|
||||
JsonElement payload =
|
||||
GSON.toJsonTree(
|
||||
ImmutableMap.of("domainList", ImmutableList.of("example.tld"), "reason", "test"));
|
||||
ConsoleBulkDomainAction action = createAction("DELETE", payload);
|
||||
action.run();
|
||||
assertThat(response.getStatus()).isEqualTo(SC_FORBIDDEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_nonexistentRegistrar() {
|
||||
JsonElement payload =
|
||||
|
||||
+17
-4
@@ -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(
|
||||
|
||||
+14
-1
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user