1
0
mirror of https://github.com/google/nomulus synced 2026-07-06 08:06:33 +00:00

Compare commits

...

4 Commits

Author SHA1 Message Date
gbrodman e5ebe96c74 Add SQL code for simplified console update history table (#2710)
We'll remove the old ones, but this one adds the new simplified version
2025-03-07 19:40:19 +00:00
gbrodman 2ff4d97b0a Refactor console bulk domain action types (#2708)
This makes the action types a bit simpler -- this is possible because
we've reduced the scope of domain actions that we want to natively
support
2025-03-07 18:12:32 +00:00
gbrodman 6b0beeb477 Add BSA label to rdap-domain 404 responses for BSA domains (#2706) 2025-03-07 13:58:18 +00:00
Lai Jiang d2d43f4115 Fix a Cloud Scheduler deployment bug (#2707)
For GKE all tasks should be on backend, BSA was on its own service
because of egress IP constraint.

Also made it possible to specify a timeout for the Cloud Scheduler job,
with the default (3m) suitable for most tasks.
2025-03-06 16:25:52 +00:00
28 changed files with 5128 additions and 4653 deletions
@@ -301,6 +301,7 @@
<url><![CDATA[/_dr/task/bsaValidate]]></url>
<name>bsaValidate</name>
<service>bsa</service>
<timeout>10m</timeout>
<description>
Validates the processed BSA data in the database against the original
block lists.
@@ -199,16 +199,16 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
/** Returns the TLD entities for the given TLD strings, throwing if any don't exist. */
public static ImmutableSet<Tld> get(Set<String> tlds) {
Map<String, Optional<Tld>> registries = CACHE.getAll(tlds);
ImmutableSet<String> missingRegistries =
registries.entrySet().stream()
Map<String, Optional<Tld>> tldObjects = CACHE.getAll(tlds);
ImmutableSet<String> missingTlds =
tldObjects.entrySet().stream()
.filter(e -> e.getValue().isEmpty())
.map(Map.Entry::getKey)
.collect(toImmutableSet());
if (missingRegistries.isEmpty()) {
return registries.values().stream().map(Optional::get).collect(toImmutableSet());
if (missingTlds.isEmpty()) {
return tldObjects.values().stream().map(Optional::get).collect(toImmutableSet());
} else {
throw new TldNotFoundException(missingRegistries);
throw new TldNotFoundException(missingTlds);
}
}
@@ -14,10 +14,10 @@
package google.registry.model.tld.label;
import static com.google.common.base.Charsets.US_ASCII;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.hash.Funnels.stringFunnel;
import static java.nio.charset.StandardCharsets.US_ASCII;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
@@ -13,8 +13,8 @@
// limitations under the License.
package google.registry.persistence.converter;
import static com.google.common.base.Charsets.US_ASCII;
import static com.google.common.hash.Funnels.stringFunnel;
import static java.nio.charset.StandardCharsets.US_ASCII;
import com.google.common.hash.BloomFilter;
import jakarta.persistence.AttributeConverter;
@@ -14,14 +14,15 @@
package google.registry.rdap;
import static com.google.common.base.Charsets.UTF_8;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN;
import static google.registry.request.Actions.getPathForAction;
import static google.registry.util.DomainNameUtils.canonicalizeHostname;
import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
@@ -41,6 +42,7 @@ import google.registry.request.Parameter;
import google.registry.request.RequestMethod;
import google.registry.request.RequestPath;
import google.registry.request.Response;
import google.registry.util.Clock;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Optional;
@@ -60,6 +62,10 @@ public abstract class RdapActionBase implements Runnable {
private static final MediaType RESPONSE_MEDIA_TYPE =
MediaType.create("application", "rdap+json").withCharset(UTF_8);
private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().create();
private static final Gson FORMATTED_OUTPUT_GSON =
new GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create();
/** Whether to include or exclude deleted items from a query. */
protected enum DeletedItemHandling {
EXCLUDE,
@@ -75,6 +81,7 @@ public abstract class RdapActionBase implements Runnable {
@Inject @Parameter("formatOutput") Optional<Boolean> formatOutputParam;
@Inject @Config("rdapResultSetMaxSize") int rdapResultSetMaxSize;
@Inject RdapMetrics rdapMetrics;
@Inject Clock clock;
/** Builder for metric recording. */
final RdapMetrics.RdapMetricInformation.Builder metricInformationBuilder =
@@ -152,6 +159,10 @@ public abstract class RdapActionBase implements Runnable {
response.setStatus(SC_OK);
setPayload(replyObject);
metricInformationBuilder.setStatusCode(SC_OK);
} catch (RdapDomainAction.DomainBlockedByBsaException e) {
logger.atInfo().withCause(e).log("Domain blocked by BSA");
setErrorCodes(SC_NOT_FOUND);
setPayload(new RdapObjectClasses.DomainBlockedByBsaErrorResponse(e.getMessage()));
} catch (HttpException e) {
logger.atInfo().withCause(e).log("Error in RDAP.");
setError(e.getResponseCode(), e.getResponseCodeString(), e.getMessage());
@@ -166,8 +177,7 @@ public abstract class RdapActionBase implements Runnable {
}
void setError(int status, String title, String description) {
metricInformationBuilder.setStatusCode(status);
response.setStatus(status);
setErrorCodes(status);
try {
setPayload(ErrorResponse.create(status, title, description));
} catch (Exception ex) {
@@ -176,21 +186,18 @@ public abstract class RdapActionBase implements Runnable {
}
}
void setErrorCodes(int status) {
metricInformationBuilder.setStatusCode(status);
response.setStatus(status);
}
void setPayload(ReplyPayloadBase replyObject) {
if (requestMethod == Action.Method.HEAD) {
return;
}
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.disableHtmlEscaping();
if (formatOutputParam.orElse(false)) {
gsonBuilder.setPrettyPrinting();
}
Gson gson = gsonBuilder.create();
TopLevelReplyObject topLevelObject =
TopLevelReplyObject.create(replyObject, rdapJsonFormatter.createTosNotice());
Gson gson = formatOutputParam.orElse(false) ? FORMATTED_OUTPUT_GSON : GSON;
response.setPayload(gson.toJson(topLevelObject.toJson()));
}
@@ -119,7 +119,7 @@ final class RdapDataStructures {
*/
@AutoValue
@RestrictJsonNames("notices[]")
abstract static class Notice extends NoticeOrRemark {
public abstract static class Notice extends NoticeOrRemark {
/**
* Notice and Remark Type are defined in 10.2.1 of RFC 9083.
@@ -20,8 +20,11 @@ import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import com.google.common.net.InternetDomainName;
import google.registry.flows.EppException;
import google.registry.flows.domain.DomainFlowUtils;
import google.registry.model.domain.Domain;
import google.registry.model.tld.Tld;
import google.registry.rdap.RdapJsonFormatter.OutputDataType;
import google.registry.rdap.RdapMetrics.EndpointType;
import google.registry.rdap.RdapObjectClasses.RdapDomain;
@@ -51,8 +54,9 @@ public class RdapDomainAction extends RdapActionBase {
// RDAP Technical Implementation Guide 2.1.1 - we must support A-label (Punycode) and U-label
// (Unicode) formats. canonicalizeName will transform Unicode to Punycode so we support both.
pathSearchString = canonicalizeName(pathSearchString);
InternetDomainName domainName;
try {
validateDomainName(pathSearchString);
domainName = validateDomainName(pathSearchString);
} catch (EppException e) {
throw new BadRequestException(
String.format(
@@ -66,6 +70,7 @@ public class RdapDomainAction extends RdapActionBase {
pathSearchString,
shouldIncludeDeleted() ? START_OF_TIME : rdapJsonFormatter.getRequestTime());
if (domain.isEmpty() || !isAuthorized(domain.get())) {
handlePossibleBsaBlock(domainName);
// RFC7480 5.3 - if the server wishes to respond that it doesn't have data satisfying the
// query, it MUST reply with 404 response code.
//
@@ -75,4 +80,17 @@ public class RdapDomainAction extends RdapActionBase {
}
return rdapJsonFormatter.createRdapDomain(domain.get(), OutputDataType.FULL);
}
private void handlePossibleBsaBlock(InternetDomainName domainName) {
Tld tld = Tld.get(domainName.parent().toString());
if (DomainFlowUtils.isBlockedByBsa(domainName.parts().getFirst(), tld, clock.nowUtc())) {
throw new DomainBlockedByBsaException(domainName + " blocked by BSA");
}
}
static class DomainBlockedByBsaException extends RuntimeException {
DomainBlockedByBsaException(String message) {
super(message);
}
}
}
@@ -63,8 +63,21 @@ public class RdapIcannStandardInformation {
.build())
.build();
/** Not required, but provided when a domain is blocked by BSA. */
private static final Notice DOMAIN_BLOCKED_BY_BSA_NOTICE =
Notice.builder()
.setTitle("Blocked Domain")
.setDescription("This name has been blocked by a GlobalBlock service")
.addLink(
Link.builder()
.setRel("alternate")
.setHref("https://brandsafetyalliance.co")
.setType("text/html")
.build())
.build();
/** Boilerplate notices required by domain responses. */
static final ImmutableList<Notice> domainBoilerplateNotices =
static final ImmutableList<Notice> DOMAIN_BOILERPLATE_NOTICES =
ImmutableList.of(
CONFORMANCE_NOTICE,
// RDAP Response Profile 2.6.3
@@ -72,8 +85,12 @@ public class RdapIcannStandardInformation {
// RDAP Response Profile 2.11
INACCURACY_COMPLAINT_FORM_NOTICE);
/** Boilerplate notice for when a domain is blocked by BSA. */
static final ImmutableList<Notice> DOMAIN_BLOCKED_BY_BSA_BOILERPLATE_NOTICES =
ImmutableList.of(DOMAIN_BLOCKED_BY_BSA_NOTICE);
/** Boilerplate remarks required by nameserver and entity responses. */
static final ImmutableList<Notice> nameserverAndEntityBoilerplateNotices =
static final ImmutableList<Notice> NAMESERVER_AND_ENTITY_BOILERPLATE_NOTICES =
ImmutableList.of(CONFORMANCE_NOTICE);
/**
@@ -39,6 +39,7 @@ import google.registry.rdap.RdapDataStructures.RdapConformance;
import google.registry.rdap.RdapDataStructures.RdapStatus;
import google.registry.rdap.RdapDataStructures.Remark;
import google.registry.util.Idn;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Optional;
/** Object Classes defined in RFC 9083 section 5. */
@@ -137,10 +138,22 @@ final class RdapObjectClasses {
* suppress them for other types of responses (e.g. help).
*/
public enum BoilerplateType {
DOMAIN,
NAMESERVER,
ENTITY,
OTHER
DOMAIN(RdapIcannStandardInformation.DOMAIN_BOILERPLATE_NOTICES),
DOMAIN_BLOCKED_BY_BSA(RdapIcannStandardInformation.DOMAIN_BLOCKED_BY_BSA_BOILERPLATE_NOTICES),
NAMESERVER(RdapIcannStandardInformation.NAMESERVER_AND_ENTITY_BOILERPLATE_NOTICES),
ENTITY(RdapIcannStandardInformation.NAMESERVER_AND_ENTITY_BOILERPLATE_NOTICES),
OTHER(ImmutableList.of());
@SuppressWarnings("ImmutableEnumChecker") // immutable lists are, in fact, immutable
private final ImmutableList<Notice> notices;
BoilerplateType(ImmutableList<Notice> notices) {
this.notices = notices;
}
public ImmutableList<Notice> getNotices() {
return notices;
}
}
/**
@@ -173,14 +186,7 @@ final class RdapObjectClasses {
@JsonableElement("notices[]") abstract Notice aTosNotice();
@JsonableElement("notices") ImmutableList<Notice> boilerplateNotices() {
return switch (aAreplyObject().boilerplateType) {
case DOMAIN -> RdapIcannStandardInformation.domainBoilerplateNotices;
case NAMESERVER, ENTITY ->
RdapIcannStandardInformation.nameserverAndEntityBoilerplateNotices;
default -> // things other than domains, nameservers and entities do not yet have
// boilerplate
ImmutableList.of();
};
return aAreplyObject().boilerplateType.getNotices();
}
static TopLevelReplyObject create(ReplyPayloadBase replyObject, Notice tosNotice) {
@@ -532,6 +538,25 @@ final class RdapObjectClasses {
}
}
/** Specialized error response body for when a domain is blocked by BSA. */
@RestrictJsonNames({})
@SuppressWarnings("UnusedVariable")
public static class DomainBlockedByBsaErrorResponse extends ReplyPayloadBase {
@JsonableElement private static final LanguageIdentifier lang = LanguageIdentifier.EN;
@JsonableElement private static final int errorCode = HttpServletResponse.SC_NOT_FOUND;
@JsonableElement private static final String title = "Not Found";
@JsonableElement private final ImmutableList<String> description;
DomainBlockedByBsaErrorResponse(String message) {
super(BoilerplateType.DOMAIN_BLOCKED_BY_BSA);
this.description = ImmutableList.of(message);
}
}
/** Error Response Body defined in 6 of RFC 9083. */
@RestrictJsonNames({})
@AutoValue
@@ -27,7 +27,7 @@ import java.util.List;
* @param numResourcesRetrieved Number of resources retrieved from the database in the process of
* assembling the data set.
*/
record RdapResultSet<T extends EppResource>(
public record RdapResultSet<T extends EppResource>(
ImmutableList<T> resources,
IncompletenessWarningType incompletenessWarningType,
int numResourcesRetrieved) {
@@ -14,9 +14,9 @@
package google.registry.rdap;
import static com.google.common.base.Charsets.UTF_8;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
@@ -69,7 +69,7 @@ public abstract class RdapSearchActionBase extends RdapActionBase {
public final BaseSearchResponse getJsonObjectForResource(
String pathSearchString, boolean isHeadRequest) {
// The pathSearchString is not used by search commands.
if (pathSearchString.length() > 0) {
if (!pathSearchString.isEmpty()) {
throw new BadRequestException("Unexpected path");
}
decodeCursorToken();
@@ -323,7 +323,8 @@ public abstract class RdapSearchActionBase extends RdapActionBase {
if (partialStringQuery.getHasWildcard()) {
builder =
builder.where(
filterField, criteriaBuilder::like,
filterField,
criteriaBuilder::like,
String.format("%s%%", partialStringQuery.getInitialString()));
} else {
// no wildcard means we use a standard equals query
@@ -40,7 +40,7 @@ import java.util.Optional;
abstract class RdapSearchResults {
/** Responding To Searches defined in 8 of RFC 9083. */
abstract static class BaseSearchResponse extends ReplyPayloadBase {
public abstract static class BaseSearchResponse extends ReplyPayloadBase {
abstract IncompletenessWarningType incompletenessWarningType();
abstract ImmutableMap<String, URI> navigationLinks();
@@ -20,6 +20,7 @@ import com.google.common.flogger.FluentLogger;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.logging.Level;
import javax.annotation.Nullable;
/** Base for exceptions that cause an HTTP error response. */
public abstract class HttpException extends RuntimeException {
@@ -33,13 +34,14 @@ public abstract class HttpException extends RuntimeException {
private final int responseCode;
protected HttpException(int responseCode, String message, Throwable cause, Level logLevel) {
protected HttpException(
int responseCode, String message, @Nullable Throwable cause, Level logLevel) {
super(message, cause);
this.responseCode = responseCode;
this.logLevel = logLevel;
}
protected HttpException(int responseCode, String message, Throwable cause) {
protected HttpException(int responseCode, String message, @Nullable Throwable cause) {
this(responseCode, message, cause, Level.INFO);
}
@@ -117,22 +119,6 @@ public abstract class HttpException extends RuntimeException {
}
}
/** Exception that causes a 403 response. */
public static final class ForbiddenException extends HttpException {
public ForbiddenException(String message) {
super(HttpServletResponse.SC_FORBIDDEN, message, null);
}
public ForbiddenException(String message, Exception cause) {
super(HttpServletResponse.SC_FORBIDDEN, message, cause);
}
@Override
public String getResponseCodeString() {
return "Forbidden";
}
}
/** Exception that causes a 404 response. */
public static final class NotFoundException extends HttpException {
public NotFoundException() {
@@ -149,18 +135,6 @@ public abstract class HttpException extends RuntimeException {
}
}
/** Exception that causes a 405 response. */
public static final class MethodNotAllowedException extends HttpException {
public MethodNotAllowedException() {
super(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "Method not allowed", null);
}
@Override
public String getResponseCodeString() {
return "Method Not Allowed";
}
}
/** Exception that causes a 409 response. */
public static final class ConflictException extends HttpException {
public ConflictException(String message) {
@@ -14,12 +14,11 @@
package google.registry.ui.server.console.domains;
import com.google.common.collect.ImmutableMap;
import com.google.gson.JsonElement;
import google.registry.model.console.ConsolePermission;
/** An action that will run a delete EPP command on the given domain. */
public class ConsoleBulkDomainDeleteActionType implements ConsoleDomainActionType {
public class ConsoleBulkDomainDeleteActionType extends ConsoleDomainActionType {
private static final String DOMAIN_DELETE_XML =
"""
@@ -42,16 +41,13 @@ public class ConsoleBulkDomainDeleteActionType implements ConsoleDomainActionTyp
</command>
</epp>""";
private final String reason;
public ConsoleBulkDomainDeleteActionType(JsonElement jsonElement) {
this.reason = jsonElement.getAsJsonObject().get("reason").getAsString();
super(jsonElement);
}
@Override
public String getXmlContentsToRun(String domainName) {
return ConsoleDomainActionType.fillSubstitutions(
DOMAIN_DELETE_XML, ImmutableMap.of("DOMAIN_NAME", domainName, "REASON", reason));
protected String getXmlTemplate() {
return DOMAIN_DELETE_XML;
}
@Override
@@ -14,12 +14,11 @@
package google.registry.ui.server.console.domains;
import com.google.common.collect.ImmutableMap;
import com.google.gson.JsonElement;
import google.registry.model.console.ConsolePermission;
/** An action that will suspend the given domain, assigning all 5 server*Prohibited statuses. */
public class ConsoleBulkDomainSuspendActionType implements ConsoleDomainActionType {
public class ConsoleBulkDomainSuspendActionType extends ConsoleDomainActionType {
private static final String DOMAIN_SUSPEND_XML =
"""
@@ -52,16 +51,13 @@ public class ConsoleBulkDomainSuspendActionType implements ConsoleDomainActionTy
</command>
</epp>""";
private final String reason;
public ConsoleBulkDomainSuspendActionType(JsonElement jsonElement) {
this.reason = jsonElement.getAsJsonObject().get("reason").getAsString();
super(jsonElement);
}
@Override
public String getXmlContentsToRun(String domainName) {
return ConsoleDomainActionType.fillSubstitutions(
DOMAIN_SUSPEND_XML, ImmutableMap.of("DOMAIN_NAME", domainName, "REASON", reason));
protected String getXmlTemplate() {
return DOMAIN_SUSPEND_XML;
}
@Override
@@ -14,12 +14,11 @@
package google.registry.ui.server.console.domains;
import com.google.common.collect.ImmutableMap;
import com.google.gson.JsonElement;
import google.registry.model.console.ConsolePermission;
/** An action that will unsuspend the given domain, removing all 5 server*Prohibited statuses. */
public class ConsoleBulkDomainUnsuspendActionType implements ConsoleDomainActionType {
public class ConsoleBulkDomainUnsuspendActionType extends ConsoleDomainActionType {
private static final String DOMAIN_SUSPEND_XML =
"""
@@ -52,16 +51,13 @@ public class ConsoleBulkDomainUnsuspendActionType implements ConsoleDomainAction
</command>
</epp>""";
private final String reason;
public ConsoleBulkDomainUnsuspendActionType(JsonElement jsonElement) {
this.reason = jsonElement.getAsJsonObject().get("reason").getAsString();
super(jsonElement);
}
@Override
public String getXmlContentsToRun(String domainName) {
return ConsoleDomainActionType.fillSubstitutions(
DOMAIN_SUSPEND_XML, ImmutableMap.of("DOMAIN_NAME", domainName, "REASON", reason));
protected String getXmlTemplate() {
return DOMAIN_SUSPEND_XML;
}
@Override
@@ -14,20 +14,18 @@
package google.registry.ui.server.console.domains;
import com.google.common.collect.ImmutableMap;
import com.google.common.escape.Escaper;
import com.google.common.xml.XmlEscapers;
import com.google.gson.JsonElement;
import google.registry.model.console.ConsolePermission;
import java.util.Map;
/**
* A type of EPP action to perform on domain(s), run by the {@link ConsoleBulkDomainAction}.
*
* <p>Each {@link BulkAction} defines the class that implements that action, including the EPP XML
* that will be run and the permission required.
* <p>Each {@link BulkAction} defines the class that extends that action, including the EPP XML that
* will be run and the permission required.
*/
public interface ConsoleDomainActionType {
public abstract class ConsoleDomainActionType {
enum BulkAction {
DELETE(ConsoleBulkDomainDeleteActionType.class),
@@ -45,20 +43,6 @@ public interface ConsoleDomainActionType {
}
}
Escaper XML_ESCAPER = XmlEscapers.xmlContentEscaper();
static String fillSubstitutions(String xmlTemplate, ImmutableMap<String, String> replacements) {
String xml = xmlTemplate;
for (Map.Entry<String, String> entry : replacements.entrySet()) {
xml = xml.replaceAll("%" + entry.getKey() + "%", XML_ESCAPER.escape(entry.getValue()));
}
return xml;
}
String getXmlContentsToRun(String domainName);
ConsolePermission getNecessaryPermission();
static ConsoleDomainActionType parseActionType(String bulkDomainAction, JsonElement jsonElement) {
BulkAction bulkAction = BulkAction.valueOf(bulkDomainAction);
try {
@@ -67,4 +51,38 @@ public interface ConsoleDomainActionType {
throw new RuntimeException(e); // shouldn't happen
}
}
private static final Escaper XML_ESCAPER = XmlEscapers.xmlContentEscaper();
private final String reason;
public ConsoleDomainActionType(JsonElement jsonElement) {
this.reason = jsonElement.getAsJsonObject().get("reason").getAsString();
}
/** Returns the full XML representing this action, including all substitutions. */
public String getXmlContentsToRun(String domainName) {
return fillSubstitutions(getXmlTemplate(), domainName);
}
/** Returns the permission necessary to successfully perform this action. */
public abstract ConsolePermission getNecessaryPermission();
/** Returns the XML template contents for this action. */
protected abstract String getXmlTemplate();
/**
* Fills out the default set of substitutions in the provided XML template.
*
* <p>Override this method if non-default substitutions are required.
*/
protected String fillSubstitutions(String xmlTemplate, String domainName) {
String xml = xmlTemplate;
xml = replaceValue(xml, "DOMAIN_NAME", domainName);
return replaceValue(xml, "REASON", reason);
}
private String replaceValue(String xml, String key, String value) {
return xml.replaceAll("%" + key + "%", XML_ESCAPER.escape(value));
}
}
@@ -13,11 +13,11 @@
// limitations under the License.
package google.registry.persistence.converter;
import static com.google.common.base.Charsets.US_ASCII;
import static com.google.common.hash.Funnels.stringFunnel;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.insertInDb;
import static java.nio.charset.StandardCharsets.US_ASCII;
import com.google.common.collect.ImmutableSet;
import com.google.common.hash.BloomFilter;
@@ -21,7 +21,6 @@ import static google.registry.testing.DatabaseHelper.insertSimpleResources;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.testcontainers.containers.PostgreSQLContainer.POSTGRESQL_PORT;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
@@ -305,7 +304,7 @@ public abstract class JpaTransactionManagerExtension
private static String readSqlInClassPath(String sqlScriptPath) {
try {
return Resources.toString(Resources.getResource(sqlScriptPath), Charsets.UTF_8);
return Resources.toString(Resources.getResource(sqlScriptPath), UTF_8);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
@@ -85,6 +85,7 @@ abstract class RdapActionBaseTestCase<A extends RdapActionBase> {
action.rdapJsonFormatter = RdapTestHelper.getTestRdapJsonFormatter(clock);
action.rdapMetrics = rdapMetrics;
action.requestMethod = GET;
action.clock = new FakeClock(DateTime.parse("2025-01-01T00:00:00.000Z"));
logout();
}
@@ -15,6 +15,7 @@
package google.registry.rdap;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.bsa.persistence.BsaTestingUtils.persistBsaLabel;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
@@ -27,8 +28,11 @@ import static google.registry.testing.FullFieldsTestEntityHelper.makeHistoryEntr
import static google.registry.testing.FullFieldsTestEntityHelper.makeRegistrar;
import static google.registry.testing.FullFieldsTestEntityHelper.makeRegistrarPocs;
import static google.registry.testing.GsonSubject.assertAboutJson;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.mockito.Mockito.verify;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.gson.JsonObject;
import google.registry.model.contact.Contact;
@@ -608,6 +612,34 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
.build());
}
@Test
void testBlockedByBsa() {
persistResource(
Tld.get("lol").asBuilder().setBsaEnrollStartTime(Optional.of(START_OF_TIME)).build());
persistBsaLabel("example");
ImmutableMap<?, ?> expectedBsaNotice =
ImmutableMap.of(
"description",
ImmutableList.of("This name has been blocked by a GlobalBlock service"),
"title",
"Blocked Domain",
"links",
ImmutableList.of(
ImmutableMap.of(
"href",
"https://brandsafetyalliance.co",
"rel",
"alternate",
"type",
"text/html")));
JsonObject expectedErrorResponse = generateExpectedJsonError("example.lol blocked by BSA", 404);
expectedErrorResponse
.getAsJsonArray("notices")
.add(RdapTestHelper.GSON.toJsonTree(expectedBsaNotice));
assertAboutJson().that(generateActualJson("example.lol")).isEqualTo(expectedErrorResponse);
assertThat(response.getStatus()).isEqualTo(404);
}
private Domain persistActiveDomainWithHost(
String label, String tld, DateTime creationTime, DateTime expirationTime) {
return persistResource(
@@ -33,8 +33,7 @@ import java.util.Map;
/** Test helper methods for RDAP tests. */
class RdapTestHelper {
private static final Gson GSON =
new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
static JsonElement createJson(String... lines) {
return GSON.fromJson(Joiner.on("\n").join(lines), JsonElement.class);
@@ -240,5 +239,4 @@ class RdapTestHelper {
obj.remove("rdapConformance");
return reply;
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1
View File
@@ -184,3 +184,4 @@ V183__remove_fk_billingrecurrence_domainhistory.sql
V184__remove_fk_pollmessage_domainhistory.sql
V185__remove_fk_domaintransactionrecord_domainhistory.sql
V186__remove_fk_domaindsdatahistory_domainhistory.sql
V187__console_update_history.sql
@@ -0,0 +1,34 @@
-- Copyright 2025 The Nomulus Authors. All Rights Reserved.
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
CREATE TABLE "ConsoleUpdateHistory" (
revision_id bigint NOT NULL,
modification_time timestamp with time zone NOT NULL,
method text NOT NULL,
type text NOT NULL,
url text NOT NULL,
description text,
acting_user text NOT NULL,
PRIMARY KEY (revision_id),
CONSTRAINT fk_console_update_history_acting_user
FOREIGN KEY(acting_user)
REFERENCES "User" (email_address)
);
CREATE INDEX IF NOT EXISTS idx_console_update_history_acting_user
ON "ConsoleUpdateHistory" (acting_user);
CREATE INDEX IF NOT EXISTS idx_console_update_history_type
ON "ConsoleUpdateHistory" (type);
CREATE INDEX IF NOT EXISTS idx_console_update_history_modification_time
ON "ConsoleUpdateHistory" (modification_time);
@@ -272,6 +272,21 @@ CREATE TABLE public."ConsoleEppActionHistory" (
);
--
-- Name: ConsoleUpdateHistory; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."ConsoleUpdateHistory" (
revision_id bigint NOT NULL,
modification_time timestamp with time zone NOT NULL,
method text NOT NULL,
type text NOT NULL,
url text NOT NULL,
description text,
acting_user text NOT NULL
);
--
-- Name: Contact; Type: TABLE; Schema: public; Owner: -
--
@@ -1530,6 +1545,14 @@ ALTER TABLE ONLY public."ConsoleEppActionHistory"
ADD CONSTRAINT "ConsoleEppActionHistory_pkey" PRIMARY KEY (history_revision_id);
--
-- Name: ConsoleUpdateHistory ConsoleUpdateHistory_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public."ConsoleUpdateHistory"
ADD CONSTRAINT "ConsoleUpdateHistory_pkey" PRIMARY KEY (revision_id);
--
-- Name: ContactHistory ContactHistory_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@@ -2052,6 +2075,27 @@ CREATE INDEX idx9g3s7mjv1yn4t06nqid39whss ON public."AllocationToken" USING btre
CREATE INDEX idx9q53px6r302ftgisqifmc6put ON public."ContactHistory" USING btree (history_type);
--
-- Name: idx_console_update_history_acting_user; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX idx_console_update_history_acting_user ON public."ConsoleUpdateHistory" USING btree (acting_user);
--
-- Name: idx_console_update_history_modification_time; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX idx_console_update_history_modification_time ON public."ConsoleUpdateHistory" USING btree (modification_time);
--
-- Name: idx_console_update_history_type; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX idx_console_update_history_type ON public."ConsoleUpdateHistory" USING btree (type);
--
-- Name: idx_registry_lock_registrar_id; Type: INDEX; Schema: public; Owner: -
--
@@ -2690,6 +2734,14 @@ ALTER TABLE ONLY public."BillingRecurrence"
ADD CONSTRAINT fk_billing_recurrence_registrar_id FOREIGN KEY (registrar_id) REFERENCES public."Registrar"(registrar_id) DEFERRABLE INITIALLY DEFERRED;
--
-- Name: ConsoleUpdateHistory fk_console_update_history_acting_user; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public."ConsoleUpdateHistory"
ADD CONSTRAINT fk_console_update_history_acting_user FOREIGN KEY (acting_user) REFERENCES public."User"(email_address);
--
-- Name: ContactHistory fk_contact_history_contact_repo_id; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -70,6 +70,7 @@ type Task struct {
URL string `xml:"url"`
Description string `xml:"description"`
Service string `xml:"service"`
Timeout string `xml:"timeout"`
Schedule string `xml:"schedule"`
Name string `xml:"name"`
}
@@ -189,7 +190,9 @@ func (manager TasksSyncManager) getArgs(task Task, operationType string) []strin
description = strings.ReplaceAll(description, "\n", " ")
var service = "backend"
if task.Service != "backend" && task.Service != "" {
// Only BSA tasks run on the BSA service in GAE. GKE tasks are always
// on the backend service.
if task.Service != "backend" && task.Service != "" && !gke {
service = task.Service
}
@@ -200,7 +203,7 @@ func (manager TasksSyncManager) getArgs(task Task, operationType string) []strin
uri = fmt.Sprintf("https://%s-dot-%s.appspot.com%s", service, projectName, strings.TrimSpace(task.URL))
}
return []string{
args := []string{
"--project", projectName,
"scheduler", "jobs", operationType,
"http", task.Name,
@@ -212,6 +215,12 @@ func (manager TasksSyncManager) getArgs(task Task, operationType string) []strin
"--oidc-service-account-email", getCloudSchedulerServiceAccountEmail(),
"--oidc-token-audience", clientId,
}
if task.Timeout != "" {
args = append(args, "--attempt-deadline", task.Timeout)
}
return args
}
func (manager TasksSyncManager) fetchExistingRecords() ExistingEntries {