Send registrars poll messages when we add/remove server-side statuses (#1417)

* Send registrars poll messages when we add/remove server-side status values
This commit is contained in:
Ben McIlwain 2021-11-16 11:35:05 -05:00 committed by GitHub
parent a5c646fab4
commit ff7ac45bf4
8 changed files with 279 additions and 24 deletions

View file

@ -16,6 +16,7 @@ package google.registry.flows.domain;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
import static com.google.common.collect.Sets.symmetricDifference;
import static com.google.common.collect.Sets.union;
import static google.registry.flows.FlowUtils.persistEntityChanges;
@ -43,6 +44,9 @@ import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_UPDATE;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.google.common.net.InternetDomainName;
import google.registry.dns.DnsQueue;
import google.registry.flows.EppException;
@ -76,6 +80,7 @@ import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppinput.EppInput;
import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.model.tld.Registry;
import java.util.Optional;
@ -175,6 +180,9 @@ public final class DomainUpdateFlow implements TransactionalFlow {
Optional<BillingEvent.OneTime> statusUpdateBillingEvent =
createBillingEventForStatusUpdates(existingDomain, newDomain, domainHistory, now);
statusUpdateBillingEvent.ifPresent(entitiesToSave::add);
Optional<PollMessage.OneTime> serverStatusUpdatePollMessage =
createPollMessageForServerStatusUpdates(existingDomain, newDomain, domainHistory, now);
serverStatusUpdatePollMessage.ifPresent(entitiesToSave::add);
EntityChanges entityChanges =
flowCustomLogic.beforeSave(
BeforeSaveParameters.newBuilder()
@ -306,4 +314,50 @@ public final class DomainUpdateFlow implements TransactionalFlow {
}
return Optional.empty();
}
/** Enqueues a poll message iff a superuser is adding/removing server statuses. */
private Optional<PollMessage.OneTime> createPollMessageForServerStatusUpdates(
DomainBase existingDomain, DomainBase newDomain, DomainHistory historyEntry, DateTime now) {
if (registrarId.equals(existingDomain.getPersistedCurrentSponsorRegistrarId())) {
// Don't send a poll message when a superuser registrar is updating its own domain.
return Optional.empty();
}
ImmutableSortedSet<String> addedServerStatuses =
Sets.difference(newDomain.getStatusValues(), existingDomain.getStatusValues()).stream()
.filter(StatusValue::isServerSettable)
.map(StatusValue::getXmlName)
.collect(toImmutableSortedSet(Ordering.natural()));
ImmutableSortedSet<String> removedServerStatuses =
Sets.difference(existingDomain.getStatusValues(), newDomain.getStatusValues()).stream()
.filter(StatusValue::isServerSettable)
.map(StatusValue::getXmlName)
.collect(toImmutableSortedSet(Ordering.natural()));
String msg = "";
if (addedServerStatuses.size() > 0 && removedServerStatuses.size() > 0) {
msg =
String.format(
"The registry administrator has added the status(es) %s and removed the status(es)"
+ " %s.",
addedServerStatuses, removedServerStatuses);
} else if (addedServerStatuses.size() > 0) {
msg =
String.format(
"The registry administrator has added the status(es) %s.", addedServerStatuses);
} else if (removedServerStatuses.size() > 0) {
msg =
String.format(
"The registry administrator has removed the status(es) %s.", removedServerStatuses);
} else {
return Optional.empty();
}
return Optional.ofNullable(
new PollMessage.OneTime.Builder()
.setParent(historyEntry)
.setEventTime(now)
.setRegistrarId(existingDomain.getCurrentSponsorRegistrarId())
.setMsg(msg)
.build());
}
}

View file

@ -112,7 +112,6 @@ public enum StatusValue implements EppEnum {
*/
PENDING_UPDATE(AllowedOn.NONE),
/** A non-client-settable status that prevents deletes of EPP resources. */
SERVER_DELETE_PROHIBITED(AllowedOn.ALL),
@ -162,6 +161,10 @@ public enum StatusValue implements EppEnum {
return xmlName.startsWith("client");
}
public boolean isServerSettable() {
return xmlName.startsWith("server");
}
public boolean isChargedStatus() {
return xmlName.startsWith("server") && xmlName.endsWith("Prohibited");
}

View file

@ -1449,4 +1449,39 @@ class EppLifecycleDomainTest extends EppTestCase {
.atTime("2001-01-08T00:00:00Z")
.hasResponse("domain_transfer_query_response_completed_fakesite.xml");
}
@TestOfyAndSql
void testDomainUpdateBySuperuser_sendsPollMessage() throws Exception {
setIsSuperuser(false);
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
createContacts(DateTime.parse("2000-06-01T00:00:00Z"));
// Create domain example.tld
assertThatCommand(
"domain_create_no_hosts_or_dsdata.xml", ImmutableMap.of("DOMAIN", "example.tld"))
.atTime("2000-06-01T00:02:00Z")
.hasResponse(
"domain_create_response.xml",
ImmutableMap.of(
"DOMAIN", "example.tld",
"CRDATE", "2000-06-01T00:02:00Z",
"EXDATE", "2002-06-01T00:02:00Z"));
assertThatLogoutSucceeds();
setIsSuperuser(true);
assertThatLoginSucceeds("TheRegistrar", "password2");
// Run domain update that adds server status as superuser
assertThatCommand("domain_update_server_hold.xml")
.atTime("2000-06-02T13:00:00Z")
.hasResponse("generic_success_response.xml");
assertThatLogoutSucceeds();
setIsSuperuser(false);
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
// Verify that the owning registrar now has the correct poll message
assertThatCommand("poll.xml")
.atTime("2000-06-02T13:00:01Z")
.hasResponse("poll_response_server_hold.xml");
assertThatLogoutSucceeds();
}
}

View file

@ -19,12 +19,22 @@ import static com.google.common.collect.Sets.union;
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.eppcommon.StatusValue.CLIENT_DELETE_PROHIBITED;
import static google.registry.model.eppcommon.StatusValue.CLIENT_HOLD;
import static google.registry.model.eppcommon.StatusValue.CLIENT_RENEW_PROHIBITED;
import static google.registry.model.eppcommon.StatusValue.SERVER_DELETE_PROHIBITED;
import static google.registry.model.eppcommon.StatusValue.SERVER_HOLD;
import static google.registry.model.eppcommon.StatusValue.SERVER_TRANSFER_PROHIBITED;
import static google.registry.model.eppcommon.StatusValue.SERVER_UPDATE_PROHIBITED;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_CREATE;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_UPDATE;
import static google.registry.model.tld.Registry.TldState.QUIET_PERIOD;
import static google.registry.testing.DatabaseHelper.assertBillingEvents;
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
import static google.registry.testing.DatabaseHelper.assertPollMessagesForResource;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.getOnlyHistoryEntryOfType;
import static google.registry.testing.DatabaseHelper.getPollMessages;
import static google.registry.testing.DatabaseHelper.loadByKey;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.newDomainBase;
@ -88,7 +98,7 @@ import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.secdns.DelegationSignerData;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.host.HostResource;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.poll.PollMessage;
import google.registry.model.tld.Registry;
import google.registry.persistence.VKey;
import google.registry.testing.DualDatabaseTest;
@ -155,7 +165,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
.build());
persistResource(
new DomainHistory.Builder()
.setType(HistoryEntry.Type.DOMAIN_CREATE)
.setType(DOMAIN_CREATE)
.setModificationTime(clock.nowUtc())
.setRegistrarId(domain.getCreationRegistrarId())
.setDomain(domain)
@ -179,7 +189,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
.build());
persistResource(
new DomainHistory.Builder()
.setType(HistoryEntry.Type.DOMAIN_CREATE)
.setType(DOMAIN_CREATE)
.setModificationTime(clock.nowUtc())
.setRegistrarId(domain.getCreationRegistrarId())
.setDomain(domain)
@ -202,8 +212,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
.and()
.hasAuthInfoPwd("2BARfoo")
.and()
.hasOneHistoryEntryEachOfTypes(
HistoryEntry.Type.DOMAIN_CREATE, HistoryEntry.Type.DOMAIN_UPDATE)
.hasOneHistoryEntryEachOfTypes(DOMAIN_CREATE, DOMAIN_UPDATE)
.and()
.hasLastEppUpdateTime(clock.nowUtc())
.and()
@ -329,10 +338,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
assertTransactionalFlow(true);
runFlowAssertResponse(loadFile("generic_success_response.xml"));
DomainBase domain = reloadResourceByForeignKey();
assertAboutDomains()
.that(domain)
.hasOneHistoryEntryEachOfTypes(
HistoryEntry.Type.DOMAIN_CREATE, HistoryEntry.Type.DOMAIN_UPDATE);
assertAboutDomains().that(domain).hasOneHistoryEntryEachOfTypes(DOMAIN_CREATE, DOMAIN_UPDATE);
assertThat(domain.getNameservers()).hasSize(13);
// getContacts does not return contacts of type REGISTRANT, so check these separately.
assertThat(domain.getContacts()).hasSize(3);
@ -349,12 +355,9 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
persistDomain();
runFlow();
DomainBase domain = reloadResourceByForeignKey();
assertAboutDomains()
.that(domain)
.hasOneHistoryEntryEachOfTypes(
HistoryEntry.Type.DOMAIN_CREATE, HistoryEntry.Type.DOMAIN_UPDATE);
assertAboutDomains().that(domain).hasOneHistoryEntryEachOfTypes(DOMAIN_CREATE, DOMAIN_UPDATE);
assertAboutHistoryEntries()
.that(getOnlyHistoryEntryOfType(domain, HistoryEntry.Type.DOMAIN_UPDATE))
.that(getOnlyHistoryEntryOfType(domain, DOMAIN_UPDATE))
.hasMetadataReason("domain-update-test")
.and()
.hasMetadataRequestedByRegistrar(true);
@ -482,10 +485,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
clock.advanceOneMilli();
runFlowAssertResponse(loadFile("generic_success_response.xml"));
DomainBase resource = reloadResourceByForeignKey();
assertAboutDomains()
.that(resource)
.hasOnlyOneHistoryEntryWhich()
.hasType(HistoryEntry.Type.DOMAIN_UPDATE);
assertAboutDomains().that(resource).hasOnlyOneHistoryEntryWhich().hasType(DOMAIN_UPDATE);
assertThat(resource.getDsData())
.isEqualTo(
expectedDsData.stream()
@ -687,9 +687,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
.setBillingTime(clock.nowUtc())
.setParent(
getOnlyHistoryEntryOfType(
reloadResourceByForeignKey(),
HistoryEntry.Type.DOMAIN_UPDATE,
DomainHistory.class))
reloadResourceByForeignKey(), DOMAIN_UPDATE, DomainHistory.class))
.build());
} else {
assertNoBillingEvents();
@ -768,7 +766,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
.that(reloadResourceByForeignKey())
.hasStatusValue(StatusValue.CLIENT_UPDATE_PROHIBITED)
.and()
.hasStatusValue(StatusValue.SERVER_HOLD);
.hasStatusValue(SERVER_HOLD);
}
private void doSecDnsFailingTest(
@ -910,12 +908,103 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
}
@TestOfyAndSql
void testSuccess_superuserStatusValueNotClientSettable() throws Exception {
void testSuccess_superuserCanSetServerStatusValues() throws Exception {
setEppInput("domain_update_prohibited_status.xml");
persistReferencedEntities();
persistDomain();
runFlowAssertResponse(
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("generic_success_response.xml"));
// No poll message because the server status was added by the owning registrar.
assertThat(getPollMessages()).isEmpty();
}
@TestOfyAndSql
void testSuccess_addingServerStatusValue_sendsPollMessage() throws Exception {
setEppInput("domain_update_prohibited_status.xml");
persistReferencedEntities();
persistDomain();
persistResource(
reloadResourceByForeignKey()
.asBuilder()
.setPersistedCurrentSponsorRegistrarId("NewRegistrar")
.build());
runFlowAssertResponse(
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("generic_success_response.xml"));
DomainBase updatedDomain = reloadResourceByForeignKey();
assertPollMessagesForResource(
updatedDomain,
new PollMessage.OneTime.Builder()
.setEventTime(clock.nowUtc())
.setParent(getOnlyHistoryEntryOfType(updatedDomain, DOMAIN_UPDATE))
.setRegistrarId("NewRegistrar")
.setMsg("The registry administrator has added the status(es) [serverHold].")
.build());
}
@TestOfyAndSql
void testSuccess_removingServerStatusValue_sendsPollMessage() throws Exception {
setEppInput("domain_update_remove_server_statuses.xml");
persistReferencedEntities();
persistDomain();
persistResource(
reloadResourceByForeignKey()
.asBuilder()
.setPersistedCurrentSponsorRegistrarId("NewRegistrar")
.setStatusValues(
ImmutableSet.of(
SERVER_DELETE_PROHIBITED,
SERVER_TRANSFER_PROHIBITED,
SERVER_UPDATE_PROHIBITED,
CLIENT_DELETE_PROHIBITED,
CLIENT_RENEW_PROHIBITED,
CLIENT_HOLD,
SERVER_HOLD))
.build());
runFlowAssertResponse(
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("generic_success_response.xml"));
DomainBase updatedDomain = reloadResourceByForeignKey();
assertPollMessagesForResource(
updatedDomain,
new PollMessage.OneTime.Builder()
.setEventTime(clock.nowUtc())
.setParent(getOnlyHistoryEntryOfType(updatedDomain, DOMAIN_UPDATE))
.setRegistrarId("NewRegistrar")
.setMsg(
"The registry administrator has removed the status(es) [serverHold,"
+ " serverTransferProhibited, serverUpdateProhibited].")
.build());
}
@TestOfyAndSql
void testSuccess_addingAndRemovingServerStatusValues_sendsPollMessage() throws Exception {
setEppInput("domain_update_change_server_statuses.xml");
persistReferencedEntities();
persistDomain();
persistResource(
reloadResourceByForeignKey()
.asBuilder()
.setPersistedCurrentSponsorRegistrarId("NewRegistrar")
.setStatusValues(
ImmutableSet.of(
SERVER_DELETE_PROHIBITED,
SERVER_TRANSFER_PROHIBITED,
CLIENT_DELETE_PROHIBITED,
CLIENT_RENEW_PROHIBITED))
.build());
runFlowAssertResponse(
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("generic_success_response.xml"));
DomainBase updatedDomain = reloadResourceByForeignKey();
assertPollMessagesForResource(
updatedDomain,
new PollMessage.OneTime.Builder()
.setEventTime(clock.nowUtc())
.setParent(getOnlyHistoryEntryOfType(updatedDomain, DOMAIN_UPDATE))
.setRegistrarId("NewRegistrar")
.setMsg(
"The registry administrator has added the status(es) [serverHold,"
+ " serverRenewProhibited] and removed the status(es)"
+ " [serverTransferProhibited].")
.build());
}
@TestOfyAndSql

View file

@ -0,0 +1,23 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<update>
<domain:update
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>example.tld</domain:name>
<domain:add>
<domain:status s="serverHold" lang="en">
Payment overdue.
</domain:status>
<domain:status s="serverRenewProhibited"/>
<domain:status s="clientUpdateProhibited"/>
</domain:add>
<domain:rem>
<domain:status s="clientRenewProhibited"/>
<domain:status s="serverTransferProhibited"/>
</domain:rem>
<domain:chg />
</domain:update>
</update>
<clTRID>ABC-12345</clTRID>
</command>
</epp>

View file

@ -0,0 +1,19 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<update>
<domain:update
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>example.tld</domain:name>
<domain:add />
<domain:rem>
<domain:status s="clientRenewProhibited"/>
<domain:status s="serverTransferProhibited"/>
<domain:status s="serverHold"/>
<domain:status s="serverUpdateProhibited"/>
</domain:rem>
<domain:chg />
</domain:update>
</update>
<clTRID>ABC-12345</clTRID>
</command>
</epp>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<command>
<update>
<domain:update
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>example.tld</domain:name>
<domain:add>
<domain:status s="serverHold" />
</domain:add>
</domain:update>
</update>
<clTRID>ABC-12345</clTRID>
</command>
</epp>

View file

@ -0,0 +1,16 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1301">
<msg>Command completed successfully; ack to dequeue</msg>
</result>
<msgQ count="1" id="1-7-TLD-14-16-2000">
<qDate>2000-06-02T13:00:00Z</qDate>
<msg>The registry administrator has added the status(es) [serverHold].
</msg>
</msgQ>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>