Add the ability to require premium fee acking for a registrar

When enabled for a registrar, all EPP operations on premium domains that have
costs (e.g.  creates, renews, transfers) will fail unless the EPP fee extension
is used to explicitly ack the amount of fee as part of the EPP transaction.

This ack is required regardless of whether premium fee acking is required at
the registry level. No data migration is necessary since false is the desired
default for this new attribute.

This CL also contains some slight refactoring of static utility methods used to
perform fee verification; there was short-circuiting at call-sites in two
places when what was really needed was two methods, one implementing additional
functionality on top of the other, and calling the inner method in the places
where short-circuiting had previously been necessary.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=184229363
This commit is contained in:
mcilwain 2018-02-01 18:47:22 -08:00 committed by Ben McIlwain
parent 6bcd40f18a
commit 98a61b8181
20 changed files with 264 additions and 119 deletions

View file

@ -234,7 +234,7 @@ public final class DomainApplicationCreateFlow implements TransactionalFlow {
}
Optional<FeeCreateCommandExtension> feeCreate =
eppInput.getSingleExtension(FeeCreateCommandExtension.class);
validateFeeChallenge(targetId, tld, now, feeCreate, feesAndCredits);
validateFeeChallenge(targetId, tld, clientId, now, feeCreate, feesAndCredits);
Optional<SecDnsCreateExtension> secDnsCreate =
validateSecDnsExtension(eppInput.getSingleExtension(SecDnsCreateExtension.class));
flowCustomLogic.afterValidation(

View file

@ -30,7 +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.validateFeesAckedIfPresent;
import static google.registry.flows.domain.DomainFlowUtils.validateNameserversAllowedOnDomain;
import static google.registry.flows.domain.DomainFlowUtils.validateNameserversAllowedOnTld;
import static google.registry.flows.domain.DomainFlowUtils.validateNameserversCountForTld;
@ -57,7 +57,6 @@ import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.domain.DomainFlowUtils.FeesRequiredForNonFreeOperationException;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.DomainApplication;
import google.registry.model.domain.DomainCommand.Update;
@ -191,15 +190,7 @@ public class DomainApplicationUpdateFlow implements TransactionalFlow {
pricingLogic.getApplicationUpdatePrice(registry, existingApplication, now);
Optional<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.
if (feeUpdate.isPresent()) {
validateFeeChallenge(targetId, tld, now, feeUpdate, feesAndCredits);
} else if (!feesAndCredits.getTotalCost().isZero()) {
// If it's not present but the cost is not zero, throw an exception.
throw new FeesRequiredForNonFreeOperationException(feesAndCredits.getTotalCost());
}
validateFeesAckedIfPresent(feeUpdate, feesAndCredits);
verifyNotInPendingDelete(
add.getContacts(),
command.getInnerChange().getRegistrant(),

View file

@ -271,7 +271,7 @@ public class DomainCreateFlow implements TransactionalFlow {
Optional<FeeCreateCommandExtension> feeCreate =
eppInput.getSingleExtension(FeeCreateCommandExtension.class);
FeesAndCredits feesAndCredits = pricingLogic.getCreatePrice(registry, targetId, now, years);
validateFeeChallenge(targetId, registry.getTldStr(), now, feeCreate, feesAndCredits);
validateFeeChallenge(targetId, registry.getTldStr(), clientId, now, feeCreate, feesAndCredits);
Optional<SecDnsCreateExtension> secDnsCreate =
validateSecDnsExtension(eppInput.getSingleExtension(SecDnsCreateExtension.class));
String repoId = createDomainRepoId(ObjectifyService.allocateId(), registry.getTldStr());

View file

@ -613,20 +613,44 @@ public class DomainFlowUtils {
}
}
/**
* Validates that fees are acked and match if they are required (typically for premium domains).
*
* <p>This is used by domain operations that have an implicit cost, e.g. domain create or renew
* (both of which add one or more years' worth of registration). Depending on registry and/or
* registrar settings, explicit price acking using the fee extension may be required for premium
* domain names.
*/
public static void validateFeeChallenge(
String domainName,
String tld,
String clientId,
DateTime priceTime,
final Optional<? extends FeeTransformCommandExtension> feeCommand,
FeesAndCredits feesAndCredits)
throws EppException {
Registry registry = Registry.get(tld);
if (registry.getPremiumPriceAckRequired()
&& isDomainPremium(domainName, priceTime)
&& !feeCommand.isPresent()) {
Registrar registrar = Registrar.loadByClientIdCached(clientId).get();
boolean premiumAckRequired =
registry.getPremiumPriceAckRequired() || registrar.getPremiumPriceAckRequired();
if (premiumAckRequired && isDomainPremium(domainName, priceTime) && !feeCommand.isPresent()) {
throw new FeesRequiredForPremiumNameException();
}
validateFeesAckedIfPresent(feeCommand, feesAndCredits);
}
/**
* Validates that non-zero fees are acked (i.e. they are specified and the amount matches).
*
* <p>This is used directly by update operations, i.e. those that otherwise don't have implicit
* costs, and is also used as a helper method to validate if fees are required for operations that
* do have implicit costs, e.g. creates and renews.
*/
public static void validateFeesAckedIfPresent(
final Optional<? extends FeeTransformCommandExtension> feeCommand,
FeesAndCredits feesAndCredits)
throws EppException {
// Check for the case where a fee command extension was required but not provided.
// This only happens when the total fees are non-zero and include custom fees requiring the
// extension.
@ -657,7 +681,7 @@ public class DomainFlowUtils {
total = total.add(credit.getCost());
}
Money feeTotal = null;
Money feeTotal;
try {
feeTotal = Money.of(feeCommand.get().getCurrency(), total);
} catch (ArithmeticException e) {

View file

@ -146,7 +146,8 @@ public final class DomainRenewFlow implements TransactionalFlow {
eppInput.getSingleExtension(FeeRenewCommandExtension.class);
FeesAndCredits feesAndCredits =
pricingLogic.getRenewPrice(Registry.get(existingDomain.getTld()), targetId, now, years);
validateFeeChallenge(targetId, existingDomain.getTld(), now, feeRenew, feesAndCredits);
validateFeeChallenge(
targetId, existingDomain.getTld(), clientId, now, feeRenew, feesAndCredits);
flowCustomLogic.afterValidation(
AfterValidationParameters.newBuilder()
.setExistingDomain(existingDomain)

View file

@ -204,7 +204,8 @@ public final class DomainRestoreRequestFlow implements TransactionalFlow {
if (!existingDomain.getGracePeriodStatuses().contains(GracePeriodStatus.REDEMPTION)) {
throw new DomainNotEligibleForRestoreException();
}
validateFeeChallenge(targetId, existingDomain.getTld(), now, feeUpdate, feesAndCredits);
validateFeeChallenge(
targetId, existingDomain.getTld(), clientId, now, feeUpdate, feesAndCredits);
}
private ImmutableSet<BillingEvent.OneTime> createRestoreAndRenewBillingEvents(

View file

@ -162,7 +162,7 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
? Optional.empty()
: Optional.of(pricingLogic.getTransferPrice(registry, targetId, now));
if (feesAndCredits.isPresent()) {
validateFeeChallenge(targetId, tld, now, feeTransfer, feesAndCredits.get());
validateFeeChallenge(targetId, tld, gainingClientId, now, feeTransfer, feesAndCredits.get());
}
HistoryEntry historyEntry = buildHistoryEntry(existingDomain, registry, now, period);
DateTime automaticTransferTime =

View file

@ -31,7 +31,7 @@ import static google.registry.flows.domain.DomainFlowUtils.updateDsData;
import static google.registry.flows.domain.DomainFlowUtils.validateContactsHaveTypes;
import static google.registry.flows.domain.DomainFlowUtils.validateDomainAllowedOnCreateRestrictedTld;
import static google.registry.flows.domain.DomainFlowUtils.validateDsData;
import static google.registry.flows.domain.DomainFlowUtils.validateFeeChallenge;
import static google.registry.flows.domain.DomainFlowUtils.validateFeesAckedIfPresent;
import static google.registry.flows.domain.DomainFlowUtils.validateNameserversAllowedOnDomain;
import static google.registry.flows.domain.DomainFlowUtils.validateNameserversAllowedOnTld;
import static google.registry.flows.domain.DomainFlowUtils.validateNameserversCountForTld;
@ -59,7 +59,6 @@ import google.registry.flows.custom.DomainUpdateFlowCustomLogic;
import google.registry.flows.custom.DomainUpdateFlowCustomLogic.AfterValidationParameters;
import google.registry.flows.custom.DomainUpdateFlowCustomLogic.BeforeSaveParameters;
import google.registry.flows.custom.EntityChanges;
import google.registry.flows.domain.DomainFlowUtils.FeesRequiredForNonFreeOperationException;
import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Reason;
@ -223,16 +222,8 @@ public final class DomainUpdateFlow implements TransactionalFlow {
Registry registry = Registry.get(tld);
Optional<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.
FeesAndCredits feesAndCredits = pricingLogic.getUpdatePrice(registry, targetId, now);
if (feeUpdate.isPresent()) {
validateFeeChallenge(targetId, existingDomain.getTld(), now, feeUpdate, feesAndCredits);
} else if (!feesAndCredits.getTotalCost().isZero()) {
// If it's not present but the cost is not zero, throw an exception.
throw new FeesRequiredForNonFreeOperationException(feesAndCredits.getTotalCost());
}
validateFeesAckedIfPresent(feeUpdate, feesAndCredits);
verifyNotInPendingDelete(
add.getContacts(),
command.getInnerChange().getRegistrant(),

View file

@ -412,6 +412,13 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable
*/
BillingMethod billingMethod;
/** Whether the registrar must acknowledge the price to register non-standard-priced domains. */
boolean premiumPriceAckRequired;
public boolean getPremiumPriceAckRequired() {
return premiumPriceAckRequired;
}
@NonFinalForTesting
private static Supplier<byte[]> saltSupplier =
() -> {
@ -864,6 +871,11 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable
return this;
}
public Builder setPremiumPriceAckRequired(boolean premiumPriceAckRequired) {
getInstance().premiumPriceAckRequired = premiumPriceAckRequired;
return this;
}
/** Build the registrar, nullifying empty fields. */
@Override
public Registrar build() {

View file

@ -318,7 +318,7 @@ public class Registry extends ImmutableObject implements Buildable {
/** Whether the pull queue that writes to authoritative DNS is paused for this TLD. */
boolean dnsPaused = DEFAULT_DNS_PAUSED;
/** Whether the price must be acknowledged to register premiun names on this TLD. */
/** Whether the price must be acknowledged to register premium names on this TLD. */
boolean premiumPriceAckRequired = true;
/**

View file

@ -259,6 +259,13 @@ abstract class CreateOrUpdateRegistrarCommand extends MutatingCommand {
description = "Hostname of registrar WHOIS server. (Default: whois.nic.google)")
String whoisServer;
@Nullable
@Parameter(
names = "--premium_price_ack_required",
description = "Whether operations on premium domains require explicit ack of prices",
arity = 1)
private Boolean premiumPriceAckRequired;
/** Returns the existing registrar (for update) or null (for creates). */
@Nullable
abstract Registrar getOldRegistrar(String clientId);
@ -389,21 +396,12 @@ abstract class CreateOrUpdateRegistrarCommand extends MutatingCommand {
.setCountryCode(countryCode)
.build());
}
if (blockPremiumNames != null) {
builder.setBlockPremiumNames(blockPremiumNames);
}
if (contactsRequireSyncing != null) {
builder.setContactsRequireSyncing(contactsRequireSyncing);
}
if (phonePasscode != null) {
builder.setPhonePasscode(phonePasscode);
}
if (icannReferralEmail != null) {
builder.setIcannReferralEmail(icannReferralEmail);
}
if (whoisServer != null) {
builder.setWhoisServer(whoisServer);
}
Optional.ofNullable(blockPremiumNames).ifPresent(builder::setBlockPremiumNames);
Optional.ofNullable(contactsRequireSyncing).ifPresent(builder::setContactsRequireSyncing);
Optional.ofNullable(phonePasscode).ifPresent(builder::setPhonePasscode);
Optional.ofNullable(icannReferralEmail).ifPresent(builder::setIcannReferralEmail);
Optional.ofNullable(whoisServer).ifPresent(builder::setWhoisServer);
Optional.ofNullable(premiumPriceAckRequired).ifPresent(builder::setPremiumPriceAckRequired);
// If the registrarName is being set, verify that it is either null or it normalizes uniquely.
String oldRegistrarName = (oldRegistrar == null) ? null : oldRegistrar.getRegistrarName();