diff --git a/java/google/registry/flows/ResourceFlowUtils.java b/java/google/registry/flows/ResourceFlowUtils.java index 1b4e375b9..baec63d4f 100644 --- a/java/google/registry/flows/ResourceFlowUtils.java +++ b/java/google/registry/flows/ResourceFlowUtils.java @@ -39,6 +39,7 @@ import google.registry.flows.exceptions.ResourceStatusProhibitsOperationExceptio import google.registry.flows.exceptions.ResourceToDeleteIsReferencedException; import google.registry.flows.exceptions.ResourceToMutateDoesNotExistException; import google.registry.flows.exceptions.ResourceToQueryDoesNotExistException; +import google.registry.flows.exceptions.TooManyResourceChecksException; import google.registry.model.EppResource; import google.registry.model.EppResource.Builder; import google.registry.model.EppResource.ForeignKeyedEppResource; @@ -316,13 +317,6 @@ public class ResourceFlowUtils { } } - /** The specified resource belongs to another client. */ - public static class ResourceNotOwnedException extends AuthorizationErrorException { - public ResourceNotOwnedException() { - super("The specified resource belongs to another client"); - } - } - /** Check that the given AuthInfo is either missing or else is valid for the given resource. */ public static void verifyOptionalAuthInfoForResource( Optional authInfo, EppResource resource) throws EppException { @@ -359,6 +353,21 @@ public class ResourceFlowUtils { } } + /** Get the list of target ids from a check command. */ + public static void verifyTargetIdCount(List targetIds, int maxChecks) + throws TooManyResourceChecksException { + if (targetIds.size() > maxChecks) { + throw new TooManyResourceChecksException(maxChecks); + } + } + + /** The specified resource belongs to another client. */ + public static class ResourceNotOwnedException extends AuthorizationErrorException { + public ResourceNotOwnedException() { + super("The specified resource belongs to another client"); + } + } + /** Authorization information for accessing resource is invalid. */ public static class BadAuthInfoForResourceException extends InvalidAuthorizationInformationErrorException { diff --git a/java/google/registry/flows/contact/ContactCheckFlow.java b/java/google/registry/flows/contact/ContactCheckFlow.java index 00358eef1..dfd5659e3 100644 --- a/java/google/registry/flows/contact/ContactCheckFlow.java +++ b/java/google/registry/flows/contact/ContactCheckFlow.java @@ -14,6 +14,7 @@ package google.registry.flows.contact; +import static google.registry.flows.ResourceFlowUtils.verifyTargetIdCount; import static google.registry.model.EppResourceUtils.checkResourcesExist; import static google.registry.model.eppoutput.Result.Code.SUCCESS; @@ -21,7 +22,6 @@ import com.google.common.collect.ImmutableList; import google.registry.config.ConfigModule.Config; import google.registry.flows.EppException; import google.registry.flows.LoggedInFlow; -import google.registry.flows.exceptions.TooManyResourceChecksException; import google.registry.model.contact.ContactCommand.Check; import google.registry.model.contact.ContactResource; import google.registry.model.eppinput.ResourceCommand; @@ -48,9 +48,7 @@ public final class ContactCheckFlow extends LoggedInFlow { @Override public final EppOutput run() throws EppException { List targetIds = ((Check) resourceCommand).getTargetIds(); - if (targetIds.size() > maxChecks) { - throw new TooManyResourceChecksException(maxChecks); - } + verifyTargetIdCount(targetIds, maxChecks); Set existingIds = checkResourcesExist(ContactResource.class, targetIds, now); ImmutableList.Builder checks = new ImmutableList.Builder<>(); for (String id : targetIds) { diff --git a/java/google/registry/flows/domain/ClaimsCheckFlow.java b/java/google/registry/flows/domain/ClaimsCheckFlow.java index ae046b5f4..627670355 100644 --- a/java/google/registry/flows/domain/ClaimsCheckFlow.java +++ b/java/google/registry/flows/domain/ClaimsCheckFlow.java @@ -14,69 +14,86 @@ package google.registry.flows.domain; +import static google.registry.flows.ResourceFlowUtils.verifyTargetIdCount; +import static google.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld; +import static google.registry.flows.domain.DomainFlowUtils.validateDomainName; +import static google.registry.flows.domain.DomainFlowUtils.validateDomainNameWithIdnTables; import static google.registry.model.domain.launch.LaunchPhase.CLAIMS; +import static google.registry.model.eppoutput.Result.Code.SUCCESS; import static google.registry.util.DateTimeUtils.isAtOrAfter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.google.common.net.InternetDomainName; +import google.registry.config.ConfigModule.Config; import google.registry.flows.EppException; +import google.registry.flows.LoggedInFlow; +import google.registry.flows.exceptions.BadCommandForRegistryPhaseException; +import google.registry.model.domain.DomainCommand.Check; import google.registry.model.domain.launch.LaunchCheckExtension; import google.registry.model.domain.launch.LaunchCheckResponseExtension; import google.registry.model.domain.launch.LaunchCheckResponseExtension.LaunchCheck; import google.registry.model.domain.launch.LaunchCheckResponseExtension.LaunchCheckName; -import google.registry.model.eppoutput.CheckData; -import google.registry.model.eppoutput.EppResponse.ResponseExtension; +import google.registry.model.eppinput.ResourceCommand; +import google.registry.model.eppoutput.EppOutput; import google.registry.model.registry.Registry; import google.registry.model.registry.Registry.TldState; import google.registry.model.tmch.ClaimsListShard; -import java.util.Map.Entry; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import javax.inject.Inject; /** * An EPP flow that checks whether strings are trademarked. * - * @error {@link google.registry.flows.ResourceCheckFlow.TooManyResourceChecksException} - * @error {@link google.registry.flows.ResourceFlow.BadCommandForRegistryPhaseException} * @error {@link google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException} + * @error {@link google.registry.flows.exceptions.BadCommandForRegistryPhaseException} + * @error {@link google.registry.flows.exceptions.TooManyResourceChecksException} * @error {@link DomainFlowUtils.TldDoesNotExistException} */ -public class ClaimsCheckFlow extends BaseDomainCheckFlow { +public final class ClaimsCheckFlow extends LoggedInFlow { - public static final ImmutableSet DISALLOWED_TLD_STATES = Sets.immutableEnumSet( - TldState.PREDELEGATION, TldState.SUNRISE); + public static final ImmutableSet DISALLOWED_TLD_STATES = + Sets.immutableEnumSet(TldState.PREDELEGATION, TldState.SUNRISE); + @Inject ResourceCommand resourceCommand; + @Inject @Config("maxChecks") int maxChecks; @Inject ClaimsCheckFlow() {} @Override - protected void initDomainCheckFlow() throws EppException { + protected final void initLoggedInFlow() throws EppException { registerExtensions(LaunchCheckExtension.class); } @Override - protected CheckData getCheckData() { - return null; - } - - @Override - protected ImmutableList getResponseExtensions() throws EppException { + public EppOutput run() throws EppException { + List targetIds = ((Check) resourceCommand).getTargetIds(); + verifyTargetIdCount(targetIds, maxChecks); + Set seenTlds = new HashSet<>(); ImmutableList.Builder launchChecksBuilder = new ImmutableList.Builder<>(); - for (Entry entry : domainNames.entrySet()) { - InternetDomainName domainName = entry.getValue(); - if (isAtOrAfter(now, Registry.get(domainName.parent().toString()).getClaimsPeriodEnd())) { - throw new BadCommandForRegistryPhaseException(); + for (String targetId : ImmutableSet.copyOf(targetIds)) { + InternetDomainName domainName = validateDomainName(targetId); + validateDomainNameWithIdnTables(domainName); + String tld = domainName.parent().toString(); + // Only validate access to a TLD the first time it is encountered. + if (seenTlds.add(tld)) { + checkAllowedAccessToTld(getAllowedTlds(), tld); + Registry registry = Registry.get(tld); + if ((!isSuperuser && DISALLOWED_TLD_STATES.contains(registry.getTldState(now))) + || isAtOrAfter(now, registry.getClaimsPeriodEnd())) { + throw new BadCommandForRegistryPhaseException(); + } } String claimKey = ClaimsListShard.get().getClaimKey(domainName.parts().get(0)); - launchChecksBuilder.add(LaunchCheck.create( - LaunchCheckName.create(claimKey != null, entry.getKey()), claimKey)); + launchChecksBuilder.add( + LaunchCheck.create( + LaunchCheckName.create(claimKey != null, targetId), claimKey)); } - return ImmutableList.of( - LaunchCheckResponseExtension.create(CLAIMS, launchChecksBuilder.build())); - } - - @Override - protected final ImmutableSet getDisallowedTldStates() { - return DISALLOWED_TLD_STATES; + return createOutput( + SUCCESS, + null, + ImmutableList.of(LaunchCheckResponseExtension.create(CLAIMS, launchChecksBuilder.build()))); } } diff --git a/java/google/registry/flows/domain/DomainCheckFlow.java b/java/google/registry/flows/domain/DomainCheckFlow.java index 6f18d4f72..4cf163b75 100644 --- a/java/google/registry/flows/domain/DomainCheckFlow.java +++ b/java/google/registry/flows/domain/DomainCheckFlow.java @@ -14,11 +14,16 @@ package google.registry.flows.domain; +import static google.registry.flows.ResourceFlowUtils.verifyTargetIdCount; +import static google.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld; import static google.registry.flows.domain.DomainFlowUtils.getReservationType; import static google.registry.flows.domain.DomainFlowUtils.handleFeeRequest; +import static google.registry.flows.domain.DomainFlowUtils.validateDomainName; +import static google.registry.flows.domain.DomainFlowUtils.validateDomainNameWithIdnTables; import static google.registry.model.EppResourceUtils.checkResourcesExist; import static google.registry.model.domain.fee.Fee.FEE_CHECK_COMMAND_EXTENSIONS_IN_PREFERENCE_ORDER; import static google.registry.model.domain.fee.Fee.FEE_EXTENSION_URIS; +import static google.registry.model.eppoutput.Result.Code.SUCCESS; import static google.registry.model.index.DomainApplicationIndex.loadActiveApplicationsByDomainName; import static google.registry.model.registry.label.ReservationType.UNRESERVED; import static google.registry.pricing.PricingEngineProxy.getPricesForDomainName; @@ -27,33 +32,44 @@ import static google.registry.util.CollectionUtils.nullToEmpty; import com.google.common.base.Predicate; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.google.common.net.InternetDomainName; +import google.registry.config.ConfigModule.Config; import google.registry.flows.EppException; import google.registry.flows.EppException.ParameterValuePolicyErrorException; +import google.registry.flows.FlowModule.ClientId; +import google.registry.flows.LoggedInFlow; +import google.registry.flows.exceptions.BadCommandForRegistryPhaseException; import google.registry.model.domain.DomainApplication; +import google.registry.model.domain.DomainCommand.Check; +import google.registry.model.domain.DomainResource; import google.registry.model.domain.fee.FeeCheckCommandExtension; import google.registry.model.domain.fee.FeeCheckCommandExtensionItem; -import google.registry.model.domain.fee.FeeCheckResponseExtension; import google.registry.model.domain.fee.FeeCheckResponseExtensionItem; import google.registry.model.domain.launch.LaunchCheckExtension; -import google.registry.model.eppoutput.CheckData; +import google.registry.model.eppinput.ResourceCommand; import google.registry.model.eppoutput.CheckData.DomainCheck; import google.registry.model.eppoutput.CheckData.DomainCheckData; +import google.registry.model.eppoutput.EppOutput; import google.registry.model.eppoutput.EppResponse.ResponseExtension; import google.registry.model.registry.Registry; import google.registry.model.registry.Registry.TldState; import google.registry.model.registry.label.ReservationType; import java.util.Collections; +import java.util.HashSet; +import java.util.List; import java.util.Set; import javax.inject.Inject; /** * An EPP flow that checks whether a domain can be provisioned. * - * @error {@link google.registry.flows.ResourceCheckFlow.TooManyResourceChecksException} + *

This flow also supports the EPP fee extension and can return pricing information. + * * @error {@link google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException} + * @error {@link google.registry.flows.exceptions.TooManyResourceChecksException} * @error {@link DomainFlowUtils.BadDomainNameCharacterException} * @error {@link DomainFlowUtils.BadDomainNamePartsCountException} * @error {@link DomainFlowUtils.BadPeriodUnitException} @@ -71,7 +87,7 @@ import javax.inject.Inject; * @error {@link DomainFlowUtils.UnknownFeeCommandException} * @error {@link DomainCheckFlow.OnlyCheckedNamesCanBeFeeCheckedException} */ -public class DomainCheckFlow extends BaseDomainCheckFlow { +public final class DomainCheckFlow extends LoggedInFlow { /** * The TLD states during which we want to report a domain with pending applications as @@ -80,19 +96,55 @@ public class DomainCheckFlow extends BaseDomainCheckFlow { private static final Set PENDING_ALLOCATION_TLD_STATES = Sets.immutableEnumSet(TldState.GENERAL_AVAILABILITY, TldState.QUIET_PERIOD); + @Inject ResourceCommand resourceCommand; + @Inject @ClientId String clientId; + @Inject @Config("maxChecks") int maxChecks; @Inject DomainCheckFlow() {} @Override - protected void initDomainCheckFlow() throws EppException { + protected final void initLoggedInFlow() throws EppException { registerExtensions(LaunchCheckExtension.class); registerExtensions(FEE_CHECK_COMMAND_EXTENSIONS_IN_PREFERENCE_ORDER); } - private String getMessageForCheck(String targetId, Set existingIds) { - if (existingIds.contains(targetId)) { + @Override + public EppOutput run() throws EppException { + List targetIds = ((Check) resourceCommand).getTargetIds(); + verifyTargetIdCount(targetIds, maxChecks); + ImmutableMap.Builder domains = new ImmutableMap.Builder<>(); + // Only check that the registrar has access to a TLD the first time it is encountered + Set seenTlds = new HashSet<>(); + for (String targetId : ImmutableSet.copyOf(targetIds)) { + InternetDomainName domainName = validateDomainName(targetId); + validateDomainNameWithIdnTables(domainName); + // This validation is moderately expensive, so cache the results. + domains.put(targetId, domainName); + String tld = domainName.parent().toString(); + if (seenTlds.add(tld)) { + checkAllowedAccessToTld(getAllowedTlds(), tld); + if (!isSuperuser && TldState.PREDELEGATION.equals(Registry.get(tld).getTldState(now))) { + throw new BadCommandForRegistryPhaseException(); + } + } + } + ImmutableMap domainNames = domains.build(); + Set existingIds = checkResourcesExist(DomainResource.class, targetIds, now); + ImmutableList.Builder checks = new ImmutableList.Builder<>(); + for (String targetId : targetIds) { + String message = getMessageForCheck(domainNames.get(targetId), existingIds); + checks.add(DomainCheck.create(message == null, targetId, message)); + } + return createOutput( + SUCCESS, + DomainCheckData.create(checks.build()), + getResponseExtensions(domainNames)); + } + + private String getMessageForCheck( + InternetDomainName domainName, Set existingIds) { + if (existingIds.contains(domainName.toString())) { return "In use"; } - InternetDomainName domainName = domainNames.get(targetId); Registry registry = Registry.get(domainName.parent().toString()); if (PENDING_ALLOCATION_TLD_STATES.contains(registry.getTldState(now)) && FluentIterable.from(loadActiveApplicationsByDomainName(domainName.toString(), now)) @@ -115,15 +167,34 @@ public class DomainCheckFlow extends BaseDomainCheckFlow { return reservationType.getMessageForCheck(); } - @Override - protected CheckData getCheckData() { - Set existingIds = checkResourcesExist(resourceClass, targetIds, now); - ImmutableList.Builder checks = new ImmutableList.Builder<>(); - for (String id : targetIds) { - String message = getMessageForCheck(id, existingIds); - checks.add(DomainCheck.create(message == null, id, message)); + + /** Handle the fee check extension. */ + private ImmutableList getResponseExtensions( + ImmutableMap domainNames) throws EppException { + FeeCheckCommandExtension feeCheck = + eppInput.getFirstExtensionOfClasses(FEE_CHECK_COMMAND_EXTENSIONS_IN_PREFERENCE_ORDER); + if (feeCheck == null) { + return null; // No fee checks were requested. } - return DomainCheckData.create(checks.build()); + ImmutableList.Builder responseItems = + new ImmutableList.Builder<>(); + for (FeeCheckCommandExtensionItem feeCheckItem : feeCheck.getItems()) { + for (String domainName : getDomainNamesToCheckForFee(feeCheckItem, domainNames.keySet())) { + FeeCheckResponseExtensionItem.Builder builder = feeCheckItem.createResponseBuilder(); + handleFeeRequest( + feeCheckItem, + builder, + domainNames.get(domainName), + clientId, + feeCheck.getCurrency(), + feeCheckItem.getEffectiveDate().isPresent() + ? feeCheckItem.getEffectiveDate().get() + : now, + eppInput); + responseItems.add(builder.setDomainNameIfSupported(domainName).build()); + } + } + return ImmutableList.of(feeCheck.createResponse(responseItems.build())); } /** @@ -131,54 +202,21 @@ public class DomainCheckFlow extends BaseDomainCheckFlow { * extension specify the domain name in the extension item, while others use the list of domain * names from the regular check domain availability list. */ - private Set getDomainNamesToCheck(FeeCheckCommandExtensionItem feeCheckItem) + private Set getDomainNamesToCheckForFee( + FeeCheckCommandExtensionItem feeCheckItem, ImmutableSet availabilityCheckDomains) throws OnlyCheckedNamesCanBeFeeCheckedException { if (feeCheckItem.isDomainNameSupported()) { String domainNameInExtension = feeCheckItem.getDomainName(); - if (!domainNames.containsKey(domainNameInExtension)) { + if (!availabilityCheckDomains.contains(domainNameInExtension)) { // Although the fee extension explicitly says it's ok to fee check a domain name that you // aren't also availability checking, we forbid it. This makes the experience simpler and // also means we can assume any domain names in the fee checks have been validated. throw new OnlyCheckedNamesCanBeFeeCheckedException(); } return ImmutableSet.of(domainNameInExtension); - } else { - // If this version of the fee extension is nameless, use the full list of domains. - return domainNames.keySet(); } - } - - /** Handle the fee check extension. */ - @Override - protected ImmutableList getResponseExtensions() throws EppException { - FeeCheckCommandExtension< - ? extends FeeCheckCommandExtensionItem, ? extends FeeCheckResponseExtension> - feeCheck = eppInput.getFirstExtensionOfClasses( - FEE_CHECK_COMMAND_EXTENSIONS_IN_PREFERENCE_ORDER); - if (feeCheck == null) { - return null; // No fee checks were requested. - } - ImmutableList.Builder feeCheckResponseItemsBuilder = - new ImmutableList.Builder<>(); - for (FeeCheckCommandExtensionItem feeCheckItem : feeCheck.getItems()) { - for (String domainName : getDomainNamesToCheck(feeCheckItem)) { - FeeCheckResponseExtensionItem.Builder builder = feeCheckItem.createResponseBuilder(); - handleFeeRequest( - feeCheckItem, - builder, - domainNames.get(domainName), - getClientId(), - feeCheck.getCurrency(), - feeCheckItem.getEffectiveDate().isPresent() - ? feeCheckItem.getEffectiveDate().get() - : now, - eppInput); - feeCheckResponseItemsBuilder - .add(builder.setDomainNameIfSupported(domainName).build()); - } - } - return ImmutableList.of( - feeCheck.createResponse(feeCheckResponseItemsBuilder.build())); + // If this version of the fee extension is nameless, use the full list of domains. + return availabilityCheckDomains; } /** By server policy, fee check names must be listed in the availability check. */ diff --git a/java/google/registry/flows/host/HostCheckFlow.java b/java/google/registry/flows/host/HostCheckFlow.java index 8d4ccca97..20eba7816 100644 --- a/java/google/registry/flows/host/HostCheckFlow.java +++ b/java/google/registry/flows/host/HostCheckFlow.java @@ -14,6 +14,7 @@ package google.registry.flows.host; +import static google.registry.flows.ResourceFlowUtils.verifyTargetIdCount; import static google.registry.model.EppResourceUtils.checkResourcesExist; import static google.registry.model.eppoutput.Result.Code.SUCCESS; @@ -21,7 +22,6 @@ import com.google.common.collect.ImmutableList; import google.registry.config.ConfigModule.Config; import google.registry.flows.EppException; import google.registry.flows.LoggedInFlow; -import google.registry.flows.exceptions.TooManyResourceChecksException; import google.registry.model.eppinput.ResourceCommand; import google.registry.model.eppoutput.CheckData.HostCheck; import google.registry.model.eppoutput.CheckData.HostCheckData; @@ -48,9 +48,7 @@ public final class HostCheckFlow extends LoggedInFlow { @Override protected final EppOutput run() throws EppException { List targetIds = ((Check) resourceCommand).getTargetIds(); - if (targetIds.size() > maxChecks) { - throw new TooManyResourceChecksException(maxChecks); - } + verifyTargetIdCount(targetIds, maxChecks); Set existingIds = checkResourcesExist(HostResource.class, targetIds, now); ImmutableList.Builder checks = new ImmutableList.Builder<>(); for (String id : targetIds) { diff --git a/javatests/google/registry/flows/domain/ClaimsCheckFlowTest.java b/javatests/google/registry/flows/domain/ClaimsCheckFlowTest.java index 822dc2de4..a2bd9885b 100644 --- a/javatests/google/registry/flows/domain/ClaimsCheckFlowTest.java +++ b/javatests/google/registry/flows/domain/ClaimsCheckFlowTest.java @@ -20,11 +20,11 @@ import static google.registry.testing.DatastoreHelper.persistResource; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; -import google.registry.flows.ResourceCheckFlow.TooManyResourceChecksException; -import google.registry.flows.ResourceFlow.BadCommandForRegistryPhaseException; import google.registry.flows.ResourceFlowTestCase; import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException; import google.registry.flows.domain.DomainFlowUtils.TldDoesNotExistException; +import google.registry.flows.exceptions.BadCommandForRegistryPhaseException; +import google.registry.flows.exceptions.TooManyResourceChecksException; import google.registry.model.domain.DomainResource; import google.registry.model.registrar.Registrar; import google.registry.model.registry.Registry; diff --git a/javatests/google/registry/flows/domain/DomainCheckFlowTest.java b/javatests/google/registry/flows/domain/DomainCheckFlowTest.java index f46012ef9..169943bbf 100644 --- a/javatests/google/registry/flows/domain/DomainCheckFlowTest.java +++ b/javatests/google/registry/flows/domain/DomainCheckFlowTest.java @@ -29,7 +29,6 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; -import google.registry.flows.ResourceCheckFlow.TooManyResourceChecksException; import google.registry.flows.ResourceCheckFlowTestCase; import google.registry.flows.domain.DomainCheckFlow.OnlyCheckedNamesCanBeFeeCheckedException; import google.registry.flows.domain.DomainFlowUtils.BadDomainNameCharacterException; @@ -48,6 +47,7 @@ import google.registry.flows.domain.DomainFlowUtils.RestoresAreAlwaysForOneYearE import google.registry.flows.domain.DomainFlowUtils.TldDoesNotExistException; import google.registry.flows.domain.DomainFlowUtils.TrailingDashException; import google.registry.flows.domain.DomainFlowUtils.UnknownFeeCommandException; +import google.registry.flows.exceptions.TooManyResourceChecksException; import google.registry.model.domain.DomainResource; import google.registry.model.domain.launch.ApplicationStatus; import google.registry.model.domain.launch.LaunchPhase; @@ -705,7 +705,7 @@ public class DomainCheckFlowTest setEppInput("domain_check_fee_not_in_avail.xml"); runFlow(); } - + @Test public void testFeeExtension_multiyearRestore_v06() throws Exception { thrown.expect(RestoresAreAlwaysForOneYearException.class);