diff --git a/core/src/main/java/google/registry/batch/SendExpiringCertificateNotificationEmailAction.java b/core/src/main/java/google/registry/batch/SendExpiringCertificateNotificationEmailAction.java
new file mode 100644
index 000000000..397e5de94
--- /dev/null
+++ b/core/src/main/java/google/registry/batch/SendExpiringCertificateNotificationEmailAction.java
@@ -0,0 +1,326 @@
+// Copyright 2021 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.batch;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
+import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
+import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
+import static org.apache.http.HttpStatus.SC_OK;
+import static org.joda.time.DateTimeZone.UTC;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Streams;
+import com.google.common.flogger.FluentLogger;
+import com.google.common.net.MediaType;
+import google.registry.config.RegistryConfig.Config;
+import google.registry.flows.certs.CertificateChecker;
+import google.registry.model.registrar.Registrar;
+import google.registry.model.registrar.RegistrarContact;
+import google.registry.model.registrar.RegistrarContact.Type;
+import google.registry.request.Action;
+import google.registry.request.Response;
+import google.registry.request.auth.Auth;
+import google.registry.util.EmailMessage;
+import google.registry.util.SendEmailService;
+import java.util.Date;
+import java.util.Optional;
+import javax.inject.Inject;
+import javax.mail.internet.AddressException;
+import javax.mail.internet.InternetAddress;
+import org.joda.time.DateTime;
+import org.joda.time.Duration;
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
+
+/** An action that sends notification emails to registrars whose certificates are expiring soon. */
+@Action(
+ service = Action.Service.BACKEND,
+ path = SendExpiringCertificateNotificationEmailAction.PATH,
+ auth = Auth.AUTH_INTERNAL_OR_ADMIN)
+public class SendExpiringCertificateNotificationEmailAction implements Runnable {
+ public static final String PATH = "/_dr/task/sendExpiringCertificateNotificationEmail";
+ /**
+ * Used as an offset when storing the last notification email sent date.
+ *
+ *
This is used to handle edges cases when the update happens in between the day switch. For
+ * instance,if the job starts at 2:00 am every day and it finishes at 2:03 of the same day, then
+ * next day at 2am, the date difference will be less than a day, which will lead to the date
+ * difference between two successive email sent date being the expected email interval days + 1;
+ */
+ protected static final Duration UPDATE_TIME_OFFSET = Duration.standardMinutes(10);
+
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd");
+
+ private final CertificateChecker certificateChecker;
+ private final String expirationWarningEmailBodyText;
+ private final SendEmailService sendEmailService;
+ private final String expirationWarningEmailSubjectText;
+ private final InternetAddress gSuiteOutgoingEmailAddress;
+ private final Response response;
+
+ @Inject
+ public SendExpiringCertificateNotificationEmailAction(
+ @Config("expirationWarningEmailBodyText") String expirationWarningEmailBodyText,
+ @Config("expirationWarningEmailSubjectText") String expirationWarningEmailSubjectText,
+ @Config("gSuiteOutgoingEmailAddress") InternetAddress gSuiteOutgoingEmailAddress,
+ SendEmailService sendEmailService,
+ CertificateChecker certificateChecker,
+ Response response) {
+ this.certificateChecker = certificateChecker;
+ this.expirationWarningEmailSubjectText = expirationWarningEmailSubjectText;
+ this.sendEmailService = sendEmailService;
+ this.gSuiteOutgoingEmailAddress = gSuiteOutgoingEmailAddress;
+ this.expirationWarningEmailBodyText = expirationWarningEmailBodyText;
+ this.response = response;
+ }
+
+ @Override
+ public void run() {
+ response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
+ try {
+ sendNotificationEmails();
+ response.setStatus(SC_OK);
+ } catch (Exception e) {
+ logger.atWarning().withCause(e).log(
+ "Exception thrown when sending expiring certificate notification emails.");
+ response.setStatus(SC_INTERNAL_SERVER_ERROR);
+ response.setPayload(String.format("Exception thrown with cause: %s", e));
+ }
+ }
+
+ /**
+ * Returns a list of registrars that should receive expiring notification emails. There are two
+ * certificates that should be considered (the main certificate and failOver certificate). The
+ * registrars should receive notifications if one of the certificate checks returns true.
+ */
+ @VisibleForTesting
+ ImmutableList getRegistrarsWithExpiringCertificates() {
+ return Streams.stream(Registrar.loadAllCached())
+ .map(
+ registrar ->
+ RegistrarInfo.create(
+ registrar,
+ registrar.getClientCertificate().isPresent()
+ && certificateChecker.shouldReceiveExpiringNotification(
+ registrar.getLastExpiringCertNotificationSentDate(),
+ registrar.getClientCertificate().get()),
+ registrar.getFailoverClientCertificate().isPresent()
+ && certificateChecker.shouldReceiveExpiringNotification(
+ registrar.getLastExpiringFailoverCertNotificationSentDate(),
+ registrar.getFailoverClientCertificate().get())))
+ .filter(
+ registrarInfo ->
+ registrarInfo.isCertExpiring() || registrarInfo.isFailOverCertExpiring())
+ .collect(toImmutableList());
+ }
+
+ /**
+ * Sends a notification email to the registrar regarding the expiring certificate and returns true
+ * if it's sent successfully.
+ */
+ @VisibleForTesting
+ boolean sendNotificationEmail(
+ Registrar registrar,
+ DateTime lastExpiringCertNotificationSentDate,
+ CertificateType certificateType,
+ Optional certificate) {
+ if (!certificate.isPresent()
+ || !certificateChecker.shouldReceiveExpiringNotification(
+ lastExpiringCertNotificationSentDate, certificate.get())) {
+ return false;
+ }
+ try {
+ ImmutableSet recipients = getEmailAddresses(registrar, Type.TECH);
+ if (recipients.isEmpty()) {
+ logger.atWarning().log(
+ "Registrar %s contains no email addresses to receive notification email.",
+ registrar.getRegistrarName());
+ return false;
+ }
+ sendEmailService.sendEmail(
+ EmailMessage.newBuilder()
+ .setFrom(gSuiteOutgoingEmailAddress)
+ .setSubject(expirationWarningEmailSubjectText)
+ .setBody(
+ getEmailBody(
+ registrar.getRegistrarName(),
+ certificateType,
+ certificateChecker.getCertificate(certificate.get()).getNotAfter()))
+ .setRecipients(recipients)
+ .setCcs(getEmailAddresses(registrar, Type.ADMIN))
+ .build());
+ /*
+ * A duration time offset is used here to ensure that date comparison between two
+ * successive dates is always greater than 1 day. This date is set as last updated date,
+ * for applicable certificate.
+ */
+ updateLastNotificationSentDate(
+ registrar,
+ DateTime.now(UTC).minusMinutes((int) UPDATE_TIME_OFFSET.getStandardMinutes()),
+ certificateType);
+ return true;
+ } catch (Exception e) {
+ throw new RuntimeException(
+ String.format(
+ "Failed to send expiring certificate notification email to registrar %s.",
+ registrar.getRegistrarName()));
+ }
+ }
+
+ /** Updates the last notification sent date in database. */
+ @VisibleForTesting
+ void updateLastNotificationSentDate(
+ Registrar registrar, DateTime now, CertificateType certificateType) {
+ try {
+ tm().transact(
+ () -> {
+ Registrar.Builder newRegistrar = tm().loadByEntity(registrar).asBuilder();
+ switch (certificateType) {
+ case PRIMARY:
+ newRegistrar.setLastExpiringCertNotificationSentDate(now);
+ tm().put(newRegistrar.build());
+ logger.atInfo().log(
+ "Updated last notification email sent date for %s certificate of "
+ + "registrar %s.",
+ certificateType.getDisplayName(), registrar.getRegistrarName());
+ break;
+ case FAILOVER:
+ newRegistrar.setLastExpiringFailoverCertNotificationSentDate(now);
+ tm().put(newRegistrar.build());
+ logger.atInfo().log(
+ "Updated last notification email sent date for %s certificate of "
+ + "registrar %s.",
+ certificateType.getDisplayName(), registrar.getRegistrarName());
+ break;
+ default:
+ throw new IllegalArgumentException(
+ String.format(
+ "Unsupported certificate type: %s being passed in when updating "
+ + "the last notification sent date to registrar %s.",
+ certificateType.toString(), registrar.getRegistrarName()));
+ }
+ });
+ } catch (Exception e) {
+ throw new RuntimeException(
+ String.format(
+ "Failed to update the last notification sent date to Registrar %s for the %s "
+ + "certificate.",
+ registrar.getRegistrarName(), certificateType.getDisplayName()));
+ }
+ }
+
+ /** Sends notification emails to registrars with expiring certificates. */
+ @VisibleForTesting
+ int sendNotificationEmails() {
+ int emailsSent = 0;
+ for (RegistrarInfo registrarInfo : getRegistrarsWithExpiringCertificates()) {
+ Registrar registrar = registrarInfo.registrar();
+ if (registrarInfo.isCertExpiring()) {
+ sendNotificationEmail(
+ registrar,
+ registrar.getLastExpiringCertNotificationSentDate(),
+ CertificateType.PRIMARY,
+ registrar.getClientCertificate());
+ emailsSent++;
+ }
+ if (registrarInfo.isFailOverCertExpiring()) {
+ sendNotificationEmail(
+ registrar,
+ registrar.getLastExpiringFailoverCertNotificationSentDate(),
+ CertificateType.FAILOVER,
+ registrar.getFailoverClientCertificate());
+ emailsSent++;
+ }
+ }
+ logger.atInfo().log(
+ "Sent %d expiring certificate notification emails to registrars.", emailsSent);
+ return emailsSent;
+ }
+
+ /** Returns a list of email addresses of the registrar that should receive a notification email */
+ @VisibleForTesting
+ ImmutableSet getEmailAddresses(Registrar registrar, Type contactType) {
+ ImmutableSortedSet contacts = registrar.getContactsOfType(contactType);
+ ImmutableSet.Builder recipientEmails = new ImmutableSet.Builder<>();
+ for (RegistrarContact contact : contacts) {
+ try {
+ recipientEmails.add(new InternetAddress(contact.getEmailAddress()));
+ } catch (AddressException e) {
+ logger.atWarning().withCause(e).log(
+ "Registrar Contact email address %s of Registrar %s is invalid; skipping.",
+ contact.getEmailAddress(), registrar.getRegistrarName());
+ }
+ }
+ return recipientEmails.build();
+ }
+
+ /**
+ * Generates email content by taking registrar name, certificate type and expiration date as
+ * parameters.
+ */
+ @VisibleForTesting
+ @SuppressWarnings("lgtm[java/dereferenced-value-may-be-null]")
+ String getEmailBody(String registrarName, CertificateType type, Date expirationDate) {
+ checkArgumentNotNull(expirationDate, "Expiration date cannot be null");
+ checkArgumentNotNull(type, "Certificate type cannot be null");
+ return String.format(
+ expirationWarningEmailBodyText,
+ registrarName,
+ type.getDisplayName(),
+ DATE_FORMATTER.print(new DateTime(expirationDate)));
+ }
+
+ /**
+ * Certificate types for X509Certificate.
+ *
+ * Note: These types are only used to indicate the type of expiring certificate in
+ * notification emails.
+ */
+ protected enum CertificateType {
+ PRIMARY("primary"),
+ FAILOVER("fail-over");
+
+ private final String displayName;
+
+ CertificateType(String displayName) {
+ this.displayName = displayName;
+ }
+
+ public String getDisplayName() {
+ return displayName;
+ }
+ }
+
+ @AutoValue
+ public abstract static class RegistrarInfo {
+ static RegistrarInfo create(
+ Registrar registrar, boolean isCertExpiring, boolean isFailOverCertExpiring) {
+ return new AutoValue_SendExpiringCertificateNotificationEmailAction_RegistrarInfo(
+ registrar, isCertExpiring, isFailOverCertExpiring);
+ }
+
+ public abstract Registrar registrar();
+
+ public abstract boolean isCertExpiring();
+
+ public abstract boolean isFailOverCertExpiring();
+ }
+}
diff --git a/core/src/main/java/google/registry/env/common/backend/WEB-INF/web.xml b/core/src/main/java/google/registry/env/common/backend/WEB-INF/web.xml
index 65e0ee205..627075b8c 100644
--- a/core/src/main/java/google/registry/env/common/backend/WEB-INF/web.xml
+++ b/core/src/main/java/google/registry/env/common/backend/WEB-INF/web.xml
@@ -355,6 +355,12 @@
/_dr/task/deleteExpiredDomains
+
+
+ backend-servlet
+ /_dr/task/sendExpiringCertificateNotificationEmail
+
+
backend-servlet
diff --git a/core/src/main/java/google/registry/env/sandbox/default/WEB-INF/cron.xml b/core/src/main/java/google/registry/env/sandbox/default/WEB-INF/cron.xml
index 0a3ba76b4..472bb074c 100644
--- a/core/src/main/java/google/registry/env/sandbox/default/WEB-INF/cron.xml
+++ b/core/src/main/java/google/registry/env/sandbox/default/WEB-INF/cron.xml
@@ -168,6 +168,15 @@
backend
+
+
+
+ This job runs an action that sends emails to partners if their certificates are expiring soon.
+
+ every day 04:30
+ backend
+
+
diff --git a/core/src/main/java/google/registry/module/backend/BackendRequestComponent.java b/core/src/main/java/google/registry/module/backend/BackendRequestComponent.java
index 09070c12c..e6a25b00a 100644
--- a/core/src/main/java/google/registry/module/backend/BackendRequestComponent.java
+++ b/core/src/main/java/google/registry/module/backend/BackendRequestComponent.java
@@ -31,6 +31,7 @@ import google.registry.batch.RefreshDnsOnHostRenameAction;
import google.registry.batch.RelockDomainAction;
import google.registry.batch.ResaveAllEppResourcesAction;
import google.registry.batch.ResaveEntityAction;
+import google.registry.batch.SendExpiringCertificateNotificationEmailAction;
import google.registry.batch.WipeOutCloudSqlAction;
import google.registry.batch.WipeoutDatastoreAction;
import google.registry.cron.CommitLogFanoutAction;
@@ -193,6 +194,8 @@ interface BackendRequestComponent {
ResaveEntityAction resaveEntityAction();
+ SendExpiringCertificateNotificationEmailAction sendExpiringCertificateNotificationEmailAction();
+
SyncGroupMembersAction syncGroupMembersAction();
SyncRegistrarsSheetAction syncRegistrarsSheetAction();
diff --git a/core/src/test/java/google/registry/batch/SendExpiringCertificateNotificationEmailActionTest.java b/core/src/test/java/google/registry/batch/SendExpiringCertificateNotificationEmailActionTest.java
new file mode 100644
index 000000000..3f66130ed
--- /dev/null
+++ b/core/src/test/java/google/registry/batch/SendExpiringCertificateNotificationEmailActionTest.java
@@ -0,0 +1,607 @@
+// Copyright 2021 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.batch;
+
+import static com.google.common.truth.Truth.assertThat;
+import static google.registry.testing.AppEngineExtension.makeRegistrar1;
+import static google.registry.testing.DatabaseHelper.loadByEntity;
+import static google.registry.testing.DatabaseHelper.persistResource;
+import static google.registry.testing.DatabaseHelper.persistSimpleResources;
+import static google.registry.util.DateTimeUtils.START_OF_TIME;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedMap;
+import google.registry.batch.SendExpiringCertificateNotificationEmailAction.CertificateType;
+import google.registry.batch.SendExpiringCertificateNotificationEmailAction.RegistrarInfo;
+import google.registry.flows.certs.CertificateChecker;
+import google.registry.model.registrar.Registrar;
+import google.registry.model.registrar.RegistrarAddress;
+import google.registry.model.registrar.RegistrarContact;
+import google.registry.model.registrar.RegistrarContact.Type;
+import google.registry.request.Response;
+import google.registry.testing.AppEngineExtension;
+import google.registry.testing.DualDatabaseTest;
+import google.registry.testing.FakeClock;
+import google.registry.testing.InjectExtension;
+import google.registry.testing.TestOfyAndSql;
+import google.registry.util.SelfSignedCaCertificate;
+import google.registry.util.SendEmailService;
+import java.security.cert.X509Certificate;
+import java.util.Optional;
+import javax.annotation.Nullable;
+import javax.mail.internet.InternetAddress;
+import org.joda.time.DateTime;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+/** Unit tests for {@link SendExpiringCertificateNotificationEmailAction}. */
+@DualDatabaseTest
+class SendExpiringCertificateNotificationEmailActionTest {
+
+ @RegisterExtension
+ public final AppEngineExtension appEngine =
+ AppEngineExtension.builder().withDatastoreAndCloudSql().withTaskQueue().build();
+
+ @RegisterExtension public final InjectExtension inject = new InjectExtension();
+ private final FakeClock clock = new FakeClock(DateTime.parse("2021-05-24T20:21:22Z"));
+ private final SendEmailService sendEmailService = mock(SendEmailService.class);
+ private CertificateChecker certificateChecker;
+ private SendExpiringCertificateNotificationEmailAction action;
+ private Registrar sampleRegistrar;
+ private Response response;
+
+ @BeforeEach
+ void beforeEach() throws Exception {
+ certificateChecker =
+ new CertificateChecker(
+ ImmutableSortedMap.of(START_OF_TIME, 825, DateTime.parse("2020-09-01T00:00:00Z"), 398),
+ 30,
+ 15,
+ 2048,
+ ImmutableSet.of("secp256r1", "secp384r1"),
+ clock);
+ String expirationWarningEmailBodyText =
+ " Hello Registrar %s,\n" + " The %s certificate is expiring on %s.";
+ String expirationWarningEmailSubjectText = "expiring certificate notification email";
+
+ action =
+ new SendExpiringCertificateNotificationEmailAction(
+ expirationWarningEmailBodyText,
+ expirationWarningEmailSubjectText,
+ new InternetAddress("test@example.com"),
+ sendEmailService,
+ certificateChecker,
+ response);
+
+ sampleRegistrar =
+ persistResource(createRegistrar("clientId", "sampleRegistrar", null, null).build());
+ }
+
+ /** Returns a sample registrar with a customized registrar name, client id and certificate* */
+ private Registrar.Builder createRegistrar(
+ String clientId,
+ String registrarName,
+ @Nullable X509Certificate certificate,
+ @Nullable X509Certificate failOverCertificate)
+ throws Exception {
+ // set up only required fields sample test data
+ Registrar.Builder builder =
+ new Registrar.Builder()
+ .setClientId(clientId)
+ .setRegistrarName(registrarName)
+ .setType(Registrar.Type.REAL)
+ .setIanaIdentifier(8L)
+ .setState(Registrar.State.ACTIVE)
+ .setInternationalizedAddress(
+ new RegistrarAddress.Builder()
+ .setStreet(ImmutableList.of("very fake street"))
+ .setCity("city")
+ .setState("state")
+ .setZip("99999")
+ .setCountryCode("US")
+ .build())
+ .setPhoneNumber("+0.000000000")
+ .setFaxNumber("+9.999999999")
+ .setEmailAddress("contact-us@test.example")
+ .setWhoisServer("whois.registrar.example")
+ .setUrl("http://www.test.example");
+
+ if (failOverCertificate != null) {
+ builder.setFailoverClientCertificate(
+ certificateChecker.serializeCertificate(failOverCertificate), clock.nowUtc());
+ }
+ if (certificate != null) {
+ builder.setClientCertificate(
+ certificateChecker.serializeCertificate(certificate), clock.nowUtc());
+ }
+ return builder;
+ }
+
+ @TestOfyAndSql
+ void sendNotificationEmail_returnsTrue() throws Exception {
+ X509Certificate expiringCertificate =
+ SelfSignedCaCertificate.create(
+ "www.example.tld",
+ DateTime.parse("2020-09-02T00:00:00Z"),
+ DateTime.parse("2021-06-01T00:00:00Z"))
+ .cert();
+ Optional cert =
+ Optional.of(certificateChecker.serializeCertificate(expiringCertificate));
+ Registrar registrar =
+ persistResource(
+ makeRegistrar1()
+ .asBuilder()
+ .setFailoverClientCertificate(cert.get(), clock.nowUtc())
+ .build());
+ ImmutableList contacts =
+ ImmutableList.of(
+ new RegistrarContact.Builder()
+ .setParent(registrar)
+ .setName("Will Doe")
+ .setEmailAddress("will@example-registrar.tld")
+ .setPhoneNumber("+1.3105551213")
+ .setFaxNumber("+1.3105551213")
+ .setTypes(ImmutableSet.of(RegistrarContact.Type.TECH))
+ .setVisibleInWhoisAsAdmin(true)
+ .setVisibleInWhoisAsTech(false)
+ .build());
+ persistSimpleResources(contacts);
+ persistResource(registrar);
+ assertThat(
+ action.sendNotificationEmail(registrar, START_OF_TIME, CertificateType.FAILOVER, cert))
+ .isEqualTo(true);
+ }
+
+ @TestOfyAndSql
+ void sendNotificationEmail_returnsFalse_noEmailRecipients() throws Exception {
+ X509Certificate expiringCertificate =
+ SelfSignedCaCertificate.create(
+ "www.example.tld",
+ DateTime.parse("2020-09-02T00:00:00Z"),
+ DateTime.parse("2021-06-02T00:00:00Z"))
+ .cert();
+ Optional cert =
+ Optional.of(certificateChecker.serializeCertificate(expiringCertificate));
+ assertThat(
+ action.sendNotificationEmail(
+ sampleRegistrar, START_OF_TIME, CertificateType.FAILOVER, cert))
+ .isEqualTo(false);
+ }
+
+ @TestOfyAndSql
+ void sendNotificationEmail_throwsRunTimeException() throws Exception {
+ doThrow(new RuntimeException("this is a runtime exception"))
+ .when(sendEmailService)
+ .sendEmail(any());
+ X509Certificate expiringCertificate =
+ SelfSignedCaCertificate.create(
+ "www.example.tld",
+ DateTime.parse("2020-09-02T00:00:00Z"),
+ DateTime.parse("2021-06-01T00:00:00Z"))
+ .cert();
+ Optional cert =
+ Optional.of(certificateChecker.serializeCertificate(expiringCertificate));
+ Registrar registrar =
+ persistResource(
+ makeRegistrar1()
+ .asBuilder()
+ .setFailoverClientCertificate(cert.get(), clock.nowUtc())
+ .build());
+ ImmutableList contacts =
+ ImmutableList.of(
+ new RegistrarContact.Builder()
+ .setParent(registrar)
+ .setName("Will Doe")
+ .setEmailAddress("will@example-registrar.tld")
+ .setPhoneNumber("+1.3105551213")
+ .setFaxNumber("+1.3105551213")
+ .setTypes(ImmutableSet.of(RegistrarContact.Type.TECH))
+ .setVisibleInWhoisAsAdmin(true)
+ .setVisibleInWhoisAsTech(false)
+ .build());
+ persistSimpleResources(contacts);
+ RuntimeException thrown =
+ assertThrows(
+ RuntimeException.class,
+ () ->
+ action.sendNotificationEmail(
+ registrar, START_OF_TIME, CertificateType.FAILOVER, cert));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains(
+ String.format(
+ "Failed to send expiring certificate notification email to registrar %s",
+ registrar.getRegistrarName()));
+ }
+
+ @TestOfyAndSql
+ void sendNotificationEmail_returnsFalse_noCertificate() {
+ assertThat(
+ action.sendNotificationEmail(
+ sampleRegistrar, START_OF_TIME, CertificateType.FAILOVER, Optional.empty()))
+ .isEqualTo(false);
+ }
+
+ @TestOfyAndSql
+ void sendNotificationEmails_allEmailsBeingAttemptedToSend() throws Exception {
+ X509Certificate expiringCertificate =
+ SelfSignedCaCertificate.create(
+ "www.example.tld",
+ DateTime.parse("2020-09-02T00:00:00Z"),
+ DateTime.parse("2021-06-01T00:00:00Z"))
+ .cert();
+ X509Certificate certificate =
+ SelfSignedCaCertificate.create(
+ "www.example.tld",
+ DateTime.parse("2020-09-02T00:00:00Z"),
+ DateTime.parse("2021-10-01T00:00:00Z"))
+ .cert();
+ int numOfRegistrars = 10;
+ int numOfRegistrarsWithExpiringCertificates = 2;
+ for (int i = 1; i <= numOfRegistrarsWithExpiringCertificates; i++) {
+ persistResource(
+ createRegistrar("oldcert" + i, "name" + i, expiringCertificate, null).build());
+ }
+ for (int i = numOfRegistrarsWithExpiringCertificates; i <= numOfRegistrars; i++) {
+ persistResource(createRegistrar("goodcert" + i, "name" + i, certificate, null).build());
+ }
+ assertThat(action.sendNotificationEmails()).isEqualTo(numOfRegistrarsWithExpiringCertificates);
+ }
+
+ @TestOfyAndSql
+ void sendNotificationEmails_allEmailsBeingAttemptedToSend_onlyMainCertificates()
+ throws Exception {
+ X509Certificate expiringCertificate =
+ SelfSignedCaCertificate.create(
+ "www.example.tld",
+ DateTime.parse("2020-09-02T00:00:00Z"),
+ DateTime.parse("2021-06-01T00:00:00Z"))
+ .cert();
+ int numOfRegistrars = 10;
+ for (int i = 1; i <= numOfRegistrars; i++) {
+ persistResource(
+ createRegistrar("oldcert" + i, "name" + i, expiringCertificate, null).build());
+ }
+ assertThat(action.sendNotificationEmails()).isEqualTo(numOfRegistrars);
+ }
+
+ @TestOfyAndSql
+ void sendNotificationEmails_allEmailsBeingAttemptedToSend_onlyFailOverCertificates()
+ throws Exception {
+ X509Certificate expiringCertificate =
+ SelfSignedCaCertificate.create(
+ "www.example.tld",
+ DateTime.parse("2020-09-02T00:00:00Z"),
+ DateTime.parse("2021-06-01T00:00:00Z"))
+ .cert();
+ int numOfRegistrars = 10;
+ for (int i = 1; i <= numOfRegistrars; i++) {
+ persistResource(
+ createRegistrar("oldcert" + i, "name" + i, null, expiringCertificate).build());
+ }
+ assertThat(action.sendNotificationEmails()).isEqualTo(numOfRegistrars);
+ }
+
+ @TestOfyAndSql
+ void sendNotificationEmails_allEmailsBeingAttemptedToSend_mixedOfCertificates() throws Exception {
+ X509Certificate expiringCertificate =
+ SelfSignedCaCertificate.create(
+ "www.example.tld",
+ DateTime.parse("2020-09-02T00:00:00Z"),
+ DateTime.parse("2021-06-01T00:00:00Z"))
+ .cert();
+ int numOfRegistrars = 10;
+ int numOfExpiringFailOverOnly = 2;
+ int numOfExpiringPrimaryOnly = 3;
+ for (int i = 1; i <= numOfExpiringFailOverOnly; i++) {
+ persistResource(
+ createRegistrar("cl" + i, "expiringFailOverOnly" + i, null, expiringCertificate).build());
+ }
+ for (int i = 1; i <= numOfExpiringPrimaryOnly; i++) {
+ persistResource(
+ createRegistrar("cli" + i, "expiringPrimaryOnly" + i, expiringCertificate, null).build());
+ }
+ for (int i = numOfExpiringFailOverOnly + numOfExpiringPrimaryOnly + 1;
+ i <= numOfRegistrars;
+ i++) {
+ persistResource(
+ createRegistrar("client" + i, "regularReg" + i, expiringCertificate, expiringCertificate)
+ .build());
+ }
+ assertThat(action.sendNotificationEmails())
+ .isEqualTo(numOfRegistrars + numOfExpiringFailOverOnly + numOfExpiringPrimaryOnly);
+ }
+
+ @TestOfyAndSql
+ void updateLastNotificationSentDate_updatedSuccessfully_primaryCertificate() throws Exception {
+ X509Certificate expiringCertificate =
+ SelfSignedCaCertificate.create(
+ "www.example.tld",
+ DateTime.parse("2020-09-02T00:00:00Z"),
+ DateTime.parse("2021-06-02T00:00:00Z"))
+ .cert();
+ Registrar registrar =
+ createRegistrar("testClientId", "registrar", expiringCertificate, null).build();
+ persistResource(registrar);
+ action.updateLastNotificationSentDate(registrar, clock.nowUtc(), CertificateType.PRIMARY);
+ assertThat(loadByEntity(registrar).getLastExpiringCertNotificationSentDate())
+ .isEqualTo(clock.nowUtc());
+ }
+
+ @TestOfyAndSql
+ void updateLastNotificationSentDate_updatedSuccessfully_failOverCertificate() throws Exception {
+ X509Certificate expiringCertificate =
+ SelfSignedCaCertificate.create(
+ "www.example.tld",
+ DateTime.parse("2020-09-02T00:00:00Z"),
+ DateTime.parse("2021-06-01T00:00:00Z"))
+ .cert();
+ Registrar registrar =
+ createRegistrar("testClientId", "registrar", null, expiringCertificate).build();
+ persistResource(registrar);
+ action.updateLastNotificationSentDate(registrar, clock.nowUtc(), CertificateType.FAILOVER);
+ assertThat(loadByEntity(registrar).getLastExpiringFailoverCertNotificationSentDate())
+ .isEqualTo(clock.nowUtc());
+ }
+
+ @TestOfyAndSql
+ void updateLastNotificationSentDate_noUpdates_noLastNotificationSentDate() throws Exception {
+ X509Certificate expiringCertificate =
+ SelfSignedCaCertificate.create(
+ "www.example.tld",
+ DateTime.parse("2020-09-02T00:00:00Z"),
+ DateTime.parse("2021-06-01T00:00:00Z"))
+ .cert();
+ Registrar registrar =
+ createRegistrar("testClientId", "registrar", null, expiringCertificate).build();
+ persistResource(registrar);
+ RuntimeException thrown =
+ assertThrows(
+ RuntimeException.class,
+ () -> action.updateLastNotificationSentDate(registrar, null, CertificateType.FAILOVER));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("Failed to update the last notification sent date to Registrar");
+ }
+
+ @TestOfyAndSql
+ void updateLastNotificationSentDate_noUpdates_invalidCertificateType() throws Exception {
+ X509Certificate expiringCertificate =
+ SelfSignedCaCertificate.create(
+ "www.example.tld",
+ DateTime.parse("2020-09-02T00:00:00Z"),
+ DateTime.parse("2021-06-01T00:00:00Z"))
+ .cert();
+ Registrar registrar =
+ createRegistrar("testClientId", "registrar", null, expiringCertificate).build();
+ persistResource(registrar);
+ IllegalArgumentException thrown =
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ action.updateLastNotificationSentDate(
+ registrar, clock.nowUtc(), CertificateType.valueOf("randomType")));
+ assertThat(thrown).hasMessageThat().contains("No enum constant");
+ }
+
+ @TestOfyAndSql
+ void getRegistrarsWithExpiringCertificates_returnsPartOfRegistrars() throws Exception {
+ X509Certificate expiringCertificate =
+ SelfSignedCaCertificate.create(
+ "www.example.tld",
+ DateTime.parse("2020-09-02T00:00:00Z"),
+ DateTime.parse("2021-06-01T00:00:00Z"))
+ .cert();
+ X509Certificate certificate =
+ SelfSignedCaCertificate.create(
+ "www.example.tld",
+ DateTime.parse("2020-09-02T00:00:00Z"),
+ DateTime.parse("2021-10-01T00:00:00Z"))
+ .cert();
+ int numOfRegistrars = 10;
+ int numOfRegistrarsWithExpiringCertificates = 2;
+ for (int i = 1; i <= numOfRegistrarsWithExpiringCertificates; i++) {
+ persistResource(
+ createRegistrar("oldcert" + i, "name" + i, expiringCertificate, null).build());
+ }
+ for (int i = numOfRegistrarsWithExpiringCertificates; i <= numOfRegistrars; i++) {
+ persistResource(createRegistrar("goodcert" + i, "name" + i, certificate, null).build());
+ }
+
+ ImmutableList results = action.getRegistrarsWithExpiringCertificates();
+ assertThat(results.size()).isEqualTo(numOfRegistrarsWithExpiringCertificates);
+ }
+
+ @TestOfyAndSql
+ void getRegistrarsWithExpiringCertificates_returnsPartOfRegistrars_failOverCertificateBranch()
+ throws Exception {
+ X509Certificate expiringCertificate =
+ SelfSignedCaCertificate.create(
+ "www.example.tld",
+ DateTime.parse("2020-09-02T00:00:00Z"),
+ DateTime.parse("2021-06-01T00:00:00Z"))
+ .cert();
+ X509Certificate certificate =
+ SelfSignedCaCertificate.create(
+ "www.example.tld",
+ DateTime.parse("2020-09-02T00:00:00Z"),
+ DateTime.parse("2021-10-01T00:00:00Z"))
+ .cert();
+ int numOfRegistrars = 10;
+ int numOfRegistrarsWithExpiringCertificates = 2;
+ for (int i = 1; i <= numOfRegistrarsWithExpiringCertificates; i++) {
+ persistResource(
+ createRegistrar("oldcert" + i, "name" + i, null, expiringCertificate).build());
+ }
+ for (int i = numOfRegistrarsWithExpiringCertificates; i <= numOfRegistrars; i++) {
+ persistResource(createRegistrar("goodcert" + i, "name" + i, null, certificate).build());
+ }
+
+ assertThat(action.getRegistrarsWithExpiringCertificates().size())
+ .isEqualTo(numOfRegistrarsWithExpiringCertificates);
+ }
+
+ @TestOfyAndSql
+ void getRegistrarsWithExpiringCertificates_returnsAllRegistrars() throws Exception {
+ X509Certificate expiringCertificate =
+ SelfSignedCaCertificate.create(
+ "www.example.tld",
+ DateTime.parse("2020-09-02T00:00:00Z"),
+ DateTime.parse("2021-06-01T00:00:00Z"))
+ .cert();
+
+ int numOfRegistrarsWithExpiringCertificates = 5;
+ for (int i = 1; i <= numOfRegistrarsWithExpiringCertificates; i++) {
+ persistResource(
+ createRegistrar("oldcert" + i, "name" + i, expiringCertificate, null).build());
+ }
+ assertThat(action.getRegistrarsWithExpiringCertificates().size())
+ .isEqualTo(numOfRegistrarsWithExpiringCertificates);
+ }
+
+ @TestOfyAndSql
+ void getRegistrarsWithExpiringCertificates_returnsNoRegistrars() throws Exception {
+ X509Certificate certificate =
+ SelfSignedCaCertificate.create(
+ "www.example.tld",
+ DateTime.parse("2020-09-02T00:00:00Z"),
+ DateTime.parse("2021-10-01T00:00:00Z"))
+ .cert();
+ int numOfRegistrars = 10;
+ for (int i = 1; i <= numOfRegistrars; i++) {
+ persistResource(createRegistrar("goodcert" + i, "name" + i, certificate, null).build());
+ }
+ assertThat(action.getRegistrarsWithExpiringCertificates()).isEmpty();
+ }
+
+ @TestOfyAndSql
+ void getRegistrarsWithExpiringCertificates_noRegistrarsInDatabase() {
+ assertThat(action.getRegistrarsWithExpiringCertificates()).isEmpty();
+ }
+
+ @TestOfyAndSql
+ void getEmailAddresses_success_returnsAnEmptyList() {
+ assertThat(action.getEmailAddresses(sampleRegistrar, Type.TECH)).isEmpty();
+ assertThat(action.getEmailAddresses(sampleRegistrar, Type.ADMIN)).isEmpty();
+ }
+
+ @TestOfyAndSql
+ void getEmailAddresses_success_returnsAListOfEmails() throws Exception {
+ Registrar registrar = persistResource(makeRegistrar1());
+ ImmutableList contacts =
+ ImmutableList.of(
+ new RegistrarContact.Builder()
+ .setParent(registrar)
+ .setName("John Doe")
+ .setEmailAddress("jd@example-registrar.tld")
+ .setPhoneNumber("+1.3105551213")
+ .setFaxNumber("+1.3105551213")
+ .setTypes(ImmutableSet.of(RegistrarContact.Type.TECH))
+ .setVisibleInWhoisAsAdmin(true)
+ .setVisibleInWhoisAsTech(false)
+ .build(),
+ new RegistrarContact.Builder()
+ .setParent(registrar)
+ .setName("John Smith")
+ .setEmailAddress("js@example-registrar.tld")
+ .setPhoneNumber("+1.1111111111")
+ .setFaxNumber("+1.1111111111")
+ .setTypes(ImmutableSet.of(RegistrarContact.Type.TECH))
+ .build(),
+ new RegistrarContact.Builder()
+ .setParent(registrar)
+ .setName("Will Doe")
+ .setEmailAddress("will@example-registrar.tld")
+ .setPhoneNumber("+1.3105551213")
+ .setFaxNumber("+1.3105551213")
+ .setTypes(ImmutableSet.of(RegistrarContact.Type.TECH))
+ .setVisibleInWhoisAsAdmin(true)
+ .setVisibleInWhoisAsTech(false)
+ .build(),
+ new RegistrarContact.Builder()
+ .setParent(registrar)
+ .setName("Mike Doe")
+ .setEmailAddress("mike@example-registrar.tld")
+ .setPhoneNumber("+1.1111111111")
+ .setFaxNumber("+1.1111111111")
+ .setTypes(ImmutableSet.of(RegistrarContact.Type.ADMIN))
+ .build(),
+ new RegistrarContact.Builder()
+ .setParent(registrar)
+ .setName("John T")
+ .setEmailAddress("john@example-registrar.tld")
+ .setPhoneNumber("+1.3105551215")
+ .setFaxNumber("+1.3105551216")
+ .setTypes(ImmutableSet.of(RegistrarContact.Type.ADMIN))
+ .setVisibleInWhoisAsTech(true)
+ .build());
+ persistSimpleResources(contacts);
+ assertThat(action.getEmailAddresses(registrar, Type.TECH))
+ .containsExactly(
+ new InternetAddress("will@example-registrar.tld"),
+ new InternetAddress("jd@example-registrar.tld"),
+ new InternetAddress("js@example-registrar.tld"));
+ assertThat(action.getEmailAddresses(registrar, Type.ADMIN))
+ .containsExactly(
+ new InternetAddress("janedoe@theregistrar.com"), // comes with makeRegistrar1()
+ new InternetAddress("mike@example-registrar.tld"),
+ new InternetAddress("john@example-registrar.tld"));
+ }
+
+ @TestOfyAndSql
+ void getEmailAddresses_failure_returnsPartialListOfEmails_skipInvalidEmails() {
+ // when building a new RegistrarContact object, there's already an email validation process.
+ // if the registrarContact is created successful, the email address of the contact object
+ // should already be validated. Ideally, there should not be an AddressException when creating
+ // a new InternetAddress using the email address string of the contact object.
+ }
+
+ @TestOfyAndSql
+ void getEmailBody_returnsEmailBodyText() {
+ String registrarName = "good registrar";
+ String certExpirationDateStr = "2021-06-15";
+ CertificateType certificateType = CertificateType.PRIMARY;
+ String emailBody =
+ action.getEmailBody(
+ registrarName, certificateType, DateTime.parse(certExpirationDateStr).toDate());
+ assertThat(emailBody).contains(registrarName);
+ assertThat(emailBody).contains(certificateType.getDisplayName());
+ assertThat(emailBody).contains(certExpirationDateStr);
+ }
+
+ @TestOfyAndSql
+ void getEmailBody_throwsIllegalArgumentException_noExpirationDate() {
+ IllegalArgumentException thrown =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> action.getEmailBody("good registrar", CertificateType.FAILOVER, null));
+ assertThat(thrown).hasMessageThat().contains("Expiration date cannot be null");
+ }
+
+ @TestOfyAndSql
+ void getEmailBody_throwsIllegalArgumentException_noCertificateType() {
+ IllegalArgumentException thrown =
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ action.getEmailBody("good registrar", null, DateTime.parse("2021-06-15").toDate()));
+ assertThat(thrown).hasMessageThat().contains("Certificate type cannot be null");
+ }
+}
diff --git a/core/src/test/resources/google/registry/module/backend/backend_routing.txt b/core/src/test/resources/google/registry/module/backend/backend_routing.txt
index e67f107b0..8a351599e 100644
--- a/core/src/test/resources/google/registry/module/backend/backend_routing.txt
+++ b/core/src/test/resources/google/registry/module/backend/backend_routing.txt
@@ -1,49 +1,50 @@
-PATH CLASS METHODS OK AUTH_METHODS MIN USER_POLICY
-/_dr/cron/commitLogCheckpoint CommitLogCheckpointAction GET y INTERNAL,API APP ADMIN
-/_dr/cron/commitLogFanout CommitLogFanoutAction GET y INTERNAL,API APP ADMIN
-/_dr/cron/fanout TldFanoutAction GET y INTERNAL,API APP ADMIN
-/_dr/cron/readDnsQueue ReadDnsQueueAction GET y INTERNAL,API APP ADMIN
-/_dr/dnsRefresh RefreshDnsAction GET y INTERNAL,API APP ADMIN
-/_dr/task/backupDatastore BackupDatastoreAction POST y INTERNAL,API APP ADMIN
-/_dr/task/brdaCopy BrdaCopyAction POST y INTERNAL,API APP ADMIN
-/_dr/task/checkDatastoreBackup CheckBackupAction POST,GET y INTERNAL,API APP ADMIN
-/_dr/task/copyDetailReports CopyDetailReportsAction POST n INTERNAL,API APP ADMIN
-/_dr/task/createSyntheticHistoryEntries CreateSyntheticHistoryEntriesAction GET n INTERNAL,API APP ADMIN
-/_dr/task/deleteContactsAndHosts DeleteContactsAndHostsAction GET n INTERNAL,API APP ADMIN
-/_dr/task/deleteExpiredDomains DeleteExpiredDomainsAction GET n INTERNAL,API APP ADMIN
-/_dr/task/deleteLoadTestData DeleteLoadTestDataAction POST n INTERNAL,API APP ADMIN
-/_dr/task/deleteOldCommitLogs DeleteOldCommitLogsAction GET n INTERNAL,API APP ADMIN
-/_dr/task/deleteProberData DeleteProberDataAction POST n INTERNAL,API APP ADMIN
-/_dr/task/expandRecurringBillingEvents ExpandRecurringBillingEventsAction GET n INTERNAL,API APP ADMIN
-/_dr/task/exportCommitLogDiff ExportCommitLogDiffAction POST y INTERNAL,API APP ADMIN
-/_dr/task/exportDomainLists ExportDomainListsAction POST n INTERNAL,API APP ADMIN
-/_dr/task/exportPremiumTerms ExportPremiumTermsAction POST n INTERNAL,API APP ADMIN
-/_dr/task/exportReservedTerms ExportReservedTermsAction POST n INTERNAL,API APP ADMIN
-/_dr/task/generateInvoices GenerateInvoicesAction POST n INTERNAL,API APP ADMIN
-/_dr/task/generateSpec11 GenerateSpec11ReportAction POST n INTERNAL,API APP ADMIN
-/_dr/task/icannReportingStaging IcannReportingStagingAction POST n INTERNAL,API APP ADMIN
-/_dr/task/icannReportingUpload IcannReportingUploadAction POST n INTERNAL,API APP ADMIN
-/_dr/task/nordnUpload NordnUploadAction POST y INTERNAL,API APP ADMIN
-/_dr/task/nordnVerify NordnVerifyAction POST y INTERNAL,API APP ADMIN
-/_dr/task/pollBigqueryJob BigqueryPollJobAction GET,POST y INTERNAL APP IGNORED
-/_dr/task/publishDnsUpdates PublishDnsUpdatesAction POST y INTERNAL,API APP ADMIN
-/_dr/task/publishInvoices PublishInvoicesAction POST n INTERNAL,API APP ADMIN
-/_dr/task/publishSpec11 PublishSpec11ReportAction POST n INTERNAL,API APP ADMIN
-/_dr/task/rdeReport RdeReportAction POST n INTERNAL,API APP ADMIN
-/_dr/task/rdeStaging RdeStagingAction GET,POST n INTERNAL,API APP ADMIN
-/_dr/task/rdeUpload RdeUploadAction POST n INTERNAL,API APP ADMIN
-/_dr/task/refreshDnsOnHostRename RefreshDnsOnHostRenameAction GET n INTERNAL,API APP ADMIN
-/_dr/task/relockDomain RelockDomainAction POST y INTERNAL,API APP ADMIN
-/_dr/task/replayCommitLogsToSql ReplayCommitLogsToSqlAction POST y INTERNAL,API APP ADMIN
-/_dr/task/resaveAllEppResources ResaveAllEppResourcesAction GET n INTERNAL,API APP ADMIN
-/_dr/task/resaveEntity ResaveEntityAction POST n INTERNAL,API APP ADMIN
-/_dr/task/syncGroupMembers SyncGroupMembersAction POST n INTERNAL,API APP ADMIN
-/_dr/task/syncRegistrarsSheet SyncRegistrarsSheetAction POST n INTERNAL,API APP ADMIN
-/_dr/task/tmchCrl TmchCrlAction POST y INTERNAL,API APP ADMIN
-/_dr/task/tmchDnl TmchDnlAction POST y INTERNAL,API APP ADMIN
-/_dr/task/tmchSmdrl TmchSmdrlAction POST y INTERNAL,API APP ADMIN
-/_dr/task/updateRegistrarRdapBaseUrls UpdateRegistrarRdapBaseUrlsAction GET y INTERNAL,API APP ADMIN
-/_dr/task/updateSnapshotView UpdateSnapshotViewAction POST n INTERNAL,API APP ADMIN
-/_dr/task/uploadDatastoreBackup UploadDatastoreBackupAction POST n INTERNAL,API APP ADMIN
-/_dr/task/wipeOutCloudSql WipeOutCloudSqlAction GET n INTERNAL,API APP ADMIN
-/_dr/task/wipeOutDatastore WipeoutDatastoreAction GET n INTERNAL,API APP ADMIN
+PATH CLASS METHODS OK AUTH_METHODS MIN USER_POLICY
+/_dr/cron/commitLogCheckpoint CommitLogCheckpointAction GET y INTERNAL,API APP ADMIN
+/_dr/cron/commitLogFanout CommitLogFanoutAction GET y INTERNAL,API APP ADMIN
+/_dr/cron/fanout TldFanoutAction GET y INTERNAL,API APP ADMIN
+/_dr/cron/readDnsQueue ReadDnsQueueAction GET y INTERNAL,API APP ADMIN
+/_dr/dnsRefresh RefreshDnsAction GET y INTERNAL,API APP ADMIN
+/_dr/task/backupDatastore BackupDatastoreAction POST y INTERNAL,API APP ADMIN
+/_dr/task/brdaCopy BrdaCopyAction POST y INTERNAL,API APP ADMIN
+/_dr/task/checkDatastoreBackup CheckBackupAction POST,GET y INTERNAL,API APP ADMIN
+/_dr/task/copyDetailReports CopyDetailReportsAction POST n INTERNAL,API APP ADMIN
+/_dr/task/createSyntheticHistoryEntries CreateSyntheticHistoryEntriesAction GET n INTERNAL,API APP ADMIN
+/_dr/task/deleteContactsAndHosts DeleteContactsAndHostsAction GET n INTERNAL,API APP ADMIN
+/_dr/task/deleteExpiredDomains DeleteExpiredDomainsAction GET n INTERNAL,API APP ADMIN
+/_dr/task/deleteLoadTestData DeleteLoadTestDataAction POST n INTERNAL,API APP ADMIN
+/_dr/task/deleteOldCommitLogs DeleteOldCommitLogsAction GET n INTERNAL,API APP ADMIN
+/_dr/task/deleteProberData DeleteProberDataAction POST n INTERNAL,API APP ADMIN
+/_dr/task/expandRecurringBillingEvents ExpandRecurringBillingEventsAction GET n INTERNAL,API APP ADMIN
+/_dr/task/exportCommitLogDiff ExportCommitLogDiffAction POST y INTERNAL,API APP ADMIN
+/_dr/task/exportDomainLists ExportDomainListsAction POST n INTERNAL,API APP ADMIN
+/_dr/task/exportPremiumTerms ExportPremiumTermsAction POST n INTERNAL,API APP ADMIN
+/_dr/task/exportReservedTerms ExportReservedTermsAction POST n INTERNAL,API APP ADMIN
+/_dr/task/generateInvoices GenerateInvoicesAction POST n INTERNAL,API APP ADMIN
+/_dr/task/generateSpec11 GenerateSpec11ReportAction POST n INTERNAL,API APP ADMIN
+/_dr/task/icannReportingStaging IcannReportingStagingAction POST n INTERNAL,API APP ADMIN
+/_dr/task/icannReportingUpload IcannReportingUploadAction POST n INTERNAL,API APP ADMIN
+/_dr/task/nordnUpload NordnUploadAction POST y INTERNAL,API APP ADMIN
+/_dr/task/nordnVerify NordnVerifyAction POST y INTERNAL,API APP ADMIN
+/_dr/task/pollBigqueryJob BigqueryPollJobAction GET,POST y INTERNAL APP IGNORED
+/_dr/task/publishDnsUpdates PublishDnsUpdatesAction POST y INTERNAL,API APP ADMIN
+/_dr/task/publishInvoices PublishInvoicesAction POST n INTERNAL,API APP ADMIN
+/_dr/task/publishSpec11 PublishSpec11ReportAction POST n INTERNAL,API APP ADMIN
+/_dr/task/rdeReport RdeReportAction POST n INTERNAL,API APP ADMIN
+/_dr/task/rdeStaging RdeStagingAction GET,POST n INTERNAL,API APP ADMIN
+/_dr/task/rdeUpload RdeUploadAction POST n INTERNAL,API APP ADMIN
+/_dr/task/refreshDnsOnHostRename RefreshDnsOnHostRenameAction GET n INTERNAL,API APP ADMIN
+/_dr/task/relockDomain RelockDomainAction POST y INTERNAL,API APP ADMIN
+/_dr/task/replayCommitLogsToSql ReplayCommitLogsToSqlAction POST y INTERNAL,API APP ADMIN
+/_dr/task/resaveAllEppResources ResaveAllEppResourcesAction GET n INTERNAL,API APP ADMIN
+/_dr/task/resaveEntity ResaveEntityAction POST n INTERNAL,API APP ADMIN
+/_dr/task/sendExpiringCertificateNotificationEmail SendExpiringCertificateNotificationEmailAction GET n INTERNAL,API APP ADMIN
+/_dr/task/syncGroupMembers SyncGroupMembersAction POST n INTERNAL,API APP ADMIN
+/_dr/task/syncRegistrarsSheet SyncRegistrarsSheetAction POST n INTERNAL,API APP ADMIN
+/_dr/task/tmchCrl TmchCrlAction POST y INTERNAL,API APP ADMIN
+/_dr/task/tmchDnl TmchDnlAction POST y INTERNAL,API APP ADMIN
+/_dr/task/tmchSmdrl TmchSmdrlAction POST y INTERNAL,API APP ADMIN
+/_dr/task/updateRegistrarRdapBaseUrls UpdateRegistrarRdapBaseUrlsAction GET y INTERNAL,API APP ADMIN
+/_dr/task/updateSnapshotView UpdateSnapshotViewAction POST n INTERNAL,API APP ADMIN
+/_dr/task/uploadDatastoreBackup UploadDatastoreBackupAction POST n INTERNAL,API APP ADMIN
+/_dr/task/wipeOutCloudSql WipeOutCloudSqlAction GET n INTERNAL,API APP ADMIN
+/_dr/task/wipeOutDatastore WipeoutDatastoreAction GET n INTERNAL,API APP ADMIN