Allow the nomulus renew_domain command to specify the client ID (#567)

* Allow the `nomulus renew_domain` command to specify the client ID

This means that a superuser can renew a domain and have the associated history
entry, one time billing event, and renewal grace period be recorded against a
specified registrar rather than the owning registrar of the domain.  This is
useful to e.g. renew a domain for free by "charging" the renewal to the
registry's fake registrar.  Since the grace period is written to the specified
cliend id as well, if the actual registrar deletes the domain, they don't get
back the money that they didn't pay in the first place.
This commit is contained in:
Ben McIlwain 2020-04-24 18:06:27 -04:00 committed by GitHub
parent 210de9340e
commit cd13f6c5d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 95 additions and 27 deletions

View file

@ -15,6 +15,7 @@
package google.registry.tools; package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Strings.isNullOrEmpty;
import static google.registry.model.EppResourceUtils.loadByForeignKey; import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.util.CollectionUtils.findDuplicates; import static google.registry.util.CollectionUtils.findDuplicates;
import static google.registry.util.PreconditionsUtils.checkArgumentPresent; import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
@ -37,6 +38,13 @@ import org.joda.time.format.DateTimeFormatter;
@Parameters(separators = " =", commandDescription = "Renew domain(s) via EPP.") @Parameters(separators = " =", commandDescription = "Renew domain(s) via EPP.")
final class RenewDomainCommand extends MutatingEppToolCommand { final class RenewDomainCommand extends MutatingEppToolCommand {
@Parameter(
names = {"-c", "--client"},
description =
"The registrar to execute as and bill the renewal to; otherwise each domain's sponsoring"
+ " registrar. Renewals by non-sponsoring registrars require --superuser as well.")
String clientId;
@Parameter( @Parameter(
names = {"-p", "--period"}, names = {"-p", "--period"},
description = "Number of years to renew the registration for (defaults to 1).") description = "Number of years to renew the registration for (defaults to 1).")
@ -63,7 +71,7 @@ final class RenewDomainCommand extends MutatingEppToolCommand {
setSoyTemplate(RenewDomainSoyInfo.getInstance(), RenewDomainSoyInfo.RENEWDOMAIN); setSoyTemplate(RenewDomainSoyInfo.getInstance(), RenewDomainSoyInfo.RENEWDOMAIN);
DomainBase domain = domainOptional.get(); DomainBase domain = domainOptional.get();
addSoyRecord( addSoyRecord(
domain.getCurrentSponsorClientId(), isNullOrEmpty(clientId) ? domain.getCurrentSponsorClientId() : clientId,
new SoyMapData( new SoyMapData(
"domainName", domain.getFullyQualifiedDomainName(), "domainName", domain.getFullyQualifiedDomainName(),
"expirationDate", domain.getRegistrationExpirationTime().toString(DATE_FORMATTER), "expirationDate", domain.getRegistrationExpirationTime().toString(DATE_FORMATTER),

View file

@ -153,6 +153,8 @@ public class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, D
doSuccessfulTest( doSuccessfulTest(
responseFilename, responseFilename,
renewalYears, renewalYears,
"TheRegistrar",
UserPrivileges.NORMAL,
substitutions, substitutions,
Money.of(USD, 11).multipliedBy(renewalYears)); Money.of(USD, 11).multipliedBy(renewalYears));
} }
@ -160,13 +162,16 @@ public class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, D
private void doSuccessfulTest( private void doSuccessfulTest(
String responseFilename, String responseFilename,
int renewalYears, int renewalYears,
String renewalClientId,
UserPrivileges userPrivileges,
Map<String, String> substitutions, Map<String, String> substitutions,
Money totalRenewCost) Money totalRenewCost)
throws Exception { throws Exception {
assertTransactionalFlow(true); assertTransactionalFlow(true);
DateTime currentExpiration = reloadResourceByForeignKey().getRegistrationExpirationTime(); DateTime currentExpiration = reloadResourceByForeignKey().getRegistrationExpirationTime();
DateTime newExpiration = currentExpiration.plusYears(renewalYears); DateTime newExpiration = currentExpiration.plusYears(renewalYears);
runFlowAssertResponse(loadFile(responseFilename, substitutions)); runFlowAssertResponse(
CommitMode.LIVE, userPrivileges, loadFile(responseFilename, substitutions));
DomainBase domain = reloadResourceByForeignKey(); DomainBase domain = reloadResourceByForeignKey();
HistoryEntry historyEntryDomainRenew = HistoryEntry historyEntryDomainRenew =
getOnlyHistoryEntryOfType(domain, HistoryEntry.Type.DOMAIN_RENEW); getOnlyHistoryEntryOfType(domain, HistoryEntry.Type.DOMAIN_RENEW);
@ -183,13 +188,13 @@ public class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, D
.and() .and()
.hasLastEppUpdateTime(clock.nowUtc()) .hasLastEppUpdateTime(clock.nowUtc())
.and() .and()
.hasLastEppUpdateClientId("TheRegistrar"); .hasLastEppUpdateClientId(renewalClientId);
assertAboutHistoryEntries().that(historyEntryDomainRenew).hasPeriodYears(renewalYears); assertAboutHistoryEntries().that(historyEntryDomainRenew).hasPeriodYears(renewalYears);
BillingEvent.OneTime renewBillingEvent = BillingEvent.OneTime renewBillingEvent =
new BillingEvent.OneTime.Builder() new BillingEvent.OneTime.Builder()
.setReason(Reason.RENEW) .setReason(Reason.RENEW)
.setTargetId(getUniqueIdFromCommand()) .setTargetId(getUniqueIdFromCommand())
.setClientId("TheRegistrar") .setClientId(renewalClientId)
.setCost(totalRenewCost) .setCost(totalRenewCost)
.setPeriodYears(renewalYears) .setPeriodYears(renewalYears)
.setEventTime(clock.nowUtc()) .setEventTime(clock.nowUtc())
@ -233,7 +238,7 @@ public class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, D
GracePeriod.create( GracePeriod.create(
GracePeriodStatus.RENEW, GracePeriodStatus.RENEW,
clock.nowUtc().plus(Registry.get("tld").getRenewGracePeriodLength()), clock.nowUtc().plus(Registry.get("tld").getRenewGracePeriodLength()),
"TheRegistrar", renewalClientId,
null), null),
renewBillingEvent)); renewBillingEvent));
} }
@ -256,6 +261,19 @@ public class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, D
ImmutableMap.of("DOMAIN", "example.tld", "EXDATE", "2005-04-03T22:00:00.0Z")); ImmutableMap.of("DOMAIN", "example.tld", "EXDATE", "2005-04-03T22:00:00.0Z"));
} }
@Test
public void testSuccess_recurringClientIdIsSame_whenSuperuserOverridesRenewal() throws Exception {
persistDomain();
setClientIdForFlow("NewRegistrar");
doSuccessfulTest(
"domain_renew_response.xml",
5,
"NewRegistrar",
UserPrivileges.SUPERUSER,
ImmutableMap.of("DOMAIN", "example.tld", "EXDATE", "2005-04-03T22:00:00.0Z"),
Money.of(USD, 55));
}
@Test @Test
public void testSuccess_customLogicFee() throws Exception { public void testSuccess_customLogicFee() throws Exception {
// The "costly-renew" domain has an additional RENEW fee of 100 from custom logic on top of the // The "costly-renew" domain has an additional RENEW fee of 100 from custom logic on top of the
@ -269,7 +287,13 @@ public class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, D
"FEE", "111.00"); "FEE", "111.00");
setEppInput("domain_renew_fee.xml", customFeeMap); setEppInput("domain_renew_fee.xml", customFeeMap);
persistDomain(); persistDomain();
doSuccessfulTest("domain_renew_response_fee.xml", 1, customFeeMap, Money.of(USD, 111)); doSuccessfulTest(
"domain_renew_response_fee.xml",
1,
"TheRegistrar",
UserPrivileges.NORMAL,
customFeeMap,
Money.of(USD, 111));
} }
@Test @Test
@ -687,7 +711,7 @@ public class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, D
@Test @Test
public void testFailure_unauthorizedClient() throws Exception { public void testFailure_unauthorizedClient() throws Exception {
sessionMetadata.setClientId("NewRegistrar"); setClientIdForFlow("NewRegistrar");
persistActiveDomain(getUniqueIdFromCommand()); persistActiveDomain(getUniqueIdFromCommand());
EppException thrown = assertThrows(ResourceNotOwnedException.class, this::runFlow); EppException thrown = assertThrows(ResourceNotOwnedException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml(); assertAboutEppExceptions().that(thrown).marshalsToXml();
@ -695,7 +719,7 @@ public class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, D
@Test @Test
public void testSuccess_superuserUnauthorizedClient() throws Exception { public void testSuccess_superuserUnauthorizedClient() throws Exception {
sessionMetadata.setClientId("NewRegistrar"); setClientIdForFlow("NewRegistrar");
persistDomain(); persistDomain();
runFlowAssertResponse( runFlowAssertResponse(
CommitMode.LIVE, CommitMode.LIVE,

View file

@ -15,21 +15,27 @@
package google.registry.tools; package google.registry.tools;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.DatastoreHelper.newDomainBase;
import static google.registry.testing.DatastoreHelper.persistActiveDomain; import static google.registry.testing.DatastoreHelper.persistActiveDomain;
import static google.registry.testing.DatastoreHelper.persistDeletedDomain; import static google.registry.testing.DatastoreHelper.persistDeletedDomain;
import static google.registry.testing.DatastoreHelper.persistNewRegistrar;
import static google.registry.testing.DatastoreHelper.persistResource; import static google.registry.testing.DatastoreHelper.persistResource;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
import com.beust.jcommander.ParameterException; import com.beust.jcommander.ParameterException;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import google.registry.model.domain.DomainBase;
import google.registry.model.ofy.Ofy; import google.registry.model.ofy.Ofy;
import google.registry.model.registrar.Registrar;
import google.registry.testing.FakeClock; import google.registry.testing.FakeClock;
import google.registry.testing.InjectRule; import google.registry.testing.InjectRule;
import google.registry.util.Clock; import google.registry.util.Clock;
import java.util.List;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.testcontainers.shaded.com.google.common.collect.ImmutableList;
/** Unit tests for {@link RenewDomainCommand}. */ /** Unit tests for {@link RenewDomainCommand}. */
public class RenewDomainCommandTest extends EppToolCommandTestCase<RenewDomainCommand> { public class RenewDomainCommandTest extends EppToolCommandTestCase<RenewDomainCommand> {
@ -63,23 +69,57 @@ public class RenewDomainCommandTest extends EppToolCommandTestCase<RenewDomainCo
.verifyNoMoreSent(); .verifyNoMoreSent();
} }
private static List<DomainBase> persistThreeDomains() {
ImmutableList.Builder<DomainBase> domains = new ImmutableList.Builder<>();
domains.add(
persistActiveDomain(
"domain1.tld",
DateTime.parse("2014-09-05T05:05:05Z"),
DateTime.parse("2015-09-05T05:05:05Z")));
domains.add(
persistActiveDomain(
"domain2.tld",
DateTime.parse("2014-11-05T05:05:05Z"),
DateTime.parse("2015-11-05T05:05:05Z")));
// The third domain is owned by a different registrar.
domains.add(
persistResource(
newDomainBase("domain3.tld")
.asBuilder()
.setCreationTimeForTest(DateTime.parse("2015-01-05T05:05:05Z"))
.setRegistrationExpirationTime(DateTime.parse("2016-01-05T05:05:05Z"))
.setPersistedCurrentSponsorClientId("NewRegistrar")
.build()));
return domains.build();
}
@Test @Test
public void testSuccess_multipleDomains() throws Exception { public void testSuccess_multipleDomains_renewsAndUsesEachDomainsRegistrar() throws Exception {
persistActiveDomain( persistThreeDomains();
"domain1.tld",
DateTime.parse("2014-09-05T05:05:05Z"),
DateTime.parse("2015-09-05T05:05:05Z"));
persistActiveDomain(
"domain2.tld",
DateTime.parse("2014-11-05T05:05:05Z"),
DateTime.parse("2015-11-05T05:05:05Z"));
persistActiveDomain(
"domain3.tld",
DateTime.parse("2015-01-05T05:05:05Z"),
DateTime.parse("2016-01-05T05:05:05Z"));
runCommandForced("--period 3", "domain1.tld", "domain2.tld", "domain3.tld"); runCommandForced("--period 3", "domain1.tld", "domain2.tld", "domain3.tld");
eppVerifier eppVerifier
.expectClientId("TheRegistrar") .expectClientId("TheRegistrar")
.verifySent(
"domain_renew.xml",
ImmutableMap.of("DOMAIN", "domain1.tld", "EXPDATE", "2015-09-05", "YEARS", "3"))
.verifySent(
"domain_renew.xml",
ImmutableMap.of("DOMAIN", "domain2.tld", "EXPDATE", "2015-11-05", "YEARS", "3"))
.expectClientId("NewRegistrar")
.verifySent(
"domain_renew.xml",
ImmutableMap.of("DOMAIN", "domain3.tld", "EXPDATE", "2016-01-05", "YEARS", "3"))
.verifyNoMoreSent();
}
@Test
public void testSuccess_multipleDomains_renewsAndUsesSpecifiedRegistrar() throws Exception {
persistThreeDomains();
persistNewRegistrar("reg3", "Registrar 3", Registrar.Type.REAL, 9783L);
runCommandForced("--period 3", "domain1.tld", "domain2.tld", "domain3.tld", "-u", "-c reg3");
eppVerifier
.expectClientId("reg3")
.expectSuperuser()
.verifySent( .verifySent(
"domain_renew.xml", "domain_renew.xml",
ImmutableMap.of("DOMAIN", "domain1.tld", "EXPDATE", "2015-09-05", "YEARS", "3")) ImmutableMap.of("DOMAIN", "domain1.tld", "EXPDATE", "2015-09-05", "YEARS", "3"))
@ -106,9 +146,7 @@ public class RenewDomainCommandTest extends EppToolCommandTestCase<RenewDomainCo
persistDeletedDomain("deleted.tld", DateTime.parse("2012-10-05T05:05:05Z")); persistDeletedDomain("deleted.tld", DateTime.parse("2012-10-05T05:05:05Z"));
IllegalArgumentException e = IllegalArgumentException e =
assertThrows(IllegalArgumentException.class, () -> runCommandForced("deleted.tld")); assertThrows(IllegalArgumentException.class, () -> runCommandForced("deleted.tld"));
assertThat(e) assertThat(e).hasMessageThat().isEqualTo("Domain 'deleted.tld' does not exist or is deleted");
.hasMessageThat()
.isEqualTo("Domain 'deleted.tld' does not exist or is deleted");
} }
@Test @Test
@ -128,9 +166,7 @@ public class RenewDomainCommandTest extends EppToolCommandTestCase<RenewDomainCo
IllegalArgumentException e = IllegalArgumentException e =
assertThrows( assertThrows(
IllegalArgumentException.class, () -> runCommandForced("domain.tld", "--period 10")); IllegalArgumentException.class, () -> runCommandForced("domain.tld", "--period 10"));
assertThat(e) assertThat(e).hasMessageThat().isEqualTo("Cannot renew domains for 10 or more years");
.hasMessageThat()
.isEqualTo("Cannot renew domains for 10 or more years");
} }
@Test @Test