1
0
mirror of https://github.com/google/nomulus synced 2026-05-22 07:41:50 +00:00

Compare commits

...

5 Commits

Author SHA1 Message Date
Weimin Yu
e42c11051e Download scheduler for BSA (#2209)
* Add BSA download scheduler
2023-11-17 16:15:14 -05:00
Weimin Yu
85b588b51f Add a disposition header to email attachments (#2223)
This may help with the billing-team with attached invoices.

This is a standard header that should do no harm.
2023-11-16 13:31:12 -05:00
Pavlo Tkach
572b7101cb Create separate BSA service (#2221) 2023-11-15 18:38:26 -05:00
Weimin Yu
445825957d Bsa Persistence entity classes (#2205)
* Add persistence model object
2023-11-15 16:43:22 -05:00
Weimin Yu
7ab76f3573 Pin Flyway tool jar to 9.22.3 (#2222)
Flyway 10+ is not compatible with Java 8.

Rollback this change after we upgrade to Java 11.
2023-11-15 14:48:55 -05:00
49 changed files with 1573 additions and 31 deletions

View File

@@ -347,6 +347,7 @@ subprojects {
def services = [':services:default',
':services:backend',
':services:bsa',
':services:tools',
':services:pubapi']

View File

@@ -0,0 +1,21 @@
// Copyright 2023 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.bsa;
/** Identifiers of the BSA lists with blocking labels. */
public enum BlockList {
BLOCK,
BLOCK_PLUS;
}

View File

@@ -0,0 +1,46 @@
// Copyright 2023 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.bsa;
/** The processing stages of a download. */
public enum DownloadStage {
/** Downloads BSA block list files. */
DOWNLOAD,
/** Generates block list diffs with the previous download. */
MAKE_DIFF,
/** Applies the label diffs to the database tables. */
APPLY_DIFF,
/**
* Makes a REST API call to BSA endpoint, declaring that processing starts for new orders in the
* diffs.
*/
START_UPLOADING,
/** Makes a REST API call to BSA endpoint, sending the domains that cannot be blocked. */
UPLOAD_DOMAINS_IN_USE,
/** Makes a REST API call to BSA endpoint, declaring the completion of order processing. */
FINISH_UPLOADING,
/** The terminal stage after processing succeeds. */
DONE,
/**
* The terminal stage indicating that the downloads are discarded because their checksums are the
* same as that of the previous download.
*/
NOP,
/**
* The terminal stage indicating that the downloads are not processed because their BSA-generated
* checksums do not match those calculated by us.
*/
CHECKSUMS_NOT_MATCH;
}

View File

@@ -0,0 +1,45 @@
// Copyright 2023 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.bsa;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import google.registry.request.Action;
import google.registry.request.Action.Service;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import javax.inject.Inject;
@Action(
service = Service.BSA,
path = PlaceholderAction.PATH,
method = Action.Method.GET,
auth = Auth.AUTH_API_ADMIN)
public class PlaceholderAction implements Runnable {
private final Response response;
static final String PATH = "/_dr/task/bsaDownload";
@Inject
public PlaceholderAction(Response response) {
this.response = response;
}
@Override
public void run() {
response.setStatus(SC_OK);
response.setPayload("Hello World");
}
}

View File

@@ -0,0 +1,102 @@
// Copyright 2023 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.bsa.persistence;
import com.google.common.base.Objects;
import google.registry.bsa.persistence.BsaDomainInUse.BsaDomainInUseId;
import google.registry.model.CreateAutoTimestamp;
import google.registry.persistence.VKey;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.Id;
import javax.persistence.IdClass;
/** A domain matching a BSA label but is in use (registered or reserved), so cannot be blocked. */
@Entity
@IdClass(BsaDomainInUseId.class)
public class BsaDomainInUse {
@Id String label;
@Id String tld;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
Reason reason;
/**
* Creation time of this record, which is the most recent time when the domain was detected to be
* in use wrt BSA. It may be during the processing of a download, or during some other job that
* refreshes the state.
*
* <p>This field is for information only.
*/
@SuppressWarnings("unused")
@Column(nullable = false)
CreateAutoTimestamp createTime = CreateAutoTimestamp.create(null);
// For Hibernate
BsaDomainInUse() {}
public BsaDomainInUse(String label, String tld, Reason reason) {
this.label = label;
this.tld = tld;
this.reason = reason;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof BsaDomainInUse)) {
return false;
}
BsaDomainInUse that = (BsaDomainInUse) o;
return Objects.equal(label, that.label)
&& Objects.equal(tld, that.tld)
&& reason == that.reason
&& Objects.equal(createTime, that.createTime);
}
@Override
public int hashCode() {
return Objects.hashCode(label, tld, reason, createTime);
}
enum Reason {
REGISTERED,
RESERVED;
}
static class BsaDomainInUseId implements Serializable {
private String label;
private String tld;
// For Hibernate
BsaDomainInUseId() {}
BsaDomainInUseId(String label, String tld) {
this.label = label;
this.tld = tld;
}
}
static VKey<BsaDomainInUse> vKey(String label, String tld) {
return VKey.create(BsaDomainInUse.class, new BsaDomainInUseId(label, tld));
}
}

View File

@@ -0,0 +1,131 @@
// Copyright 2023 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.bsa.persistence;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static google.registry.bsa.DownloadStage.DOWNLOAD;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.bsa.BlockList;
import google.registry.bsa.DownloadStage;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.UpdateAutoTimestamp;
import google.registry.persistence.VKey;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.Table;
import org.joda.time.DateTime;
/** Records of ongoing and completed download jobs. */
@Entity
@Table(indexes = {@Index(columnList = "creationTime")})
public class BsaDownload {
private static final Joiner CSV_JOINER = Joiner.on(',');
private static final Splitter CSV_SPLITTER = Splitter.on(',');
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long jobId;
@Column(nullable = false)
CreateAutoTimestamp creationTime = CreateAutoTimestamp.create(null);
@Column(nullable = false)
UpdateAutoTimestamp updateTime = UpdateAutoTimestamp.create(null);
@Column(nullable = false)
String blockListChecksums = "";
@Column(nullable = false)
@Enumerated(EnumType.STRING)
DownloadStage stage = DOWNLOAD;
BsaDownload() {}
long getJobId() {
return jobId;
}
DateTime getCreationTime() {
return creationTime.getTimestamp();
}
/**
* Returns the starting time of this job as a string, which can be used as folder name on GCS when
* storing download data.
*/
public String getJobName() {
return getCreationTime().toString();
}
public DownloadStage getStage() {
return this.stage;
}
BsaDownload setStage(DownloadStage stage) {
this.stage = stage;
return this;
}
BsaDownload setChecksums(ImmutableMap<BlockList, String> checksums) {
blockListChecksums =
CSV_JOINER.withKeyValueSeparator("=").join(ImmutableSortedMap.copyOf(checksums));
return this;
}
ImmutableMap<BlockList, String> getChecksums() {
if (blockListChecksums.isEmpty()) {
return ImmutableMap.of();
}
return CSV_SPLITTER.withKeyValueSeparator('=').split(blockListChecksums).entrySet().stream()
.collect(
toImmutableMap(entry -> BlockList.valueOf(entry.getKey()), entry -> entry.getValue()));
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof BsaDownload)) {
return false;
}
BsaDownload that = (BsaDownload) o;
return Objects.equal(creationTime, that.creationTime)
&& Objects.equal(updateTime, that.updateTime)
&& Objects.equal(blockListChecksums, that.blockListChecksums)
&& stage == that.stage;
}
@Override
public int hashCode() {
return Objects.hashCode(creationTime, updateTime, blockListChecksums, stage);
}
static VKey<BsaDownload> vKey(long jobId) {
return VKey.create(BsaDownload.class, Long.valueOf(jobId));
}
}

View File

@@ -0,0 +1,79 @@
// Copyright 2023 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.bsa.persistence;
import com.google.common.base.Objects;
import google.registry.persistence.VKey;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import org.joda.time.DateTime;
/**
* Specifies a second-level TLD name that should be blocked from registration in all TLDs except by
* the label's owner.
*
* <p>The label is valid (wrt IDN) in at least one TLD.
*/
@Entity
public final class BsaLabel {
@Id String label;
/**
* Creation time of this label. This field is for human use, and should give the name of the GCS
* folder that contains the downloaded BSA data.
*
* <p>See {@link BsaDownload#getCreationTime} and {@link BsaDownload#getJobName} for more
* information.
*/
@SuppressWarnings("unused")
@Column(nullable = false)
DateTime creationTime;
// For Hibernate.
BsaLabel() {}
BsaLabel(String label, DateTime creationTime) {
this.label = label;
this.creationTime = creationTime;
}
/** Returns the label to be blocked. */
public String getLabel() {
return label;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof BsaLabel)) {
return false;
}
BsaLabel label1 = (BsaLabel) o;
return Objects.equal(label, label1.label) && Objects.equal(creationTime, label1.creationTime);
}
@Override
public int hashCode() {
return Objects.hashCode(label, creationTime);
}
static VKey<BsaLabel> vKey(String label) {
return VKey.create(BsaLabel.class, label);
}
}

View File

@@ -0,0 +1,73 @@
// Copyright 2023 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.bsa.persistence;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableMap;
import google.registry.bsa.BlockList;
import google.registry.bsa.DownloadStage;
import java.util.Optional;
/** Information needed when handling a download from BSA. */
@AutoValue
public abstract class DownloadSchedule {
abstract long jobId();
public abstract String jobName();
public abstract DownloadStage stage();
/** The most recent job that ended in the {@code DONE} stage. */
public abstract Optional<CompletedJob> latestCompleted();
/**
* Returns true if download should be processed even if the checksums show that it has not changed
* from the previous one.
*/
abstract boolean alwaysDownload();
static DownloadSchedule of(BsaDownload currentJob) {
return new AutoValue_DownloadSchedule(
currentJob.getJobId(),
currentJob.getJobName(),
currentJob.getStage(),
Optional.empty(),
/* alwaysDownload= */ true);
}
static DownloadSchedule of(
BsaDownload currentJob, CompletedJob latestCompleted, boolean alwaysDownload) {
return new AutoValue_DownloadSchedule(
currentJob.getJobId(),
currentJob.getJobName(),
currentJob.getStage(),
Optional.of(latestCompleted),
/* alwaysDownload= */ alwaysDownload);
}
/** Information about a completed BSA download job. */
@AutoValue
public abstract static class CompletedJob {
public abstract String jobName();
public abstract ImmutableMap<BlockList, String> checksums();
static CompletedJob of(BsaDownload completedJob) {
return new AutoValue_DownloadSchedule_CompletedJob(
completedJob.getJobName(), completedJob.getChecksums());
}
}
}

View File

@@ -0,0 +1,131 @@
// Copyright 2023 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.bsa.persistence;
import static com.google.common.base.Verify.verify;
import static google.registry.bsa.DownloadStage.CHECKSUMS_NOT_MATCH;
import static google.registry.bsa.DownloadStage.DONE;
import static google.registry.bsa.DownloadStage.NOP;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static org.joda.time.Duration.standardSeconds;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import google.registry.bsa.persistence.DownloadSchedule.CompletedJob;
import google.registry.util.Clock;
import java.util.Optional;
import javax.inject.Inject;
import org.joda.time.Duration;
/**
* Assigns work for each cron invocation of the BSA Download job.
*
* <p>The download job is invoked at a divisible fraction of the desired data freshness to
* accommodate potential retries. E.g., for 30-minute data freshness with up to two retries on
* error, the cron schedule for the job should be set to 10 minutes.
*
* <p>The processing of each BSA download progresses through multiple stages as described in {@code
* DownloadStage} until it reaches one of the terminal stages. Each stage is check-pointed on
* completion, therefore if an invocation fails mid-process, the next invocation will skip the
* completed stages. No new downloads will start as long as the most recent one is still being
* processed.
*
* <p>When a new download is scheduled, the block list checksums from the most recent completed job
* is included. If the new checksums match the previous ones, the download may be skipped and the
* job should terminate in the {@code NOP} stage. However, if the checksums have stayed unchanged
* for longer than the user-provided {@code maxNopInterval}, the download will be processed.
*
* <p>The BSA downloads contains server-provided checksums. If they do not match the checksums
* generated on Nomulus' side, the download is skipped and the job should terminate in the {@code
* CHECKSUMS_NOT_MATCH} stage.
*/
public final class DownloadScheduler {
/** Allows a new download to proceed if the cron job fires a little early due to NTP drift. */
private static final Duration CRON_JITTER = standardSeconds(5);
private final Duration downloadInterval;
private final Duration maxNopInterval;
private final Clock clock;
@Inject
DownloadScheduler(Duration downloadInterval, Duration maxNopInterval, Clock clock) {
this.downloadInterval = downloadInterval;
this.maxNopInterval = maxNopInterval;
this.clock = clock;
}
/**
* Returns a {@link DownloadSchedule} instance that describes the work to be performed by an
* invocation of the download action, if applicable; or {@link Optional#empty} when there is
* nothing to do.
*/
public Optional<DownloadSchedule> schedule() {
return tm().transact(
() -> {
ImmutableList<BsaDownload> recentJobs = loadRecentProcessedJobs();
if (recentJobs.isEmpty()) {
// No jobs initiated ever.
return Optional.of(scheduleNewJob(Optional.empty()));
}
BsaDownload mostRecent = recentJobs.get(0);
if (mostRecent.getStage().equals(DONE)) {
return isTimeAgain(mostRecent, downloadInterval)
? Optional.of(scheduleNewJob(Optional.of(mostRecent)))
: Optional.empty();
} else if (recentJobs.size() == 1) {
// First job ever, still in progress
return Optional.of(DownloadSchedule.of(recentJobs.get(0)));
} else {
// Job in progress, with completed previous jobs.
BsaDownload prev = recentJobs.get(1);
verify(prev.getStage().equals(DONE), "Unexpectedly found two ongoing jobs.");
return Optional.of(
DownloadSchedule.of(
mostRecent,
CompletedJob.of(prev),
isTimeAgain(mostRecent, maxNopInterval)));
}
});
}
private boolean isTimeAgain(BsaDownload mostRecent, Duration interval) {
return mostRecent.getCreationTime().plus(interval).minus(CRON_JITTER).isBefore(clock.nowUtc());
}
/**
* Adds a new {@link BsaDownload} to the database and returns a {@link DownloadSchedule} for it.
*/
private DownloadSchedule scheduleNewJob(Optional<BsaDownload> prevJob) {
BsaDownload job = new BsaDownload();
tm().insert(job);
return prevJob
.map(
prev ->
DownloadSchedule.of(job, CompletedJob.of(prev), isTimeAgain(prev, maxNopInterval)))
.orElseGet(() -> DownloadSchedule.of(job));
}
@VisibleForTesting
ImmutableList<BsaDownload> loadRecentProcessedJobs() {
return ImmutableList.copyOf(
tm().getEntityManager()
.createQuery(
"FROM BsaDownload WHERE stage NOT IN :nop_stages ORDER BY creationTime DESC")
.setParameter("nop_stages", ImmutableList.of(CHECKSUMS_NOT_MATCH, NOP))
.setMaxResults(2)
.getResultList());
}
}

View File

@@ -1458,6 +1458,15 @@ public final class RegistryConfig {
return makeUrl(CONFIG_SETTINGS.get().gcpProject.backendServiceUrl);
}
/**
* Returns the address of the Nomulus app bsa HTTP server.
*
* <p>This is used by the {@code nomulus} tool to connect to the App Engine remote API.
*/
public static URL getBsaServer() {
return makeUrl(CONFIG_SETTINGS.get().gcpProject.bsaServiceUrl);
}
/**
* Returns the address of the Nomulus app tools HTTP server.
*

View File

@@ -53,6 +53,7 @@ public class RegistryConfigSettings {
public boolean isLocal;
public String defaultServiceUrl;
public String backendServiceUrl;
public String bsaServiceUrl;
public String toolsServiceUrl;
public String pubapiServiceUrl;
}

View File

@@ -20,9 +20,11 @@ gcpProject:
# URLs of the services for the project.
defaultServiceUrl: https://default.example.com
backendServiceUrl: https://backend.example.com
bsaServiceUrl: https://bsa.example.com
toolsServiceUrl: https://tools.example.com
pubapiServiceUrl: https://pubapi.example.com
gSuite:
# Publicly accessible domain name of the running G Suite instance.
domainName: domain-registry.example

View File

@@ -18,13 +18,6 @@
value="alpha"/>
</system-properties>
<!-- Enable external traffic to go through VPC, required for static ip -->
<vpc-access-connector>
<name>projects/domain-registry-alpha/locations/us-central1/connectors/appengine-connector</name>
<egress-setting>all-traffic</egress-setting>
</vpc-access-connector>
<static-files>
<include path="/*.html" expiration="1m"/>
</static-files>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java17</runtime>
<service>bsa</service>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>
<max-instances>100</max-instances>
<idle-timeout>10m</idle-timeout>
</basic-scaling>
<system-properties>
<property name="java.util.logging.config.file"
value="WEB-INF/logging.properties"/>
<property name="google.registry.environment"
value="alpha"/>
</system-properties>
<!-- Enable external traffic to go through VPC, required for static ip -->
<vpc-access-connector>
<name>projects/domain-registry-alpha/locations/us-central1/connectors/appengine-connector</name>
<egress-setting>all-traffic</egress-setting>
</vpc-access-connector>
<static-files>
<include path="/*.html" expiration="1m"/>
</static-files>
</appengine-web-app>

View File

@@ -31,6 +31,12 @@ encoding="UTF-8"?>
<context-root>backend</context-root>
</web>
</module>
<module>
<web>
<web-uri>bsa</web-uri>
<context-root>bsa</context-root>
</web>
</module>
<module>
<web>
<web-uri>tools</web-uri>

View File

@@ -0,0 +1,17 @@
# A default java.util.logging configuration.
# (All App Engine logging is through java.util.logging by default).
#
# To use this configuration, copy it into your application's WEB-INF
# folder and add the following to your appengine-web.xml:
#
# <system-properties>
# <property name="java.util.logging.config.file" value="WEB-INF/logging.properties"/>
# </system-properties>
#
# Set the default logging level for all loggers to INFO.
.level = INFO
# Turn off logging in Hibernate classes for misleading ERROR-level logs
org.hibernate.engine.jdbc.batch.internal.BatchingBatch.level=OFF
org.hibernate.engine.jdbc.spi.SqlExceptionHelper.level=OFF

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee" version="2.5"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<!-- Servlets -->
<!-- Servlet for injected backends actions -->
<servlet>
<display-name>BsaServlet</display-name>
<servlet-name>bsa-servlet</servlet-name>
<servlet-class>google.registry.module.bsa.BsaServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<!-- Test action -->
<servlet-mapping>
<servlet-name>backend-servlet</servlet-name>
<url-pattern>/_dr/task/bsa</url-pattern>
</servlet-mapping>
<!-- Security config -->
<security-constraint>
<web-resource-collection>
<web-resource-name>Internal</web-resource-name>
<description>
Admin-only internal section. Requests for paths covered by the URL patterns below will be
checked for a logged-in user account that's allowed to access the AppEngine admin console
(NOTE: this includes Editor/Viewer permissions in addition to Owner and the new IAM
App Engine Admin role. See https://cloud.google.com/appengine/docs/java/access-control
specifically the "Access handlers that have a login:admin restriction" line.)
TODO(b/28219927): lift some of these restrictions so that we can allow OAuth authentication
for endpoints that need to be accessed by open-source automated processes.
</description>
<!-- Internal AppEngine endpoints. The '_ah' is short for app hosting. -->
<url-pattern>/_ah/*</url-pattern>
<!-- Registrar console (should not be available on non-default module). -->
<url-pattern>/registrar*</url-pattern>
<!-- Verbatim JavaScript sources (only visible to admins for debugging). -->
<url-pattern>/assets/sources/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
<!-- Repeated here since catch-all rule below is not inherited. -->
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
<!-- Require TLS on all requests. -->
<security-constraint>
<web-resource-collection>
<web-resource-name>Secure</web-resource-name>
<description>
Require encryption for all paths. http URLs will be redirected to https.
</description>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
</web-app>

View File

@@ -18,12 +18,6 @@
value="crash"/>
</system-properties>
<!-- Enable external traffic to go through VPC, required for static ip -->
<vpc-access-connector>
<name>projects/domain-registry-crash/locations/us-central1/connectors/appengine-connector</name>
<egress-setting>all-traffic</egress-setting>
</vpc-access-connector>
<static-files>
<include path="/*.html" expiration="1m"/>
</static-files>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java17</runtime>
<service>bsa</service>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>
<max-instances>10</max-instances>
<idle-timeout>10m</idle-timeout>
</basic-scaling>
<system-properties>
<property name="java.util.logging.config.file"
value="WEB-INF/logging.properties"/>
<property name="google.registry.environment"
value="crash"/>
</system-properties>
<!-- Enable external traffic to go through VPC, required for static ip -->
<vpc-access-connector>
<name>projects/domain-registry-crash/locations/us-central1/connectors/appengine-connector</name>
<egress-setting>all-traffic</egress-setting>
</vpc-access-connector>
<static-files>
<include path="/*.html" expiration="1m"/>
</static-files>
</appengine-web-app>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java17</runtime>
<service>bsa</service>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>
<max-instances>10</max-instances>
<idle-timeout>10m</idle-timeout>
</basic-scaling>
<system-properties>
<property name="java.util.logging.config.file"
value="WEB-INF/logging.properties"/>
<property name="google.registry.environment"
value="local"/>
<property name="appengine.generated.dir"
value="/tmp/domain-registry-appengine-generated/local/"/>
</system-properties>
<static-files>
<include path="/*.html">
<http-header name="Cache-Control" value="max-age=0,must-revalidate" />
</include>
</static-files>
</appengine-web-app>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<service>bsa</service>
<!--app-engine-apis>true</app-engine-apis-->
<threadsafe>true</threadsafe>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4_1G</instance-class>
<basic-scaling>
<max-instances>100</max-instances>
<idle-timeout>10m</idle-timeout>
</basic-scaling>
<system-properties>
<property name="java.util.logging.config.file"
value="WEB-INF/logging.properties"/>
<property name="google.registry.environment"
value="production"/>
</system-properties>
<static-files>
<include path="/*.html" expiration="1d"/>
</static-files>
<!-- Prevent uncaught servlet errors from leaking a stack trace. -->
<static-error-handlers>
<handler file="error.html"/>
</static-error-handlers>
</appengine-web-app>

View File

@@ -22,12 +22,6 @@
<include path="/*.html" expiration="1h"/>
</static-files>
<!-- Enable external traffic to go through VPC, required for static ip -->
<vpc-access-connector>
<name>projects/domain-registry-qa/locations/us-central1/connectors/appengine-connector</name>
<egress-setting>all-traffic</egress-setting>
</vpc-access-connector>
<!-- Prevent uncaught servlet errors from leaking a stack trace. -->
<static-error-handlers>
<handler file="error.html"/>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java17</runtime>
<service>bsa</service>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>
<max-instances>10</max-instances>
<idle-timeout>10m</idle-timeout>
</basic-scaling>
<system-properties>
<property name="java.util.logging.config.file"
value="WEB-INF/logging.properties"/>
<property name="google.registry.environment"
value="qa"/>
</system-properties>
<static-files>
<include path="/*.html" expiration="1h"/>
</static-files>
<!-- Enable external traffic to go through VPC, required for static ip -->
<vpc-access-connector>
<name>projects/domain-registry-qa/locations/us-central1/connectors/appengine-connector</name>
<egress-setting>all-traffic</egress-setting>
</vpc-access-connector>
<!-- Prevent uncaught servlet errors from leaking a stack trace. -->
<static-error-handlers>
<handler file="error.html"/>
</static-error-handlers>
</appengine-web-app>

View File

@@ -23,12 +23,6 @@
<include path="/*.html" expiration="1d"/>
</static-files>
<!-- Enable external traffic to go through VPC, required for static ip -->
<vpc-access-connector>
<name>projects/domain-registry-sandbox/locations/us-central1/connectors/appengine-connector</name>
<egress-setting>all-traffic</egress-setting>
</vpc-access-connector>
<!-- Prevent uncaught servlet errors from leaking a stack trace. -->
<static-error-handlers>
<handler file="error.html"/>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<service>bsa</service>
<!--app-engine-apis>true</app-engine-apis-->
<threadsafe>true</threadsafe>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>
<max-instances>100</max-instances>
<idle-timeout>10m</idle-timeout>
</basic-scaling>
<system-properties>
<property name="java.util.logging.config.file"
value="WEB-INF/logging.properties"/>
<property name="google.registry.environment"
value="sandbox"/>
</system-properties>
<static-files>
<include path="/*.html" expiration="1d"/>
</static-files>
<!-- Enable external traffic to go through VPC, required for static ip -->
<vpc-access-connector>
<name>projects/domain-registry-sandbox/locations/us-central1/connectors/appengine-connector</name>
<egress-setting>all-traffic</egress-setting>
</vpc-access-connector>
<!-- Prevent uncaught servlet errors from leaking a stack trace. -->
<static-error-handlers>
<handler file="error.html"/>
</static-error-handlers>
</appengine-web-app>

View File

@@ -138,6 +138,7 @@ public final class GmailClient {
BodyPart attachmentPart = new MimeBodyPart();
attachmentPart.setContent(attachment.content(), attachment.contentType().toString());
attachmentPart.setFileName(attachment.filename());
attachmentPart.setDisposition(MimeBodyPart.ATTACHMENT);
multipart.addBodyPart(attachmentPart);
}
msg.addRecipients(RecipientType.BCC, toArray(emailMessage.bccs(), Address.class));

View File

@@ -0,0 +1,46 @@
// Copyright 2023 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.module.bsa;
import com.google.monitoring.metrics.MetricReporter;
import dagger.Component;
import dagger.Lazy;
import google.registry.config.CredentialModule;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.module.bsa.BsaRequestComponent.BsaRequestComponentModule;
import google.registry.monitoring.whitebox.StackdriverModule;
import google.registry.request.Modules.GsonModule;
import google.registry.request.Modules.UserServiceModule;
import google.registry.request.auth.AuthModule;
import google.registry.util.UtilsModule;
import javax.inject.Singleton;
@Singleton
@Component(
modules = {
AuthModule.class,
UtilsModule.class,
UserServiceModule.class,
GsonModule.class,
ConfigModule.class,
StackdriverModule.class,
CredentialModule.class,
BsaRequestComponentModule.class
})
interface BsaComponent {
BsaRequestHandler requestHandler();
Lazy<MetricReporter> metricReporter();
}

View File

@@ -0,0 +1,45 @@
// Copyright 2023 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.module.bsa;
import dagger.Module;
import dagger.Subcomponent;
import google.registry.bsa.PlaceholderAction;
import google.registry.request.RequestComponentBuilder;
import google.registry.request.RequestModule;
import google.registry.request.RequestScope;
@RequestScope
@Subcomponent(
modules = {
RequestModule.class,
})
interface BsaRequestComponent {
PlaceholderAction bsaAction();
@Subcomponent.Builder
abstract class Builder implements RequestComponentBuilder<BsaRequestComponent> {
@Override
public abstract Builder requestModule(RequestModule requestModule);
@Override
public abstract BsaRequestComponent build();
}
@Module(subcomponents = BsaRequestComponent.class)
class BsaRequestComponentModule {}
}

View File

@@ -0,0 +1,29 @@
// Copyright 2023 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.module.bsa;
import google.registry.request.RequestHandler;
import google.registry.request.auth.RequestAuthenticator;
import javax.inject.Inject;
import javax.inject.Provider;
public class BsaRequestHandler extends RequestHandler<BsaRequestComponent> {
@Inject
public BsaRequestHandler(
Provider<BsaRequestComponent.Builder> componentBuilderProvider,
RequestAuthenticator requestAuthenticator) {
super(componentBuilderProvider, requestAuthenticator);
}
}

View File

@@ -0,0 +1,30 @@
// Copyright 2023 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.module.bsa;
import com.google.monitoring.metrics.MetricReporter;
import dagger.Lazy;
import google.registry.module.ServletBase;
public final class BsaServlet extends ServletBase {
private static final BsaComponent component = DaggerBsaComponent.create();
private static final BsaRequestHandler requestHandler = component.requestHandler();
private static final Lazy<MetricReporter> metricReporter = component.metricReporter();
public BsaServlet() {
super(requestHandler, metricReporter);
}
}

View File

@@ -0,0 +1,16 @@
// Copyright 2023 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.
@javax.annotation.ParametersAreNonnullByDefault
package google.registry.module.bsa;

View File

@@ -39,7 +39,7 @@ import javax.persistence.PersistenceException;
* <p>See the {@code logging.properties} files in the {@code env} package for the specific Hibernate
* classes that have logging suppressed.
*/
class DatabaseException extends PersistenceException {
public class DatabaseException extends PersistenceException {
private transient String cachedMessage;

View File

@@ -30,11 +30,13 @@ public @interface Action {
/** App Engine services supported by the request processor. */
enum Service {
BSA("bsa"),
DEFAULT("default"),
TOOLS("tools"),
BACKEND("backend"),
PUBAPI("pubapi");
private final String serviceId;
Service(String serviceId) {

View File

@@ -170,6 +170,8 @@ public class ServiceConnection {
return RegistryConfig.getToolsServer();
case BACKEND:
return RegistryConfig.getBackendServer();
case BSA:
return RegistryConfig.getBsaServer();
case PUBAPI:
return RegistryConfig.getPubapiServer();
}

View File

@@ -38,6 +38,9 @@
<mapping-file>META-INF/orm.xml</mapping-file>
<class>google.registry.bsa.persistence.BsaDownload</class>
<class>google.registry.bsa.persistence.BsaLabel</class>
<class>google.registry.bsa.persistence.BsaDomainInUse</class>
<class>google.registry.model.billing.BillingCancellation</class>
<class>google.registry.model.billing.BillingEvent</class>
<class>google.registry.model.billing.BillingRecurrence</class>

View File

@@ -0,0 +1,74 @@
// Copyright 2023 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.bsa.persistence;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static org.joda.time.DateTimeZone.UTC;
import static org.junit.jupiter.api.Assertions.assertThrows;
import google.registry.bsa.persistence.BsaDomainInUse.Reason;
import google.registry.persistence.transaction.DatabaseException;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationWithCoverageExtension;
import google.registry.testing.FakeClock;
import org.joda.time.DateTime;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link BsaDomainInUse}. */
public class BsaDomainInUseTest {
protected FakeClock fakeClock = new FakeClock(DateTime.now(UTC));
@RegisterExtension
final JpaIntegrationWithCoverageExtension jpa =
new JpaTestExtensions.Builder().withClock(fakeClock).buildIntegrationWithCoverageExtension();
@Test
void persist() {
tm().transact(() -> tm().put(new BsaLabel("label", fakeClock.nowUtc())));
tm().transact(() -> tm().put(new BsaDomainInUse("label", "tld", Reason.REGISTERED)));
BsaDomainInUse persisted =
tm().transact(() -> tm().loadByKey(BsaDomainInUse.vKey("label", "tld")));
assertThat(persisted.label).isEqualTo("label");
assertThat(persisted.tld).isEqualTo("tld");
assertThat(persisted.reason).isEqualTo(Reason.REGISTERED);
}
@Test
void cascadeDeletion() {
tm().transact(() -> tm().put(new BsaLabel("label", fakeClock.nowUtc())));
tm().transact(() -> tm().put(new BsaDomainInUse("label", "tld", Reason.REGISTERED)));
assertThat(tm().transact(() -> tm().loadByKeyIfPresent(BsaDomainInUse.vKey("label", "tld"))))
.isPresent();
tm().transact(() -> tm().delete(BsaLabel.vKey("label")));
assertThat(tm().transact(() -> tm().loadByKeyIfPresent(BsaDomainInUse.vKey("label", "tld"))))
.isEmpty();
}
@Test
void insertDomainWithoutLabel() {
assertThat(
assertThrows(
DatabaseException.class,
() ->
tm().transact(
() -> tm().put(new BsaDomainInUse("label", "tld", Reason.REGISTERED)))))
.hasMessageThat()
.contains("violates foreign key constraint");
}
}

View File

@@ -0,0 +1,66 @@
// Copyright 2023 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.bsa.persistence;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.bsa.BlockList.BLOCK;
import static google.registry.bsa.BlockList.BLOCK_PLUS;
import static google.registry.bsa.DownloadStage.DOWNLOAD;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static org.joda.time.DateTimeZone.UTC;
import com.google.common.collect.ImmutableMap;
import google.registry.bsa.BlockList;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationWithCoverageExtension;
import google.registry.testing.FakeClock;
import org.joda.time.DateTime;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit test for {@link BsaDownload}. */
public class BsaDownloadTest {
protected FakeClock fakeClock = new FakeClock(DateTime.now(UTC));
@RegisterExtension
final JpaIntegrationWithCoverageExtension jpa =
new JpaTestExtensions.Builder().withClock(fakeClock).buildIntegrationWithCoverageExtension();
@Test
void saveJob() {
BsaDownload persisted = tm().transact(() -> tm().getEntityManager().merge(new BsaDownload()));
assertThat(persisted.jobId).isNotNull();
assertThat(persisted.creationTime.getTimestamp()).isEqualTo(fakeClock.nowUtc());
assertThat(persisted.stage).isEqualTo(DOWNLOAD);
}
@Test
void loadJobByKey() {
BsaDownload persisted = tm().transact(() -> tm().getEntityManager().merge(new BsaDownload()));
assertThat(tm().transact(() -> tm().loadByKey(BsaDownload.vKey(persisted.jobId))))
.isEqualTo(persisted);
}
@Test
void checksums() {
BsaDownload job = new BsaDownload();
assertThat(job.getChecksums()).isEmpty();
ImmutableMap<BlockList, String> checksums = ImmutableMap.of(BLOCK, "a", BLOCK_PLUS, "b");
job.setChecksums(checksums);
assertThat(job.getChecksums()).isEqualTo(checksums);
assertThat(job.blockListChecksums).isEqualTo("BLOCK=a,BLOCK_PLUS=b");
}
}

View File

@@ -0,0 +1,44 @@
// Copyright 2023 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.bsa.persistence;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static org.joda.time.DateTimeZone.UTC;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationWithCoverageExtension;
import google.registry.testing.FakeClock;
import org.joda.time.DateTime;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link BsaLabel}. */
public class BsaLabelTest {
protected FakeClock fakeClock = new FakeClock(DateTime.now(UTC));
@RegisterExtension
final JpaIntegrationWithCoverageExtension jpa =
new JpaTestExtensions.Builder().withClock(fakeClock).buildIntegrationWithCoverageExtension();
@Test
void persist() {
tm().transact(() -> tm().put(new BsaLabel("label", fakeClock.nowUtc())));
BsaLabel persisted = tm().transact(() -> tm().loadByKey(BsaLabel.vKey("label")));
assertThat(persisted.getLabel()).isEqualTo("label");
assertThat(persisted.creationTime).isEqualTo(fakeClock.nowUtc());
}
}

View File

@@ -0,0 +1,210 @@
// Copyright 2023 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.bsa.persistence;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static google.registry.bsa.DownloadStage.CHECKSUMS_NOT_MATCH;
import static google.registry.bsa.DownloadStage.DONE;
import static google.registry.bsa.DownloadStage.DOWNLOAD;
import static google.registry.bsa.DownloadStage.MAKE_DIFF;
import static google.registry.bsa.DownloadStage.NOP;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static org.joda.time.DateTimeZone.UTC;
import static org.joda.time.Duration.standardDays;
import static org.joda.time.Duration.standardMinutes;
import static org.joda.time.Duration.standardSeconds;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import google.registry.bsa.BlockList;
import google.registry.bsa.DownloadStage;
import google.registry.bsa.persistence.DownloadSchedule.CompletedJob;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationWithCoverageExtension;
import google.registry.testing.FakeClock;
import java.util.Optional;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link DownloadScheduler} */
public class DownloadSchedulerTest {
static final Duration DOWNLOAD_INTERVAL = standardMinutes(30);
static final Duration MAX_NOP_INTERVAL = standardDays(1);
protected FakeClock fakeClock = new FakeClock(DateTime.now(UTC));
@RegisterExtension
final JpaIntegrationWithCoverageExtension jpa =
new JpaTestExtensions.Builder().withClock(fakeClock).buildIntegrationWithCoverageExtension();
private DownloadScheduler scheduler;
@BeforeEach
void setup() {
scheduler = new DownloadScheduler(DOWNLOAD_INTERVAL, MAX_NOP_INTERVAL, fakeClock);
}
@AfterEach
void dbCheck() {
ImmutableSet<DownloadStage> terminalStages = ImmutableSet.of(DONE, NOP, CHECKSUMS_NOT_MATCH);
assertThat(
tm().transact(
() ->
tm().getEntityManager()
.createQuery("FROM BsaDownload", BsaDownload.class)
.getResultStream()
.filter(job -> !terminalStages.contains(job.getStage()))
.count()))
.isAtMost(1);
}
@Test
void firstJobEver() {
Optional<DownloadSchedule> scheduleOptional = scheduler.schedule();
assertThat(scheduleOptional).isPresent();
DownloadSchedule schedule = scheduleOptional.get();
assertThat(schedule.latestCompleted()).isEmpty();
assertThat(schedule.jobName()).isEqualTo(fakeClock.nowUtc().toString());
assertThat(schedule.stage()).isEqualTo(DownloadStage.DOWNLOAD);
assertThat(schedule.alwaysDownload()).isTrue();
}
@Test
void oneInProgressJob() {
BsaDownload inProgressJob = insertOneJobAndAdvanceClock(MAKE_DIFF);
Optional<DownloadSchedule> scheduleOptional = scheduler.schedule();
assertThat(scheduleOptional).isPresent();
DownloadSchedule schedule = scheduleOptional.get();
assertThat(schedule.jobId()).isEqualTo(inProgressJob.jobId);
assertThat(schedule.jobName()).isEqualTo(inProgressJob.getJobName());
assertThat(schedule.stage()).isEqualTo(MAKE_DIFF);
assertThat(schedule.latestCompleted()).isEmpty();
assertThat(schedule.alwaysDownload()).isTrue();
}
@Test
void oneInProgressJobOneCompletedJob() {
BsaDownload completed = insertOneJobAndAdvanceClock(DONE);
BsaDownload inProgressJob = insertOneJobAndAdvanceClock(MAKE_DIFF);
Optional<DownloadSchedule> scheduleOptional = scheduler.schedule();
assertThat(scheduleOptional).isPresent();
DownloadSchedule schedule = scheduleOptional.get();
assertThat(schedule.jobId()).isEqualTo(inProgressJob.jobId);
assertThat(schedule.jobName()).isEqualTo(inProgressJob.getJobName());
assertThat(schedule.stage()).isEqualTo(MAKE_DIFF);
assertThat(schedule.alwaysDownload()).isFalse();
assertThat(schedule.latestCompleted()).isPresent();
CompletedJob lastCompleted = schedule.latestCompleted().get();
assertThat(lastCompleted.jobName()).isEqualTo(completed.getJobName());
assertThat(lastCompleted.checksums()).isEqualTo(completed.getChecksums());
}
@Test
void doneJob_noNewSchedule() {
insertOneJobAndAdvanceClock(DONE);
assertThat(scheduler.schedule()).isEmpty();
}
@Test
void doneJob_newSchedule() {
BsaDownload completed = insertOneJobAndAdvanceClock(DONE);
fakeClock.advanceBy(DOWNLOAD_INTERVAL);
Optional<DownloadSchedule> scheduleOptional = scheduler.schedule();
assertThat(scheduleOptional).isPresent();
DownloadSchedule schedule = scheduleOptional.get();
assertThat(schedule.stage()).isEqualTo(DOWNLOAD);
assertThat(schedule.alwaysDownload()).isFalse();
assertThat(schedule.latestCompleted()).isPresent();
CompletedJob completedJob = schedule.latestCompleted().get();
assertThat(completedJob.jobName()).isEqualTo(completed.getJobName());
assertThat(completedJob.checksums()).isEqualTo(completedJob.checksums());
}
@Test
void doneJob_newSchedule_alwaysDownload() {
insertOneJobAndAdvanceClock(DONE);
fakeClock.advanceBy(MAX_NOP_INTERVAL);
Optional<DownloadSchedule> scheduleOptional = scheduler.schedule();
assertThat(scheduleOptional).isPresent();
DownloadSchedule schedule = scheduleOptional.get();
assertThat(schedule.alwaysDownload()).isTrue();
}
@Test
void doneJob_cronEarlyWithJitter_newSchedule() {
insertOneJobAndAdvanceClock(DONE);
fakeClock.advanceBy(DOWNLOAD_INTERVAL.minus(standardSeconds(5)));
assertThat(scheduler.schedule()).isPresent();
}
@Test
void doneJob_cronEarlyMoreThanJitter_newSchedule() {
insertOneJobAndAdvanceClock(DONE);
fakeClock.advanceBy(DOWNLOAD_INTERVAL.minus(standardSeconds(6)));
assertThat(scheduler.schedule()).isEmpty();
}
@Test
void loadRecentProcessedJobs_noneExists() {
assertThat(tm().transact(() -> scheduler.loadRecentProcessedJobs())).isEmpty();
}
@Test
void loadRecentProcessedJobs_nopJobsOnly() {
insertOneJobAndAdvanceClock(DownloadStage.NOP);
insertOneJobAndAdvanceClock(DownloadStage.CHECKSUMS_NOT_MATCH);
assertThat(tm().transact(() -> scheduler.loadRecentProcessedJobs())).isEmpty();
}
@Test
void loadRecentProcessedJobs_oneInProgressJob() {
BsaDownload job = insertOneJobAndAdvanceClock(MAKE_DIFF);
assertThat(tm().transact(() -> scheduler.loadRecentProcessedJobs())).containsExactly(job);
}
@Test
void loadRecentProcessedJobs_oneDoneJob() {
BsaDownload job = insertOneJobAndAdvanceClock(DONE);
assertThat(tm().transact(() -> scheduler.loadRecentProcessedJobs())).containsExactly(job);
}
@Test
void loadRecentProcessedJobs_multipleJobs() {
insertOneJobAndAdvanceClock(DownloadStage.DONE);
insertOneJobAndAdvanceClock(DownloadStage.DONE);
BsaDownload completed = insertOneJobAndAdvanceClock(DownloadStage.DONE);
insertOneJobAndAdvanceClock(DownloadStage.NOP);
insertOneJobAndAdvanceClock(DownloadStage.CHECKSUMS_NOT_MATCH);
BsaDownload inprogress = insertOneJobAndAdvanceClock(DownloadStage.APPLY_DIFF);
assertThat(tm().transact(() -> scheduler.loadRecentProcessedJobs()))
.containsExactly(inprogress, completed)
.inOrder();
}
private BsaDownload insertOneJobAndAdvanceClock(DownloadStage stage) {
BsaDownload job = new BsaDownload();
job.setStage(stage);
job.setChecksums(ImmutableMap.of(BlockList.BLOCK, "1", BlockList.BLOCK_PLUS, "2"));
tm().transact(() -> tm().insert(job));
fakeClock.advanceOneMilli();
return job;
}
}

View File

@@ -138,6 +138,7 @@ public class GmailClientTest {
assertThat(attachment.getContentType()).startsWith(CSV_UTF_8.toString());
assertThat(attachment.getContentType()).endsWith("name=filename");
assertThat(attachment.getContent()).isEqualTo("foo,bar\nbaz,qux");
assertThat(attachment.getDisposition()).isEqualTo("attachment");
}
@Test

View File

@@ -0,0 +1,36 @@
// Copyright 2023 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.module.bsa;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.Test;
class BsaServletTest {
private final HttpServletRequest req = mock(HttpServletRequest.class);
private final HttpServletResponse rsp = mock(HttpServletResponse.class);
@Test
void testService_unknownPath_returnsNotFound() throws Exception {
when(req.getMethod()).thenReturn("GET");
when(req.getRequestURI()).thenReturn("/lol");
new BsaServlet().service(req, rsp);
verify(rsp).sendError(404);
}
}

View File

@@ -16,6 +16,9 @@ package google.registry.schema.integration;
import static com.google.common.truth.Truth.assert_;
import google.registry.bsa.persistence.BsaDomainInUseTest;
import google.registry.bsa.persistence.BsaDownloadTest;
import google.registry.bsa.persistence.BsaLabelTest;
import google.registry.model.billing.BillingBaseTest;
import google.registry.model.common.CursorTest;
import google.registry.model.common.DnsRefreshRequestTest;
@@ -82,6 +85,9 @@ import org.junit.runner.RunWith;
BeforeSuiteTest.class,
AllocationTokenTest.class,
BillingBaseTest.class,
BsaDomainInUseTest.class,
BsaDownloadTest.class,
BsaLabelTest.class,
BulkPricingPackageTest.class,
ClaimsListDaoTest.class,
ContactHistoryTest.class,

View File

@@ -85,6 +85,29 @@
primary key (billing_recurrence_id)
);
create table "BsaDomainInUse" (
label text not null,
tld text not null,
creation_time timestamptz not null,
reason text not null,
primary key (label, tld)
);
create table "BsaDownload" (
job_id bigserial not null,
block_list_checksums text not null,
creation_time timestamptz not null,
stage text not null,
update_timestamp timestamptz,
primary key (job_id)
);
create table "BsaLabel" (
label text not null,
creation_time timestamptz not null,
primary key (label)
);
create table "ClaimsEntry" (
revision_id int8 not null,
domain_label text not null,
@@ -785,6 +808,7 @@ create index IDXoqttafcywwdn41um6kwlt0n8b on "BillingRecurrence" (domain_repo_id
create index IDXp3usbtvk0v1m14i5tdp4xnxgc on "BillingRecurrence" (recurrence_end_time);
create index IDXp0pxi708hlu4n40qhbtihge8x on "BillingRecurrence" (recurrence_last_expansion);
create index IDXjny8wuot75b5e6p38r47wdawu on "BillingRecurrence" (recurrence_time_of_year);
create index IDXj874kw19bgdnkxo1rue45jwlw on "BsaDownload" (creation_time);
create index IDX3y752kr9uh4kh6uig54vemx0l on "Contact" (creation_time);
create index IDXtm415d6fe1rr35stm33s5mg18 on "Contact" (current_sponsor_registrar_id);
create index IDXn1f711wicdnooa2mqb7g1m55o on "Contact" (deletion_time);

View File

@@ -66,6 +66,14 @@ sized to support not just the normal ongoing DNS load but also the load incurred
by MapReduces, both scheduled (such as RDE) and on-demand (asynchronous
contact/host deletion).
#### BSA service
The bsa service is responsible for business logic behind Nomulus and BSA
functionality. Requests to the backend service are handled by the `BsaServlet`,
which provides all of the endpoints exposed in `BsaRequestComponent`. These
include tasks for downloading, processing and uploading BSA data.
#### Tools service
The tools service is responsible for servicing requests from the `nomulus`

View File

@@ -46,7 +46,7 @@ else
-PmavenUrl="${gcs_prefix}"/maven \
-PpluginsUrl="${gcs_prefix}"/plugins
for service in default pubapi backend tools
for service in default pubapi backend bsa tools
do
mv services/"${service}"/build/staged-app "${dest}/${service}"
done

View File

@@ -43,7 +43,7 @@ steps:
gcloud auth activate-service-account --key-file=tool-credential.json
for service in default pubapi backend tools
for service in default pubapi backend bsa tools
do
for version in $(gcloud app versions list \
--filter="SERVICE:$service AND SERVING_STATUS:STOPPED" \

View File

@@ -75,7 +75,7 @@ steps:
gcloud app versions list \
--project $project_id --hide-no-traffic \
--format="csv[no-heading](SERVICE,VERSION.ID)" | \
grep -e "^backend\|^default\|^pubapi\|^tools" |\
grep -e "^backend\|^default\|^bsa\|^pubapi\|^tools" |\
while read line; do echo "${TAG_NAME},$line"; done | tee "$local_map"
num_versions=$(cat "$local_map" | wc -l)
if [ "$num_versions" -ne 4 ]; then

View File

@@ -30,8 +30,7 @@ FROM gcr.io/${PROJECT_ID}/builder:${TAG_NAME}
COPY deploy_sql_schema.sh /usr/local/bin/
RUN \
FLYWAY_MAVEN=https://repo1.maven.org/maven2/org/flywaydb/flyway-commandline \
&& FLYWAY_VERSION=$(curl ${FLYWAY_MAVEN}/maven-metadata.xml \
| grep -oP "<release>\K.*(?=</release>)") \
&& FLYWAY_VERSION="9.22.3" \
&& echo "Downloading Flyway-commandline-${FLYWAY_VERSION}" \
&& mkdir -p /flyway \
&& curl -L ${FLYWAY_MAVEN}/${FLYWAY_VERSION}/flyway-commandline-${FLYWAY_VERSION}.tar.gz \

View File

@@ -43,6 +43,7 @@ include 'proxy'
include 'util'
include 'services:default'
include 'services:backend'
include 'services:bsa'
include 'services:tools'
include 'services:pubapi'
include 'java8compatibility'