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 extends ResponseExtension> 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;