From 2cea084e4d130038fd0f79ec329075f6d4b328e7 Mon Sep 17 00:00:00 2001 From: Rachel Guan Date: Thu, 19 Aug 2021 12:49:45 -0400 Subject: [PATCH] Add sending notification email mechanism for expiring certificates (#1179) * Resolve rebase conflict * Fix and imporove based on feedback. --- ...ingCertificateNotificationEmailAction.java | 326 ++++++++++ .../env/common/backend/WEB-INF/web.xml | 6 + .../env/sandbox/default/WEB-INF/cron.xml | 9 + .../backend/BackendRequestComponent.java | 3 + ...ertificateNotificationEmailActionTest.java | 607 ++++++++++++++++++ .../module/backend/backend_routing.txt | 99 +-- 6 files changed, 1001 insertions(+), 49 deletions(-) create mode 100644 core/src/main/java/google/registry/batch/SendExpiringCertificateNotificationEmailAction.java create mode 100644 core/src/test/java/google/registry/batch/SendExpiringCertificateNotificationEmailActionTest.java 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