diff --git a/docs/flows.md b/docs/flows.md index 7a1598b45..01234be34 100644 --- a/docs/flows.md +++ b/docs/flows.md @@ -734,6 +734,8 @@ application. Updates cannot change the domain name that is being applied for. * Technical contact is required. * 2004 * The specified status value cannot be set by clients. + * The fees passed in the transform command do not match the fees that will + be charged. * 2102 * Changing 'maxSigLife' is not supported. * The 'urgent' attribute is not supported. diff --git a/java/google/registry/flows/domain/DomainApplicationDeleteFlow.java b/java/google/registry/flows/domain/DomainApplicationDeleteFlow.java index eafa46093..5f22f1e44 100644 --- a/java/google/registry/flows/domain/DomainApplicationDeleteFlow.java +++ b/java/google/registry/flows/domain/DomainApplicationDeleteFlow.java @@ -109,10 +109,21 @@ public final class DomainApplicationDeleteFlow implements TransactionalFlow { .build(); updateForeignKeyIndexDeletionTime(newApplication); handlePendingTransferOnDelete(existingApplication, newApplication, now, historyEntry); + handleExtraFlowLogic(tld, historyEntry, existingApplication, now); ofy().save().entities(newApplication, historyEntry); return responseBuilder.build(); } + private void handleExtraFlowLogic(String tld, HistoryEntry historyEntry, + DomainApplication existingApplication, DateTime now) throws EppException { + Optional extraFlowLogic = + RegistryExtraFlowLogicProxy.newInstanceForTld(tld); + if (extraFlowLogic.isPresent()) { + extraFlowLogic.get().performAdditionalApplicationDeleteLogic( + existingApplication, clientId, now, eppInput, historyEntry); + } + } + /** A sunrise application cannot be deleted during landrush. */ static class SunriseApplicationCannotBeDeletedInLandrushException extends StatusProhibitsOperationException { diff --git a/java/google/registry/flows/domain/DomainApplicationUpdateFlow.java b/java/google/registry/flows/domain/DomainApplicationUpdateFlow.java index 9c9bd568d..2b933431a 100644 --- a/java/google/registry/flows/domain/DomainApplicationUpdateFlow.java +++ b/java/google/registry/flows/domain/DomainApplicationUpdateFlow.java @@ -30,6 +30,7 @@ import static google.registry.flows.domain.DomainFlowUtils.cloneAndLinkReference import static google.registry.flows.domain.DomainFlowUtils.updateDsData; import static google.registry.flows.domain.DomainFlowUtils.validateContactsHaveTypes; import static google.registry.flows.domain.DomainFlowUtils.validateDsData; +import static google.registry.flows.domain.DomainFlowUtils.validateFeeChallenge; import static google.registry.flows.domain.DomainFlowUtils.validateNameserversAllowedOnTld; import static google.registry.flows.domain.DomainFlowUtils.validateNameserversCountForTld; import static google.registry.flows.domain.DomainFlowUtils.validateNoDuplicateContacts; @@ -53,11 +54,15 @@ import google.registry.flows.FlowModule.ClientId; import google.registry.flows.FlowModule.Superuser; import google.registry.flows.FlowModule.TargetId; import google.registry.flows.TransactionalFlow; +import google.registry.flows.domain.DomainFlowUtils.FeesRequiredForNonFreeUpdateException; +import google.registry.flows.domain.TldSpecificLogicProxy.EppCommandOperations; import google.registry.model.ImmutableObject; import google.registry.model.domain.DomainApplication; import google.registry.model.domain.DomainCommand.Update; import google.registry.model.domain.DomainCommand.Update.AddRemove; import google.registry.model.domain.DomainCommand.Update.Change; +import google.registry.model.domain.fee.FeeUpdateCommandExtension; +import google.registry.model.domain.flags.FlagsUpdateCommandExtension; import google.registry.model.domain.launch.ApplicationStatus; import google.registry.model.domain.launch.LaunchUpdateExtension; import google.registry.model.domain.metadata.MetadataExtension; @@ -67,8 +72,10 @@ 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.registry.Registry; import google.registry.model.reporting.HistoryEntry; import javax.inject.Inject; +import org.joda.money.Money; import org.joda.time.DateTime; /** @@ -87,6 +94,7 @@ import org.joda.time.DateTime; * @error {@link DomainFlowUtils.ApplicationDomainNameMismatchException} * @error {@link DomainFlowUtils.DuplicateContactForRoleException} * @error {@link DomainFlowUtils.EmptySecDnsUpdateException} + * @error {@link DomainFlowUtils.FeesMismatchException} * @error {@link DomainFlowUtils.LinkedResourcesDoNotExistException} * @error {@link DomainFlowUtils.MaxSigLifeChangeNotSupportedException} * @error {@link DomainFlowUtils.MissingAdminContactException} @@ -134,9 +142,11 @@ public class DomainApplicationUpdateFlow implements TransactionalFlow { @Override public final EppResponse run() throws EppException { extensionManager.register( + FeeUpdateCommandExtension.class, LaunchUpdateExtension.class, MetadataExtension.class, - SecDnsUpdateExtension.class); + SecDnsUpdateExtension.class, + FlagsUpdateCommandExtension.class); extensionManager.validate(); validateClientIsLoggedIn(clientId); DateTime now = ofy().getTransactionTime(); @@ -146,16 +156,17 @@ public class DomainApplicationUpdateFlow implements TransactionalFlow { verifyApplicationDomainMatchesTargetId(existingApplication, targetId); verifyNoDisallowedStatuses(existingApplication, UPDATE_DISALLOWED_STATUSES); verifyOptionalAuthInfo(authInfo, existingApplication); - verifyUpdateAllowed(existingApplication, command); + verifyUpdateAllowed(existingApplication, command, now); HistoryEntry historyEntry = buildHistory(existingApplication, now); DomainApplication newApplication = updateApplication(existingApplication, command, now); validateNewApplication(newApplication); + handleExtraFlowLogic(newApplication.getTld(), historyEntry, newApplication, now); ofy().save().entities(newApplication, historyEntry); return responseBuilder.build(); } protected final void verifyUpdateAllowed( - DomainApplication existingApplication, Update command) throws EppException { + DomainApplication existingApplication, Update command, DateTime now) throws EppException { AddRemove add = command.getInnerAdd(); AddRemove remove = command.getInnerRemove(); if (!isSuperuser) { @@ -170,6 +181,20 @@ public class DomainApplicationUpdateFlow implements TransactionalFlow { throw new ApplicationStatusProhibitsUpdateException( existingApplication.getApplicationStatus()); } + EppCommandOperations commandOperations = TldSpecificLogicProxy.getApplicationUpdatePrice( + Registry.get(tld), existingApplication, clientId, now, eppInput); + FeeUpdateCommandExtension feeUpdate = + eppInput.getSingleExtension(FeeUpdateCommandExtension.class); + // If the fee extension is present, validate it (even if the cost is zero, to check for price + // mismatches). Don't rely on the the validateFeeChallenge check for feeUpdate nullness, because + // it throws an error if the name is premium, and we don't want to do that here. + Money totalCost = commandOperations.getTotalCost(); + if (feeUpdate != null) { + validateFeeChallenge(targetId, tld, now, feeUpdate, totalCost); + } else if (!totalCost.isZero()) { + // If it's not present but the cost is not zero, throw an exception. + throw new FeesRequiredForNonFreeUpdateException(); + } verifyNotInPendingDelete( add.getContacts(), command.getInnerChange().getRegistrant(), @@ -223,6 +248,16 @@ public class DomainApplicationUpdateFlow implements TransactionalFlow { validateNameserversCountForTld(newApplication.getTld(), newApplication.getNameservers().size()); } + private void handleExtraFlowLogic(String tld, HistoryEntry historyEntry, + DomainApplication newApplication, DateTime now) throws EppException { + Optional extraFlowLogic = + RegistryExtraFlowLogicProxy.newInstanceForTld(tld); + if (extraFlowLogic.isPresent()) { + extraFlowLogic.get().performAdditionalApplicationUpdateLogic( + newApplication, clientId, now, eppInput, historyEntry); + } + } + /** Application status prohibits this domain update. */ static class ApplicationStatusProhibitsUpdateException extends StatusProhibitsOperationException { public ApplicationStatusProhibitsUpdateException(ApplicationStatus status) { diff --git a/java/google/registry/flows/domain/RegistryExtraFlowLogic.java b/java/google/registry/flows/domain/RegistryExtraFlowLogic.java index a39eff57d..f9da770f9 100644 --- a/java/google/registry/flows/domain/RegistryExtraFlowLogic.java +++ b/java/google/registry/flows/domain/RegistryExtraFlowLogic.java @@ -37,12 +37,7 @@ public interface RegistryExtraFlowLogic { public Set getExtensionFlags( DomainResource domainResource, String clientId, DateTime asOfDate); - /** - * Performs additional tasks required for an application create command. - * - *

Any changes should not be persisted to Datastore until commitAdditionalLogicChanges is - * called. - */ + /** Performs additional tasks required for an application create command. */ public void performAdditionalApplicationCreateLogic( DomainApplication application, String clientId, @@ -51,11 +46,30 @@ public interface RegistryExtraFlowLogic { EppInput eppInput, HistoryEntry historyEntry) throws EppException; - /** - * Computes the expected creation fee. - * - *

For use in fee challenges and the like. - */ + /** Performs additional tasks required for an application delete command. */ + public void performAdditionalApplicationDeleteLogic( + DomainApplication application, + String clientId, + DateTime asOfDate, + EppInput eppInput, + HistoryEntry historyEntry) throws EppException; + + /** Computes the expected application update fee. */ + public BaseFee getApplicationUpdateFeeOrCredit( + DomainApplication application, + String clientId, + DateTime asOfDate, + EppInput eppInput) throws EppException; + + /** Performs additional tasks required for an application update command. */ + public void performAdditionalApplicationUpdateLogic( + DomainApplication application, + String clientId, + DateTime asOfDate, + EppInput eppInput, + HistoryEntry historyEntry) throws EppException; + + /** Computes the expected creation fee. */ public BaseFee getCreateFeeOrCredit( String domainName, String clientId, @@ -80,11 +94,7 @@ public interface RegistryExtraFlowLogic { EppInput eppInput, HistoryEntry historyEntry) throws EppException; - /** - * Computes the expected renewal fee. - * - *

For use in fee challenges and the like. - */ + /** Computes the expected renewal fee. */ public BaseFee getRenewFeeOrCredit( DomainResource domain, String clientId, @@ -118,11 +128,7 @@ public interface RegistryExtraFlowLogic { EppInput eppInput, HistoryEntry historyEntry) throws EppException; - /** - * Computes the expected update fee. - * - *

For use in fee challenges and the like. - */ + /** Computes the expected update fee. */ public BaseFee getUpdateFeeOrCredit( DomainResource domain, String clientId, diff --git a/java/google/registry/flows/domain/TldSpecificLogicProxy.java b/java/google/registry/flows/domain/TldSpecificLogicProxy.java index 831a54b64..49d3b8700 100644 --- a/java/google/registry/flows/domain/TldSpecificLogicProxy.java +++ b/java/google/registry/flows/domain/TldSpecificLogicProxy.java @@ -30,6 +30,7 @@ import com.googlecode.objectify.Key; import google.registry.flows.EppException; import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException; import google.registry.model.ImmutableObject; +import google.registry.model.domain.DomainApplication; import google.registry.model.domain.DomainResource; import google.registry.model.domain.LrpTokenEntity; import google.registry.model.domain.fee.BaseFee; @@ -259,6 +260,29 @@ public final class TldSpecificLogicProxy { return new EppCommandOperations(currency, feeOrCredit); } + /** Returns a new domain application update price for the pricer. */ + public static EppCommandOperations getApplicationUpdatePrice( + Registry registry, + DomainApplication application, + String clientId, + DateTime date, + EppInput eppInput) throws EppException { + CurrencyUnit currency = registry.getCurrency(); + + // If there is extra flow logic, it may specify an update price. Otherwise, there is none. + BaseFee feeOrCredit; + Optional extraFlowLogic = + RegistryExtraFlowLogicProxy.newInstanceForTld(registry.getTldStr()); + if (extraFlowLogic.isPresent()) { + feeOrCredit = extraFlowLogic.get() + .getApplicationUpdateFeeOrCredit(application, clientId, date, eppInput); + } else { + feeOrCredit = Fee.create(Money.zero(registry.getCurrency()).getAmount(), FeeType.UPDATE); + } + + return new EppCommandOperations(currency, feeOrCredit); + } + /** Returns the fee class for a given domain and date. */ public static Optional getFeeClass(String domainName, DateTime date) { return getDomainFeeClass(domainName, date); diff --git a/javatests/google/registry/flows/domain/DomainApplicationDeleteFlowTest.java b/javatests/google/registry/flows/domain/DomainApplicationDeleteFlowTest.java index dbb031438..01059c759 100644 --- a/javatests/google/registry/flows/domain/DomainApplicationDeleteFlowTest.java +++ b/javatests/google/registry/flows/domain/DomainApplicationDeleteFlowTest.java @@ -25,6 +25,7 @@ import static google.registry.testing.DatastoreHelper.persistActiveDomainApplica import static google.registry.testing.DatastoreHelper.persistResource; import static google.registry.testing.GenericEppResourceSubject.assertAboutEppResources; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.googlecode.objectify.Key; import google.registry.flows.EppException.UnimplementedExtensionException; @@ -39,6 +40,8 @@ import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException import google.registry.model.EppResource; import google.registry.model.contact.ContactResource; import google.registry.model.domain.DomainApplication; +import google.registry.model.domain.TestExtraLogicManager; +import google.registry.model.domain.TestExtraLogicManager.TestExtraLogicManagerSuccessException; import google.registry.model.domain.launch.LaunchPhase; import google.registry.model.eppcommon.StatusValue; import google.registry.model.host.HostResource; @@ -58,6 +61,8 @@ public class DomainApplicationDeleteFlowTest @Before public void setUp() { createTld("tld", TldState.SUNRUSH); + createTld("extra", TldState.LANDRUSH); + RegistryExtraFlowLogicProxy.setOverride("extra", TestExtraLogicManager.class); } public void doSuccessfulTest() throws Exception { @@ -172,7 +177,7 @@ public class DomainApplicationDeleteFlowTest @Test public void testFailure_sunriseDuringLandrush() throws Exception { createTld("tld", TldState.LANDRUSH); - setEppInput("domain_delete_application_landrush.xml"); + setEppInput("domain_delete_application_landrush.xml", ImmutableMap.of("DOMAIN", "example.tld")); persistResource(newDomainApplication("example.tld") .asBuilder() .setRepoId("1-TLD") @@ -185,7 +190,7 @@ public class DomainApplicationDeleteFlowTest @Test public void testSuccess_superuserSunriseDuringLandrush() throws Exception { createTld("tld", TldState.LANDRUSH); - setEppInput("domain_delete_application_landrush.xml"); + setEppInput("domain_delete_application_landrush.xml", ImmutableMap.of("DOMAIN", "example.tld")); persistResource(newDomainApplication("example.tld") .asBuilder() .setRepoId("1-TLD") @@ -199,7 +204,7 @@ public class DomainApplicationDeleteFlowTest @Test public void testSuccess_sunrushDuringLandrush() throws Exception { createTld("tld", TldState.LANDRUSH); - setEppInput("domain_delete_application_landrush.xml"); + setEppInput("domain_delete_application_landrush.xml", ImmutableMap.of("DOMAIN", "example.tld")); persistResource(newDomainApplication("example.tld") .asBuilder() .setRepoId("1-TLD") @@ -222,7 +227,7 @@ public class DomainApplicationDeleteFlowTest @Test public void testFailure_mismatchedPhase() throws Exception { - setEppInput("domain_delete_application_landrush.xml"); + setEppInput("domain_delete_application_landrush.xml", ImmutableMap.of("DOMAIN", "example.tld")); persistResource( newDomainApplication("example.tld").asBuilder().setRepoId("1-TLD").build()); thrown.expect(LaunchPhaseMismatchException.class); @@ -301,4 +306,17 @@ public class DomainApplicationDeleteFlowTest thrown.expect(ApplicationDomainNameMismatchException.class); runFlow(); } + + @Test + public void testSuccess_extraLogic() throws Exception { + persistResource(newDomainApplication("example.extra") + .asBuilder() + .setRepoId("1-TLD") + .setPhase(LaunchPhase.LANDRUSH) + .build()); + setEppInput( + "domain_delete_application_landrush.xml", ImmutableMap.of("DOMAIN", "example.extra")); + thrown.expect(TestExtraLogicManagerSuccessException.class, "application deleted"); + runFlow(); + } } diff --git a/javatests/google/registry/flows/domain/DomainApplicationUpdateFlowTest.java b/javatests/google/registry/flows/domain/DomainApplicationUpdateFlowTest.java index cf2d606c9..7706a70db 100644 --- a/javatests/google/registry/flows/domain/DomainApplicationUpdateFlowTest.java +++ b/javatests/google/registry/flows/domain/DomainApplicationUpdateFlowTest.java @@ -28,6 +28,7 @@ import static google.registry.testing.DatastoreHelper.persistResource; import static google.registry.testing.DomainApplicationSubject.assertAboutApplications; import static google.registry.util.DateTimeUtils.START_OF_TIME; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.googlecode.objectify.Key; import google.registry.flows.EppException.UnimplementedExtensionException; @@ -40,6 +41,7 @@ import google.registry.flows.domain.DomainApplicationUpdateFlow.ApplicationStatu import google.registry.flows.domain.DomainFlowUtils.ApplicationDomainNameMismatchException; import google.registry.flows.domain.DomainFlowUtils.DuplicateContactForRoleException; import google.registry.flows.domain.DomainFlowUtils.EmptySecDnsUpdateException; +import google.registry.flows.domain.DomainFlowUtils.FeesMismatchException; import google.registry.flows.domain.DomainFlowUtils.LinkedResourcesDoNotExistException; import google.registry.flows.domain.DomainFlowUtils.MaxSigLifeChangeNotSupportedException; import google.registry.flows.domain.DomainFlowUtils.MissingAdminContactException; @@ -59,6 +61,8 @@ import google.registry.model.domain.DesignatedContact; import google.registry.model.domain.DesignatedContact.Type; import google.registry.model.domain.DomainApplication; import google.registry.model.domain.DomainApplication.Builder; +import google.registry.model.domain.TestExtraLogicManager; +import google.registry.model.domain.TestExtraLogicManager.TestExtraLogicManagerSuccessException; import google.registry.model.domain.launch.ApplicationStatus; import google.registry.model.domain.secdns.DelegationSignerData; import google.registry.model.eppcommon.StatusValue; @@ -89,6 +93,8 @@ public class DomainApplicationUpdateFlowTest @Before public void setUp() { createTld("tld", TldState.SUNRUSH); + createTld("flags", TldState.SUNRISE); + RegistryExtraFlowLogicProxy.setOverride("flags", TestExtraLogicManager.class); } private void persistReferencedEntities() { @@ -668,4 +674,26 @@ public class DomainApplicationUpdateFlowTest persistApplication(); doSuccessfulTest(); } + + @Test + public void testFailure_flags_feeMismatch() throws Exception { + persistReferencedEntities(); + persistResource( + newDomainApplication("update-42.flags").asBuilder().setRepoId("1-ROID").build()); + setEppInput("domain_update_sunrise_flags.xml", ImmutableMap.of("FEE", "12")); + clock.advanceOneMilli(); + thrown.expect(FeesMismatchException.class); + runFlow(); + } + + @Test + public void testSuccess_flags() throws Exception { + persistReferencedEntities(); + persistResource( + newDomainApplication("update-42.flags").asBuilder().setRepoId("1-ROID").build()); + setEppInput("domain_update_sunrise_flags.xml", ImmutableMap.of("FEE", "42")); + clock.advanceOneMilli(); + thrown.expect(TestExtraLogicManagerSuccessException.class, "add:flag1;remove:flag2"); + runFlow(); + } } diff --git a/javatests/google/registry/flows/domain/testdata/domain_delete_application_landrush.xml b/javatests/google/registry/flows/domain/testdata/domain_delete_application_landrush.xml index ba7063bbe..44ef95e2f 100644 --- a/javatests/google/registry/flows/domain/testdata/domain_delete_application_landrush.xml +++ b/javatests/google/registry/flows/domain/testdata/domain_delete_application_landrush.xml @@ -4,7 +4,7 @@ - example.tld + %DOMAIN% diff --git a/javatests/google/registry/flows/domain/testdata/domain_update_sunrise_flags.xml b/javatests/google/registry/flows/domain/testdata/domain_update_sunrise_flags.xml new file mode 100644 index 000000000..9303000e2 --- /dev/null +++ b/javatests/google/registry/flows/domain/testdata/domain_update_sunrise_flags.xml @@ -0,0 +1,39 @@ + + + + + update-42.flags + + + ns2.example.tld + + + + + ns1.example.tld + + + + + + + sunrise + 1-ROID + + + USD + %FEE% + + + + flag1 + + + flag2 + + + + ABC-12345 + + diff --git a/javatests/google/registry/model/domain/TestExtraLogicManager.java b/javatests/google/registry/model/domain/TestExtraLogicManager.java index 42360d332..4c1259e79 100644 --- a/javatests/google/registry/model/domain/TestExtraLogicManager.java +++ b/javatests/google/registry/model/domain/TestExtraLogicManager.java @@ -96,10 +96,7 @@ public class TestExtraLogicManager implements RegistryExtraFlowLogic { } } - /** - * Performs additional tasks required for an application create command. Any changes should not be - * persisted to Datastore until commitAdditionalLogicChanges is called. - */ + /** Performs additional tasks required for an application create command. */ @Override public void performAdditionalApplicationCreateLogic( DomainApplication application, @@ -116,6 +113,47 @@ public class TestExtraLogicManager implements RegistryExtraFlowLogic { throw new TestExtraLogicManagerSuccessException(Joiner.on(',').join(flags.getFlags())); } + /** Performs additional tasks required for an application create command. */ + @Override + public void performAdditionalApplicationDeleteLogic( + DomainApplication application, + String clientId, + DateTime asOfDate, + EppInput eppInput, + HistoryEntry historyEntry) throws EppException { + throw new TestExtraLogicManagerSuccessException("application deleted"); + } + + /** Computes the expected application update cost, for use in fee challenges and the like. */ + @Override + public BaseFee getApplicationUpdateFeeOrCredit( + DomainApplication application, + String clientId, + DateTime asOfDate, + EppInput eppInput) throws EppException { + return domainNameToFeeOrCredit(application.getFullyQualifiedDomainName()); + } + + /** Performs additional tasks required for an application update command. */ + @Override + public void performAdditionalApplicationUpdateLogic( + DomainApplication application, + String clientId, + DateTime asOfDate, + EppInput eppInput, + HistoryEntry historyEntry) throws EppException { + FlagsUpdateCommandExtension flags = + eppInput.getSingleExtension(FlagsUpdateCommandExtension.class); + if (flags == null) { + return; + } + throw new TestExtraLogicManagerSuccessException( + "add:" + + Joiner.on(',').join(flags.getAddFlags().getFlags()) + + ";remove:" + + Joiner.on(',').join(flags.getRemoveFlags().getFlags())); + } + /** Computes the expected create cost, for use in fee challenges and the like. */ @Override public BaseFee getCreateFeeOrCredit(