diff --git a/java/google/registry/flows/FlowModule.java b/java/google/registry/flows/FlowModule.java index 4010677dd..b91401086 100644 --- a/java/google/registry/flows/FlowModule.java +++ b/java/google/registry/flows/FlowModule.java @@ -15,6 +15,7 @@ package google.registry.flows; import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Strings.nullToEmpty; import com.google.common.base.Optional; import com.google.common.base.Strings; @@ -22,6 +23,7 @@ import dagger.Module; import dagger.Provides; import google.registry.flows.exceptions.OnlyToolCanPassMetadataException; import google.registry.flows.picker.FlowPicker; +import google.registry.model.domain.launch.ApplicationIdTargetExtension; import google.registry.model.domain.metadata.MetadataExtension; import google.registry.model.eppcommon.AuthInfo; import google.registry.model.eppcommon.Trid; @@ -194,6 +196,15 @@ public class FlowModule { return ((SingleResourceCommand) resourceCommand).getTargetId(); } + @Provides + @FlowScope + @ApplicationId + static String provideApplicationId(EppInput eppInput) { + // Treat a missing application id as empty so we can always inject a non-null value. + return nullToEmpty( + eppInput.getSingleExtension(ApplicationIdTargetExtension.class).getApplicationId()); + } + @Provides @FlowScope @PollMessageId @@ -254,6 +265,11 @@ public class FlowModule { @Documented public @interface TargetId {} + /** Dagger qualifier for the application id for domain application flows. */ + @Qualifier + @Documented + public @interface ApplicationId {} + /** Dagger qualifier for the message id for poll flows. */ @Qualifier @Documented diff --git a/java/google/registry/flows/domain/DomainApplicationInfoFlow.java b/java/google/registry/flows/domain/DomainApplicationInfoFlow.java index 7c036280d..6083a6a05 100644 --- a/java/google/registry/flows/domain/DomainApplicationInfoFlow.java +++ b/java/google/registry/flows/domain/DomainApplicationInfoFlow.java @@ -15,17 +15,29 @@ package google.registry.flows.domain; import static google.registry.flows.EppXmlTransformer.unmarshal; +import static google.registry.flows.ResourceFlowUtils.loadResourceForQuery; +import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfoForResource; import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership; -import static google.registry.flows.domain.DomainFlowUtils.verifyLaunchApplicationIdMatchesDomain; +import static google.registry.flows.domain.DomainFlowUtils.addSecDnsExtensionIfPresent; +import static google.registry.flows.domain.DomainFlowUtils.verifyApplicationDomainMatchesTargetId; +import static google.registry.model.eppoutput.Result.Code.SUCCESS; +import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import google.registry.flows.EppException; import google.registry.flows.EppException.ParameterValuePolicyErrorException; import google.registry.flows.EppException.RequiredParameterMissingException; +import google.registry.flows.FlowModule.ApplicationId; +import google.registry.flows.FlowModule.ClientId; +import google.registry.flows.FlowModule.TargetId; +import google.registry.flows.LoggedInFlow; import google.registry.model.domain.DomainApplication; -import google.registry.model.domain.DomainApplication.Builder; +import google.registry.model.domain.DomainCommand.Info; import google.registry.model.domain.launch.LaunchInfoExtension; import google.registry.model.domain.launch.LaunchInfoResponseExtension; +import google.registry.model.eppcommon.AuthInfo; +import google.registry.model.eppinput.ResourceCommand; +import google.registry.model.eppoutput.EppOutput; import google.registry.model.eppoutput.EppResponse.ResponseExtension; import google.registry.model.mark.Mark; import google.registry.model.smd.EncodedSignedMark; @@ -33,75 +45,86 @@ import google.registry.model.smd.SignedMark; import javax.inject.Inject; /** - * An EPP flow that reads a domain application. + * An EPP flow that returns information about a domain application. + * + *

Only the registrar that owns the application can see its info. The flow can optionally include + * delegated hosts in its response. * * @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException} - * @error {@link google.registry.flows.ResourceQueryFlow.ResourceToQueryDoesNotExistException} + * @error {@link google.registry.flows.exceptions.ResourceToQueryDoesNotExistException} * @error {@link DomainFlowUtils.ApplicationDomainNameMismatchException} * @error {@link DomainApplicationInfoFlow.ApplicationLaunchPhaseMismatchException} - * @error {@link DomainApplicationInfoFlow.MissingApplicationIdException} + * @error {@link MissingApplicationIdException} */ -public class DomainApplicationInfoFlow extends BaseDomainInfoFlow { - - private boolean includeMarks; +public final class DomainApplicationInfoFlow extends LoggedInFlow { + @Inject ResourceCommand resourceCommand; + @Inject Optional authInfo; + @Inject @ClientId String clientId; + @Inject @TargetId String targetId; + @Inject @ApplicationId String applicationId; @Inject DomainApplicationInfoFlow() {} @Override - protected final void initSingleResourceFlow() throws EppException { + protected final void initLoggedInFlow() throws EppException { registerExtensions(LaunchInfoExtension.class); - // We need to do this in init rather than verify or we'll get the generic "object not found". - LaunchInfoExtension extension = eppInput.getSingleExtension(LaunchInfoExtension.class); - if (extension.getApplicationId() == null) { + } + + @Override + public final EppOutput run() throws EppException { + if (applicationId.isEmpty()) { throw new MissingApplicationIdException(); } - includeMarks = Boolean.TRUE.equals(extension.getIncludeMark()); // Default to false. - } - - @Override - protected final void verifyQueryIsAllowed() throws EppException { - verifyLaunchApplicationIdMatchesDomain(command, existingResource); - if (!existingResource.getPhase().equals( - eppInput.getSingleExtension(LaunchInfoExtension.class).getPhase())) { + DomainApplication application = + loadResourceForQuery(DomainApplication.class, applicationId, now); + verifyApplicationDomainMatchesTargetId(application, targetId); + verifyOptionalAuthInfoForResource(authInfo, application); + LaunchInfoExtension launchInfo = eppInput.getSingleExtension(LaunchInfoExtension.class); + if (!application.getPhase().equals(launchInfo.getPhase())) { throw new ApplicationLaunchPhaseMismatchException(); } + // We don't support authInfo for applications, so if it's another registrar always fail. + verifyResourceOwnership(clientId, application); + return createOutput( + SUCCESS, + getResourceInfo(application), + getDomainResponseExtensions(application, launchInfo)); } - @Override - protected final DomainApplication getResourceInfo() throws EppException { - // We don't support authInfo for applications, so if it's another registrar always fail. - verifyResourceOwnership(getClientId(), existingResource); - if (!command.getHostsRequest().requestDelegated()) { + DomainApplication getResourceInfo(DomainApplication application) { + if (!((Info) resourceCommand).getHostsRequest().requestDelegated()) { // Delegated hosts are present by default, so clear them out if they aren't wanted. // This requires overriding the implicit status values so that we don't get INACTIVE added due // to the missing nameservers. - return existingResource.asBuilder() + return application.asBuilder() .setNameservers(null) .buildWithoutImplicitStatusValues(); } - return existingResource; + return application; } - @Override - protected final ImmutableList getDomainResponseExtensions() - throws EppException { + ImmutableList getDomainResponseExtensions( + DomainApplication application, LaunchInfoExtension launchInfo) { ImmutableList.Builder marksBuilder = new ImmutableList.Builder<>(); - if (includeMarks) { - for (EncodedSignedMark encodedMark : existingResource.getEncodedSignedMarks()) { + if (Boolean.TRUE.equals(launchInfo.getIncludeMark())) { // Default to false. + for (EncodedSignedMark encodedMark : application.getEncodedSignedMarks()) { try { marksBuilder.add(unmarshal(SignedMark.class, encodedMark.getBytes()).getMark()); } catch (EppException e) { // This is a serious error; don't let the benign EppException propagate. - throw new IllegalStateException("Could not decode a stored encoded signed mark"); + throw new IllegalStateException("Could not decode a stored encoded signed mark", e); } } } - return ImmutableList.of(new LaunchInfoResponseExtension.Builder() - .setPhase(existingResource.getPhase()) - .setApplicationId(existingResource.getForeignKey()) - .setApplicationStatus(existingResource.getApplicationStatus()) + ImmutableList.Builder extensions = new ImmutableList.Builder<>(); + extensions.add(new LaunchInfoResponseExtension.Builder() + .setPhase(application.getPhase()) + .setApplicationId(application.getForeignKey()) + .setApplicationStatus(application.getApplicationStatus()) .setMarks(marksBuilder.build()) .build()); + addSecDnsExtensionIfPresent(extensions, application.getDsData()); + return extensions.build(); } /** Application id is required. */ diff --git a/java/google/registry/flows/domain/DomainFlowUtils.java b/java/google/registry/flows/domain/DomainFlowUtils.java index 8ab7fb2f8..724ba34ea 100644 --- a/java/google/registry/flows/domain/DomainFlowUtils.java +++ b/java/google/registry/flows/domain/DomainFlowUtils.java @@ -56,6 +56,7 @@ import google.registry.model.billing.BillingEvent.Reason; import google.registry.model.contact.ContactResource; import google.registry.model.domain.DesignatedContact; import google.registry.model.domain.DesignatedContact.Type; +import google.registry.model.domain.DomainApplication; import google.registry.model.domain.DomainBase; import google.registry.model.domain.DomainCommand.CreateOrUpdate; import google.registry.model.domain.DomainCommand.InvalidReferencesException; @@ -71,9 +72,11 @@ import google.registry.model.domain.fee.FeeTransformCommandExtension; import google.registry.model.domain.launch.LaunchExtension; import google.registry.model.domain.launch.LaunchPhase; import google.registry.model.domain.secdns.DelegationSignerData; +import google.registry.model.domain.secdns.SecDnsInfoExtension; import google.registry.model.eppcommon.StatusValue; import google.registry.model.eppinput.EppInput; import google.registry.model.eppinput.ResourceCommand.SingleResourceCommand; +import google.registry.model.eppoutput.EppResponse.ResponseExtension; import google.registry.model.host.HostResource; import google.registry.model.mark.Mark; import google.registry.model.mark.ProtectedMark; @@ -385,6 +388,14 @@ public class DomainFlowUtils { } } + /** Verifies that an application's domain name matches the target id (from a command). */ + static void verifyApplicationDomainMatchesTargetId( + DomainApplication application, String targetId) throws EppException { + if (!application.getFullyQualifiedDomainName().equals(targetId)) { + throw new ApplicationDomainNameMismatchException(); + } + } + /** * Verifies that a domain name is allowed to be delegated to the given client id. The only case * where it would not be allowed is if domain name is premium, and premium names are blocked by @@ -738,6 +749,21 @@ public class DomainFlowUtils { .build(); } + /** + * Adds a secDns extension to a list if the given set of dsData is non-empty. + * + *

According to RFC 5910 section 2, we should only return this if the client specified the + * "urn:ietf:params:xml:ns:secDNS-1.1" when logging in. However, this is a "SHOULD" not a "MUST" + * and we are going to ignore it; clients who don't care about secDNS can just ignore it. + */ + static void addSecDnsExtensionIfPresent( + ImmutableList.Builder extensions, + ImmutableSet dsData) { + if (!dsData.isEmpty()) { + extensions.add(SecDnsInfoExtension.create(dsData)); + } + } + /** Encoded signed marks must use base64 encoding. */ static class Base64RequiredForEncodedSignedMarksException extends ParameterValuePolicyErrorException { diff --git a/java/google/registry/flows/domain/DomainInfoFlow.java b/java/google/registry/flows/domain/DomainInfoFlow.java index 82b131744..7e81a4379 100644 --- a/java/google/registry/flows/domain/DomainInfoFlow.java +++ b/java/google/registry/flows/domain/DomainInfoFlow.java @@ -14,13 +14,23 @@ package google.registry.flows.domain; +import static google.registry.flows.ResourceFlowUtils.loadResourceForQuery; +import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfoForResource; +import static google.registry.flows.domain.DomainFlowUtils.addSecDnsExtensionIfPresent; import static google.registry.flows.domain.DomainFlowUtils.handleFeeRequest; +import static google.registry.model.eppoutput.Result.Code.SUCCESS; +import static google.registry.util.CollectionUtils.forceEmptyToNull; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.net.InternetDomainName; import google.registry.flows.EppException; +import google.registry.flows.FlowModule.ClientId; +import google.registry.flows.FlowModule.TargetId; +import google.registry.flows.LoggedInFlow; +import google.registry.model.domain.DomainCommand.Info; +import google.registry.model.domain.DomainCommand.Info.HostsRequest; import google.registry.model.domain.DomainResource; import google.registry.model.domain.DomainResource.Builder; import google.registry.model.domain.fee06.FeeInfoCommandExtensionV06; @@ -28,50 +38,70 @@ import google.registry.model.domain.fee06.FeeInfoResponseExtensionV06; import google.registry.model.domain.flags.FlagsInfoResponseExtension; import google.registry.model.domain.rgp.GracePeriodStatus; import google.registry.model.domain.rgp.RgpInfoExtension; +import google.registry.model.eppcommon.AuthInfo; +import google.registry.model.eppinput.ResourceCommand; +import google.registry.model.eppoutput.EppOutput; import google.registry.model.eppoutput.EppResponse.ResponseExtension; import java.util.List; import javax.inject.Inject; /** - * An EPP flow that reads a domain. + * An EPP flow that returns information about a domain. + * + *

The registrar that owns the domain, and any registrar presenting a valid authInfo for the + * domain, will get a rich result with all of the domain's fields. All other requests will be + * answered with a minimal result containing only basic information about the domain. * * @error {@link google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException} - * @error {@link google.registry.flows.ResourceQueryFlow.ResourceToQueryDoesNotExistException} + * @error {@link google.registry.flows.exceptions.ResourceToQueryDoesNotExistException} * @error {@link DomainFlowUtils.BadPeriodUnitException} * @error {@link DomainFlowUtils.CurrencyUnitMismatchException} * @error {@link DomainFlowUtils.FeeChecksDontSupportPhasesException} * @error {@link DomainFlowUtils.RestoresAreAlwaysForOneYearException} */ -public class DomainInfoFlow extends BaseDomainInfoFlow { +public final class DomainInfoFlow extends LoggedInFlow { + @Inject Optional authInfo; + @Inject @ClientId String clientId; + @Inject @TargetId String targetId; + @Inject ResourceCommand resourceCommand; @Inject DomainInfoFlow() {} @Override - protected void initSingleResourceFlow() throws EppException { + protected void initLoggedInFlow() throws EppException { registerExtensions(FeeInfoCommandExtensionV06.class); } @Override - protected final DomainResource getResourceInfo() { + public final EppOutput run() throws EppException { + DomainResource domain = loadResourceForQuery(DomainResource.class, targetId, now); + verifyOptionalAuthInfoForResource(authInfo, domain); + return createOutput( + SUCCESS, + getResourceInfo(domain), + getDomainResponseExtensions(domain)); + } + + private DomainResource getResourceInfo(DomainResource domain) { // If authInfo is non-null, then the caller is authorized to see the full information since we - // will have already verified the authInfo is valid in ResourceQueryFlow.verifyIsAllowed(). - if (!getClientId().equals(existingResource.getCurrentSponsorClientId()) - && command.getAuthInfo() == null) { + // will have already verified the authInfo is valid. + if (!(clientId.equals(domain.getCurrentSponsorClientId()) || authInfo.isPresent())) { // Registrars can only see a few fields on unauthorized domains. // This is a policy decision that is left up to us by the rfcs. return new DomainResource.Builder() - .setFullyQualifiedDomainName(existingResource.getFullyQualifiedDomainName()) - .setRepoId(existingResource.getRepoId()) - .setCurrentSponsorClientId(existingResource.getCurrentSponsorClientId()) - .setRegistrant(existingResource.getRegistrant()) + .setFullyQualifiedDomainName(domain.getFullyQualifiedDomainName()) + .setRepoId(domain.getRepoId()) + .setCurrentSponsorClientId(domain.getCurrentSponsorClientId()) + .setRegistrant(domain.getRegistrant()) // If we didn't do this, we'd get implicit status values. .buildWithoutImplicitStatusValues(); } - Builder info = existingResource.asBuilder(); - if (!command.getHostsRequest().requestSubordinate()) { + HostsRequest hostsRequest = ((Info) resourceCommand).getHostsRequest(); + Builder info = domain.asBuilder(); + if (!hostsRequest.requestSubordinate()) { info.setSubordinateHosts(null); } - if (!command.getHostsRequest().requestDelegated()) { + if (!hostsRequest.requestDelegated()) { // Delegated hosts are present by default, so clear them out if they aren't wanted. // This requires overriding the implicit status values so that we don't get INACTIVE added due // to the missing nameservers. @@ -80,14 +110,11 @@ public class DomainInfoFlow extends BaseDomainInfoFlow return info.build(); } - @Override - protected final ImmutableList getDomainResponseExtensions() + private ImmutableList getDomainResponseExtensions(DomainResource domain) throws EppException { ImmutableList.Builder extensions = new ImmutableList.Builder<>(); - // According to RFC 5910 section 2, we should only return this if the client specified the - // "urn:ietf:params:xml:ns:rgp-1.0" when logging in. However, this is a "SHOULD" not a "MUST" - // and we are going to ignore it; clients who don't care about rgp can just ignore it. - ImmutableSet gracePeriodStatuses = existingResource.getGracePeriodStatuses(); + addSecDnsExtensionIfPresent(extensions, domain.getDsData()); + ImmutableSet gracePeriodStatuses = domain.getGracePeriodStatuses(); if (!gracePeriodStatuses.isEmpty()) { extensions.add(RgpInfoExtension.create(gracePeriodStatuses)); } @@ -98,8 +125,8 @@ public class DomainInfoFlow extends BaseDomainInfoFlow handleFeeRequest( feeInfo, builder, - InternetDomainName.from(getTargetId()), - getClientId(), + InternetDomainName.from(targetId), + clientId, null, feeInfo.getEffectiveDate().isPresent() ? feeInfo.getEffectiveDate().get() : now, eppInput); @@ -107,15 +134,14 @@ public class DomainInfoFlow extends BaseDomainInfoFlow } // If the TLD uses the flags extension, add it to the info response. Optional extraLogicManager = - RegistryExtraFlowLogicProxy.newInstanceForDomain(existingResource); + RegistryExtraFlowLogicProxy.newInstanceForDomain(domain); if (extraLogicManager.isPresent()) { List flags = extraLogicManager.get().getExtensionFlags( - existingResource, this.getClientId(), now); // As-of date is always now for info commands. + domain, clientId, now); // As-of date is always now for info commands. if (!flags.isEmpty()) { extensions.add(FlagsInfoResponseExtension.create(flags)); } } - - return extensions.build(); + return forceEmptyToNull(extensions.build()); } } diff --git a/javatests/google/registry/flows/domain/DomainApplicationInfoFlowTest.java b/javatests/google/registry/flows/domain/DomainApplicationInfoFlowTest.java index 6d38022f5..7585f0b33 100644 --- a/javatests/google/registry/flows/domain/DomainApplicationInfoFlowTest.java +++ b/javatests/google/registry/flows/domain/DomainApplicationInfoFlowTest.java @@ -28,10 +28,10 @@ import com.google.common.collect.ImmutableSet; import com.googlecode.objectify.Key; import google.registry.flows.ResourceFlowTestCase; import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException; -import google.registry.flows.ResourceQueryFlow.ResourceToQueryDoesNotExistException; import google.registry.flows.domain.DomainApplicationInfoFlow.ApplicationLaunchPhaseMismatchException; import google.registry.flows.domain.DomainApplicationInfoFlow.MissingApplicationIdException; import google.registry.flows.domain.DomainFlowUtils.ApplicationDomainNameMismatchException; +import google.registry.flows.exceptions.ResourceToQueryDoesNotExistException; import google.registry.model.contact.ContactResource; import google.registry.model.domain.DesignatedContact; import google.registry.model.domain.DesignatedContact.Type; diff --git a/javatests/google/registry/flows/domain/DomainInfoFlowTest.java b/javatests/google/registry/flows/domain/DomainInfoFlowTest.java index 3dbfc36a9..3d8e1a17b 100644 --- a/javatests/google/registry/flows/domain/DomainInfoFlowTest.java +++ b/javatests/google/registry/flows/domain/DomainInfoFlowTest.java @@ -29,11 +29,11 @@ import com.google.common.collect.ImmutableSet; import com.googlecode.objectify.Key; import google.registry.flows.ResourceFlowTestCase; import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException; -import google.registry.flows.ResourceQueryFlow.ResourceToQueryDoesNotExistException; import google.registry.flows.domain.DomainFlowUtils.BadPeriodUnitException; import google.registry.flows.domain.DomainFlowUtils.CurrencyUnitMismatchException; import google.registry.flows.domain.DomainFlowUtils.FeeChecksDontSupportPhasesException; import google.registry.flows.domain.DomainFlowUtils.RestoresAreAlwaysForOneYearException; +import google.registry.flows.exceptions.ResourceToQueryDoesNotExistException; import google.registry.model.billing.BillingEvent.Recurring; import google.registry.model.contact.ContactAuthInfo; import google.registry.model.contact.ContactResource;