From b070c46231ca023b1d693b25c85287fc9e2d439e Mon Sep 17 00:00:00 2001 From: gbrodman Date: Fri, 1 Aug 2025 15:51:44 -0400 Subject: [PATCH] Allow superuser status to override EPP resource delete prohibited status (#2789) --- .../java/google/registry/flows/FlowUtils.java | 5 ++ .../flows/contact/ContactDeleteFlow.java | 10 ++-- .../flows/domain/DomainDeleteFlow.java | 9 ++-- .../registry/flows/host/HostDeleteFlow.java | 10 ++-- .../flows/contact/ContactDeleteFlowTest.java | 37 +++++++++++++++ .../flows/domain/DomainDeleteFlowTest.java | 46 +++++++++++++++++++ .../flows/host/HostDeleteFlowTest.java | 37 +++++++++++++++ 7 files changed, 134 insertions(+), 20 deletions(-) diff --git a/core/src/main/java/google/registry/flows/FlowUtils.java b/core/src/main/java/google/registry/flows/FlowUtils.java index 40a723df2..c3c2b2425 100644 --- a/core/src/main/java/google/registry/flows/FlowUtils.java +++ b/core/src/main/java/google/registry/flows/FlowUtils.java @@ -21,6 +21,7 @@ import static google.registry.xml.ValidationMode.STRICT; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableSet; import com.google.common.flogger.FluentLogger; import google.registry.flows.EppException.CommandUseErrorException; import google.registry.flows.EppException.ParameterValueRangeErrorException; @@ -30,6 +31,7 @@ import google.registry.flows.custom.EntityChanges; import google.registry.model.EppResource; import google.registry.model.adapters.CurrencyUnitAdapter.UnknownCurrencyException; import google.registry.model.eppcommon.EppXmlTransformer; +import google.registry.model.eppcommon.StatusValue; import google.registry.model.eppinput.EppInput.WrongProtocolVersionException; import google.registry.model.eppoutput.EppOutput; import google.registry.model.host.InetAddressAdapter.IpVersionMismatchException; @@ -40,6 +42,9 @@ import java.util.List; /** Static utility functions for flows. */ public final class FlowUtils { + public static final ImmutableSet DELETE_PROHIBITED_STATUSES = + ImmutableSet.of(StatusValue.CLIENT_DELETE_PROHIBITED, StatusValue.SERVER_DELETE_PROHIBITED); + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private FlowUtils() {} diff --git a/core/src/main/java/google/registry/flows/contact/ContactDeleteFlow.java b/core/src/main/java/google/registry/flows/contact/ContactDeleteFlow.java index eb7daf165..f25a03e5f 100644 --- a/core/src/main/java/google/registry/flows/contact/ContactDeleteFlow.java +++ b/core/src/main/java/google/registry/flows/contact/ContactDeleteFlow.java @@ -14,6 +14,7 @@ package google.registry.flows.contact; +import static google.registry.flows.FlowUtils.DELETE_PROHIBITED_STATUSES; import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn; import static google.registry.flows.ResourceFlowUtils.checkLinkedDomains; import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence; @@ -65,12 +66,6 @@ import org.joda.time.DateTime; @ReportingSpec(ActivityReportField.CONTACT_DELETE) public final class ContactDeleteFlow implements MutatingFlow { - private static final ImmutableSet DISALLOWED_STATUSES = - ImmutableSet.of( - StatusValue.CLIENT_DELETE_PROHIBITED, - StatusValue.PENDING_DELETE, - StatusValue.SERVER_DELETE_PROHIBITED); - @Inject ExtensionManager extensionManager; @Inject @RegistrarId String registrarId; @Inject @TargetId String targetId; @@ -91,9 +86,10 @@ public final class ContactDeleteFlow implements MutatingFlow { DateTime now = tm().getTransactionTime(); checkLinkedDomains(targetId, now, Contact.class); Contact existingContact = loadAndVerifyExistence(Contact.class, targetId, now); - verifyNoDisallowedStatuses(existingContact, DISALLOWED_STATUSES); verifyOptionalAuthInfo(authInfo, existingContact); + verifyNoDisallowedStatuses(existingContact, ImmutableSet.of(StatusValue.PENDING_DELETE)); if (!isSuperuser) { + verifyNoDisallowedStatuses(existingContact, DELETE_PROHIBITED_STATUSES); verifyResourceOwnership(registrarId, existingContact); } // Handle pending transfers on contact deletion. diff --git a/core/src/main/java/google/registry/flows/domain/DomainDeleteFlow.java b/core/src/main/java/google/registry/flows/domain/DomainDeleteFlow.java index 357545234..e398fb2fb 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainDeleteFlow.java +++ b/core/src/main/java/google/registry/flows/domain/DomainDeleteFlow.java @@ -17,6 +17,7 @@ package google.registry.flows.domain; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Strings.isNullOrEmpty; import static google.registry.dns.DnsUtils.requestDomainDnsRefresh; +import static google.registry.flows.FlowUtils.DELETE_PROHIBITED_STATUSES; import static google.registry.flows.FlowUtils.createHistoryEntryId; import static google.registry.flows.FlowUtils.persistEntityChanges; import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn; @@ -122,11 +123,6 @@ public final class DomainDeleteFlow implements MutatingFlow, SqlStatementLogging private static final FluentLogger logger = FluentLogger.forEnclosingClass(); - private static final ImmutableSet DISALLOWED_STATUSES = ImmutableSet.of( - StatusValue.CLIENT_DELETE_PROHIBITED, - StatusValue.PENDING_DELETE, - StatusValue.SERVER_DELETE_PROHIBITED); - @Inject ExtensionManager extensionManager; @Inject EppInput eppInput; @Inject SessionMetadata sessionMetadata; @@ -304,9 +300,10 @@ public final class DomainDeleteFlow implements MutatingFlow, SqlStatementLogging private void verifyDeleteAllowed(Domain existingDomain, Tld tld, DateTime now) throws EppException { - verifyNoDisallowedStatuses(existingDomain, DISALLOWED_STATUSES); verifyOptionalAuthInfo(authInfo, existingDomain); + verifyNoDisallowedStatuses(existingDomain, ImmutableSet.of(StatusValue.PENDING_DELETE)); if (!isSuperuser) { + verifyNoDisallowedStatuses(existingDomain, DELETE_PROHIBITED_STATUSES); verifyResourceOwnership(registrarId, existingDomain); verifyNotInPredelegation(tld, now); checkAllowedAccessToTld(registrarId, tld.getTld().toString()); diff --git a/core/src/main/java/google/registry/flows/host/HostDeleteFlow.java b/core/src/main/java/google/registry/flows/host/HostDeleteFlow.java index 93d7d973f..c7cc04c06 100644 --- a/core/src/main/java/google/registry/flows/host/HostDeleteFlow.java +++ b/core/src/main/java/google/registry/flows/host/HostDeleteFlow.java @@ -15,6 +15,7 @@ package google.registry.flows.host; import static google.registry.dns.DnsUtils.requestHostDnsRefresh; +import static google.registry.flows.FlowUtils.DELETE_PROHIBITED_STATUSES; import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn; import static google.registry.flows.ResourceFlowUtils.checkLinkedDomains; import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence; @@ -65,12 +66,6 @@ import org.joda.time.DateTime; @ReportingSpec(ActivityReportField.HOST_DELETE) public final class HostDeleteFlow implements MutatingFlow { - private static final ImmutableSet DISALLOWED_STATUSES = - ImmutableSet.of( - StatusValue.CLIENT_DELETE_PROHIBITED, - StatusValue.PENDING_DELETE, - StatusValue.SERVER_DELETE_PROHIBITED); - @Inject ExtensionManager extensionManager; @Inject @RegistrarId String registrarId; @Inject @TargetId String targetId; @@ -91,8 +86,9 @@ public final class HostDeleteFlow implements MutatingFlow { validateHostName(targetId); checkLinkedDomains(targetId, now, Host.class); Host existingHost = loadAndVerifyExistence(Host.class, targetId, now); - verifyNoDisallowedStatuses(existingHost, DISALLOWED_STATUSES); + verifyNoDisallowedStatuses(existingHost, ImmutableSet.of(StatusValue.PENDING_DELETE)); if (!isSuperuser) { + verifyNoDisallowedStatuses(existingHost, DELETE_PROHIBITED_STATUSES); // Hosts transfer with their superordinate domains, so for hosts with a superordinate domain, // the client id, needs to be read off of it. EppResource owningResource = diff --git a/core/src/test/java/google/registry/flows/contact/ContactDeleteFlowTest.java b/core/src/test/java/google/registry/flows/contact/ContactDeleteFlowTest.java index 33bebdd44..71c419704 100644 --- a/core/src/test/java/google/registry/flows/contact/ContactDeleteFlowTest.java +++ b/core/src/test/java/google/registry/flows/contact/ContactDeleteFlowTest.java @@ -182,6 +182,43 @@ class ContactDeleteFlowTest extends ResourceFlowTestCase runFlow(CommitMode.LIVE, UserPrivileges.SUPERUSER))) + .marshalsToXml(); + } + @Test void testFailure_unauthorizedClient() throws Exception { sessionMetadata.setRegistrarId("NewRegistrar"); diff --git a/core/src/test/java/google/registry/flows/domain/DomainDeleteFlowTest.java b/core/src/test/java/google/registry/flows/domain/DomainDeleteFlowTest.java index a336ca532..d6d93ce11 100644 --- a/core/src/test/java/google/registry/flows/domain/DomainDeleteFlowTest.java +++ b/core/src/test/java/google/registry/flows/domain/DomainDeleteFlowTest.java @@ -917,6 +917,52 @@ class DomainDeleteFlowTest extends ResourceFlowTestCase runFlow(CommitMode.LIVE, UserPrivileges.SUPERUSER)); + assertThat(thrown).hasMessageThat().contains("pendingDelete"); + } + @Test void testSuccess_metadata() throws Exception { eppRequestSource = EppRequestSource.TOOL; diff --git a/core/src/test/java/google/registry/flows/host/HostDeleteFlowTest.java b/core/src/test/java/google/registry/flows/host/HostDeleteFlowTest.java index cc4f8f221..187a04965 100644 --- a/core/src/test/java/google/registry/flows/host/HostDeleteFlowTest.java +++ b/core/src/test/java/google/registry/flows/host/HostDeleteFlowTest.java @@ -153,6 +153,43 @@ class HostDeleteFlowTest extends ResourceFlowTestCase { assertSqlDeleteSuccess(); } + @Test + void testSuccess_clientDeleteProhibited_superuser() throws Exception { + persistResource( + persistActiveHost("ns1.example.tld") + .asBuilder() + .addStatusValue(StatusValue.CLIENT_DELETE_PROHIBITED) + .build()); + runFlowAssertResponse( + CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("host_delete_response.xml")); + } + + @Test + void testSuccess_serverDeleteProhibited_superuser() throws Exception { + persistResource( + persistActiveHost("ns1.example.tld") + .asBuilder() + .addStatusValue(StatusValue.SERVER_DELETE_PROHIBITED) + .build()); + runFlowAssertResponse( + CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("host_delete_response.xml")); + } + + @Test + void testFailure_pendingDelete_superuser() throws Exception { + persistResource( + persistActiveHost("ns1.example.tld") + .asBuilder() + .addStatusValue(StatusValue.PENDING_DELETE) + .build()); + assertAboutEppExceptions() + .that( + assertThrows( + ResourceStatusProhibitsOperationException.class, + () -> runFlow(CommitMode.LIVE, UserPrivileges.SUPERUSER))) + .marshalsToXml(); + } + @Test void testSuccess_authorizedClientReadFromSuperordinate() throws Exception { sessionMetadata.setRegistrarId("TheRegistrar");