From cadf9d4af22ea2d8880f0f63e18176ada42495cc Mon Sep 17 00:00:00 2001 From: mcilwain Date: Wed, 7 Sep 2016 08:58:15 -0700 Subject: [PATCH 01/31] Use smaller shard size in ClaimsListShardTest The default production value of 10,000 was unnecessarily large for testing purposes. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=132441792 --- {docs => g3doc}/app-engine-architecture.md | 0 {docs => g3doc}/code-structure.md | 0 {docs => g3doc}/configuration.md | 0 {docs => g3doc}/developing.md | 0 {docs => g3doc}/extension-points.md | 0 {docs => g3doc}/install.md | 0 {docs => g3doc}/registry-tool.md | 0 .../registry/model/tmch/ClaimsListShard.java | 7 +++++-- .../registry/model/tmch/ClaimsListShardTest.java | 15 ++++++++++++--- 9 files changed, 17 insertions(+), 5 deletions(-) rename {docs => g3doc}/app-engine-architecture.md (100%) rename {docs => g3doc}/code-structure.md (100%) rename {docs => g3doc}/configuration.md (100%) rename {docs => g3doc}/developing.md (100%) rename {docs => g3doc}/extension-points.md (100%) rename {docs => g3doc}/install.md (100%) rename {docs => g3doc}/registry-tool.md (100%) diff --git a/docs/app-engine-architecture.md b/g3doc/app-engine-architecture.md similarity index 100% rename from docs/app-engine-architecture.md rename to g3doc/app-engine-architecture.md diff --git a/docs/code-structure.md b/g3doc/code-structure.md similarity index 100% rename from docs/code-structure.md rename to g3doc/code-structure.md diff --git a/docs/configuration.md b/g3doc/configuration.md similarity index 100% rename from docs/configuration.md rename to g3doc/configuration.md diff --git a/docs/developing.md b/g3doc/developing.md similarity index 100% rename from docs/developing.md rename to g3doc/developing.md diff --git a/docs/extension-points.md b/g3doc/extension-points.md similarity index 100% rename from docs/extension-points.md rename to g3doc/extension-points.md diff --git a/docs/install.md b/g3doc/install.md similarity index 100% rename from docs/install.md rename to g3doc/install.md diff --git a/docs/registry-tool.md b/g3doc/registry-tool.md similarity index 100% rename from docs/registry-tool.md rename to g3doc/registry-tool.md diff --git a/java/google/registry/model/tmch/ClaimsListShard.java b/java/google/registry/model/tmch/ClaimsListShard.java index 58d106b61..23ac8569b 100644 --- a/java/google/registry/model/tmch/ClaimsListShard.java +++ b/java/google/registry/model/tmch/ClaimsListShard.java @@ -44,6 +44,7 @@ import google.registry.model.annotations.VirtualEntity; import google.registry.model.common.CrossTldSingleton; import google.registry.util.CollectionUtils; import google.registry.util.Concurrent; +import google.registry.util.NonFinalForTesting; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -69,8 +70,10 @@ import org.joda.time.DateTime; @NotBackedUp(reason = Reason.EXTERNALLY_SOURCED) public class ClaimsListShard extends ImmutableObject { + /** The number of claims list entries to store per shard. Do not modify except for in tests. */ @VisibleForTesting - public static final int SHARD_SIZE = 10000; + @NonFinalForTesting + static int shardSize = 10000; @Id long id; @@ -159,7 +162,7 @@ public class ClaimsListShard extends ImmutableObject { final Key parentKey = ClaimsListRevision.createKey(); // Save the ClaimsList shards in separate transactions. - Concurrent.transform(CollectionUtils.partitionMap(labelsToKeys, SHARD_SIZE), + Concurrent.transform(CollectionUtils.partitionMap(labelsToKeys, shardSize), new Function, ClaimsListShard>() { @Override public ClaimsListShard apply(final ImmutableMap labelsToKeysShard) { diff --git a/javatests/google/registry/model/tmch/ClaimsListShardTest.java b/javatests/google/registry/model/tmch/ClaimsListShardTest.java index 351de6e76..a9dacd829 100644 --- a/javatests/google/registry/model/tmch/ClaimsListShardTest.java +++ b/javatests/google/registry/model/tmch/ClaimsListShardTest.java @@ -26,10 +26,12 @@ import google.registry.model.tmch.ClaimsListShard.ClaimsListRevision; import google.registry.model.tmch.ClaimsListShard.UnshardedSaveException; import google.registry.testing.AppEngineRule; import google.registry.testing.ExceptionRule; +import google.registry.testing.InjectRule; import java.util.HashMap; import java.util.List; import java.util.Map; import org.joda.time.DateTime; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -47,7 +49,13 @@ public class ClaimsListShardTest { @Rule public final ExceptionRule thrown = new ExceptionRule(); - protected final DateTime now = DateTime.now(UTC); + @Rule + public final InjectRule inject = new InjectRule(); + + @Before + public void before() throws Exception { + inject.setStaticField(ClaimsListShard.class, "shardSize", 10); + } @Test public void test_unshardedSaveFails() throws Exception { @@ -73,9 +81,10 @@ public class ClaimsListShardTest { public void test_savesAndGets_withSharding() throws Exception { // Create a ClaimsList that will need 4 shards to save. Map labelsToKeys = new HashMap<>(); - for (int i = 0; i <= ClaimsListShard.SHARD_SIZE * 3; i++) { + for (int i = 0; i <= ClaimsListShard.shardSize * 3; i++) { labelsToKeys.put(Integer.toString(i), Integer.toString(i)); } + DateTime now = DateTime.now(UTC); // Save it with sharding, and make sure that reloading it works. ClaimsListShard unsharded = ClaimsListShard.create(now, ImmutableMap.copyOf(labelsToKeys)); unsharded.save(); @@ -88,7 +97,7 @@ public class ClaimsListShardTest { // Create a smaller ClaimsList that will need only 2 shards to save. labelsToKeys = new HashMap<>(); - for (int i = 0; i <= ClaimsListShard.SHARD_SIZE; i++) { + for (int i = 0; i <= ClaimsListShard.shardSize; i++) { labelsToKeys.put(Integer.toString(i), Integer.toString(i)); } unsharded = ClaimsListShard.create(now.plusDays(1), ImmutableMap.copyOf(labelsToKeys)); From 8cfa64359859693f8007007ae2a91a5158949c79 Mon Sep 17 00:00:00 2001 From: mcilwain Date: Wed, 7 Sep 2016 09:00:07 -0700 Subject: [PATCH 02/31] Switch Optional.ofNullable() to Optional.fromNullable() in EppMetric The latter is the canonical way to call it; the former is not available in public Guava (and is just a pass-through to fromNullable anyway). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=132441956 --- .../google/registry/monitoring/whitebox/EppMetric.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/java/google/registry/monitoring/whitebox/EppMetric.java b/java/google/registry/monitoring/whitebox/EppMetric.java index 832d6f9f6..7ca133f3f 100644 --- a/java/google/registry/monitoring/whitebox/EppMetric.java +++ b/java/google/registry/monitoring/whitebox/EppMetric.java @@ -69,11 +69,11 @@ public abstract class EppMetric implements BigQueryMetric { requestId, startTimestamp, endTimestamp, - Optional.ofNullable(commandName), - Optional.ofNullable(clientId), - Optional.ofNullable(privilegeLevel), - Optional.ofNullable(eppTarget), - Optional.ofNullable(status), + Optional.fromNullable(commandName), + Optional.fromNullable(clientId), + Optional.fromNullable(privilegeLevel), + Optional.fromNullable(eppTarget), + Optional.fromNullable(status), attempts); } From 2b42118aaf69edb28a0097ca878b33419a6a3054 Mon Sep 17 00:00:00 2001 From: mcilwain Date: Wed, 7 Sep 2016 09:06:34 -0700 Subject: [PATCH 03/31] Make boolean operator precedence unambiguous in ClaimsListShard ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=132442796 --- java/google/registry/model/tmch/ClaimsListShard.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/java/google/registry/model/tmch/ClaimsListShard.java b/java/google/registry/model/tmch/ClaimsListShard.java index 23ac8569b..87a1bc48c 100644 --- a/java/google/registry/model/tmch/ClaimsListShard.java +++ b/java/google/registry/model/tmch/ClaimsListShard.java @@ -181,8 +181,9 @@ public class ClaimsListShard extends ImmutableObject { ofy().transactNew(new VoidWork() { @Override public void vrun() { - verify(getCurrentRevision() == null && oldRevision == null - || getCurrentRevision().equals(oldRevision), + verify( + (getCurrentRevision() == null && oldRevision == null) + || getCurrentRevision().equals(oldRevision), "ClaimsList on Registries was updated by someone else while attempting to update."); ofy().saveWithoutBackup().entity(ClaimsListSingleton.create(parentKey)); // Delete the old ClaimsListShard entities. From c41c4dbbdccbe8da61fc3397f7354e63a29dd239 Mon Sep 17 00:00:00 2001 From: mcilwain Date: Wed, 7 Sep 2016 11:33:37 -0700 Subject: [PATCH 04/31] Fix alphabetical ordering in FOSS schema ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=132460165 --- javatests/google/registry/model/schema.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javatests/google/registry/model/schema.txt b/javatests/google/registry/model/schema.txt index aaf323318..249634e6b 100644 --- a/javatests/google/registry/model/schema.txt +++ b/javatests/google/registry/model/schema.txt @@ -251,10 +251,10 @@ class google.registry.model.domain.DomainBase { class google.registry.model.domain.DomainResource { @Id java.lang.String repoId; com.google.common.collect.ImmutableSortedMap> revisions; - com.googlecode.objectify.Key deletePollMessage; com.googlecode.objectify.Key autorenewBillingEvent; com.googlecode.objectify.Key application; com.googlecode.objectify.Key autorenewPollMessage; + com.googlecode.objectify.Key deletePollMessage; google.registry.model.CreateAutoTimestamp creationTime; google.registry.model.UpdateAutoTimestamp updateTimestamp; google.registry.model.domain.DomainAuthInfo authInfo; From a6db24c8bb2c7b33ee629ad43062e681ad5f8fd8 Mon Sep 17 00:00:00 2001 From: mcilwain Date: Wed, 7 Sep 2016 14:04:49 -0700 Subject: [PATCH 05/31] Fix NullPointerException in group syncing We were trying to set the scopes too early in the creation of a GoogleCredential. They aren't yet known at this point in the code and should be an empty collection (and also not null, which yields an NPE). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=132477868 --- java/google/registry/request/BUILD | 1 + java/google/registry/request/Modules.java | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/java/google/registry/request/BUILD b/java/google/registry/request/BUILD index 3b5140c2c..269656aa5 100644 --- a/java/google/registry/request/BUILD +++ b/java/google/registry/request/BUILD @@ -42,6 +42,7 @@ java_library( "//java/com/google/api/client/json", "//java/com/google/api/client/json/jackson2", "//java/com/google/common/base", + "//java/com/google/common/collect", "//third_party/java/appengine:appengine-api", "//third_party/java/dagger", "//java/google/registry/config", diff --git a/java/google/registry/request/Modules.java b/java/google/registry/request/Modules.java index ef92c4062..94eb0f516 100644 --- a/java/google/registry/request/Modules.java +++ b/java/google/registry/request/Modules.java @@ -32,6 +32,7 @@ import com.google.appengine.api.urlfetch.URLFetchServiceFactory; import com.google.appengine.api.users.UserService; import com.google.appengine.api.users.UserServiceFactory; import com.google.common.base.Function; +import com.google.common.collect.ImmutableSet; import dagger.Binds; import dagger.Module; import dagger.Provides; @@ -211,6 +212,10 @@ public final class Modules { * Provides a GoogleCredential that will connect to GAE using delegated admin access. This is * needed for API calls requiring domain admin access to the relevant GAFYD using delegated * scopes, e.g. the Directory API and the Groupssettings API. + * + *

Note that you must call {@link GoogleCredential#createScoped} on the credential provided + * by this method first before using it, as this does not and cannot set the scopes, and a + * credential without scopes doesn't actually provide access to do anything. */ @Provides @Singleton @@ -226,7 +231,11 @@ public final class Modules { .setServiceAccountPrivateKey(googleCredential.getServiceAccountPrivateKey()) // TODO(b/31317128): Also set serviceAccountProjectId from value off googleCredential when // that functionality is publicly released. - .setServiceAccountScopes(googleCredential.getServiceAccountScopes()) + // Set the scopes to empty because the default value is null, which throws an NPE in the + // GoogleCredential constructor. We don't yet know the actual scopes to use here, and it + // is thus the responsibility of every user of a delegated admin credential to call + // createScoped() on it first to get the version with the correct scopes set. + .setServiceAccountScopes(ImmutableSet.of()) .setServiceAccountUser(googleAppsAdminEmailAddress) .build(); } From 95cc7ab3d80b976ee9acba3a833a29b2b52650e1 Mon Sep 17 00:00:00 2001 From: mountford Date: Wed, 7 Sep 2016 15:23:50 -0700 Subject: [PATCH 06/31] Add extra logic for all relevant flows This CL enhances various domain flows (check, create, delete, renew, restore, transfer, update) so that they invoke the appropriate methods on the object implementing the TLD's RegistryExtraFlowLogic (if any). TldSpecificLogicProxy is also updated to invoke RegistryExtraFlowLogic proxy (if any) to fetch the appropriate price. The tests use a made-up extra flow logic object which can be attached to a test TLD to make sure that the proper routines are being invoked. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=132486734 --- java/google/registry/flows/EppException.java | 8 + .../flows/ResourceSyncDeleteFlow.java | 4 +- .../flows/ResourceTransferRequestFlow.java | 15 +- .../flows/domain/BaseDomainCreateFlow.java | 25 +- .../flows/domain/BaseDomainUpdateFlow.java | 19 +- .../domain/DomainApplicationCreateFlow.java | 3 +- .../flows/domain/DomainCheckFlow.java | 4 +- .../flows/domain/DomainCreateFlow.java | 13 +- .../domain/DomainCreateOrAllocateFlow.java | 1 + .../flows/domain/DomainDeleteFlow.java | 20 +- .../flows/domain/DomainFlowUtils.java | 44 ++- .../registry/flows/domain/DomainInfoFlow.java | 6 +- .../flows/domain/DomainRenewFlow.java | 21 +- .../domain/DomainRestoreRequestFlow.java | 14 + .../domain/DomainTransferRequestFlow.java | 21 +- .../flows/domain/DomainUpdateFlow.java | 6 +- .../flows/domain/RegistryExtraFlowLogic.java | 91 +++++- .../domain/RegistryExtraFlowLogicProxy.java | 23 +- .../flows/domain/TldSpecificLogicProxy.java | 297 ++++++++++++++++++ .../registry/model/domain/fee/BaseFee.java | 4 +- .../registry/model/domain/fee/Credit.java | 5 +- .../fee/FeeTransformCommandExtensionImpl.java | 4 +- .../fee/FeeTransformResponseExtension.java | 6 +- .../FeeTransformResponseExtensionImpl.java | 9 +- ...ansformResponseExtensionImplNoCredits.java | 5 +- .../flags/FlagsCreateCommandExtension.java | 4 + .../flags/FlagsTransferCommandExtension.java | 8 + .../pricing/TldSpecificLogicProxy.java | 195 ------------ .../flows/domain/DomainCreateFlowTest.java | 21 +- .../flows/domain/DomainDeleteFlowTest.java | 15 +- .../flows/domain/DomainRenewFlowTest.java | 14 +- .../domain/DomainRestoreRequestFlowTest.java | 22 +- .../domain/DomainTransferRequestFlowTest.java | 13 + .../flows/domain/DomainUpdateFlowTest.java | 2 +- .../domain/TldSpecificLogicProxyTest.java | 221 +++++++++++++ ...eck_fee_multiple_commands_response_v06.xml | 2 +- ...eck_fee_multiple_commands_response_v12.xml | 2 +- ..._check_fee_premium_response_v11_update.xml | 2 +- .../domain_check_fee_premium_response_v12.xml | 2 +- .../domain/testdata/domain_create_flags.xml | 32 ++ .../domain/testdata/domain_delete_flags.xml | 11 + .../domain/testdata/domain_renew_flags.xml | 13 + .../domain_transfer_request_flags.xml | 27 ++ .../domain_update_restore_request_flags.xml | 17 + .../model/domain/TestExtraLogicManager.java | 170 +++++++++- .../pricing/TldSpecificLogicProxyTest.java | 106 ------- 46 files changed, 1173 insertions(+), 394 deletions(-) create mode 100644 java/google/registry/flows/domain/TldSpecificLogicProxy.java delete mode 100644 java/google/registry/pricing/TldSpecificLogicProxy.java create mode 100644 javatests/google/registry/flows/domain/TldSpecificLogicProxyTest.java create mode 100644 javatests/google/registry/flows/domain/testdata/domain_create_flags.xml create mode 100644 javatests/google/registry/flows/domain/testdata/domain_delete_flags.xml create mode 100644 javatests/google/registry/flows/domain/testdata/domain_renew_flags.xml create mode 100644 javatests/google/registry/flows/domain/testdata/domain_transfer_request_flags.xml create mode 100644 javatests/google/registry/flows/domain/testdata/domain_update_restore_request_flags.xml delete mode 100644 javatests/google/registry/pricing/TldSpecificLogicProxyTest.java diff --git a/java/google/registry/flows/EppException.java b/java/google/registry/flows/EppException.java index 405810d18..345e8ca4d 100644 --- a/java/google/registry/flows/EppException.java +++ b/java/google/registry/flows/EppException.java @@ -250,4 +250,12 @@ public abstract class EppException extends Exception { super("Specified protocol version is not implemented"); } } + + /** Specified protocol version is not implemented. */ + @EppResultCode(Code.CommandFailed) + public static class CommandFailedException extends EppException { + public CommandFailedException() { + super("Command failed"); + } + } } diff --git a/java/google/registry/flows/ResourceSyncDeleteFlow.java b/java/google/registry/flows/ResourceSyncDeleteFlow.java index 974a3dcfa..31171ae92 100644 --- a/java/google/registry/flows/ResourceSyncDeleteFlow.java +++ b/java/google/registry/flows/ResourceSyncDeleteFlow.java @@ -36,7 +36,7 @@ public abstract class ResourceSyncDeleteFlow @Override @SuppressWarnings("unchecked") - protected final R createOrMutateResource() { + protected final R createOrMutateResource() throws EppException { B builder = (B) prepareDeletedResourceAsBuilder(existingResource, now); setDeleteProperties(builder); return builder.build(); @@ -52,7 +52,7 @@ public abstract class ResourceSyncDeleteFlow /** Set any resource-specific properties before deleting. */ @SuppressWarnings("unused") - protected void setDeleteProperties(B builder) {} + protected void setDeleteProperties(B builder) throws EppException {} /** Modify any other resources that need to be informed of this delete. */ protected void modifySyncDeleteRelatedResources() {} diff --git a/java/google/registry/flows/ResourceTransferRequestFlow.java b/java/google/registry/flows/ResourceTransferRequestFlow.java index 01a75fdb7..4b60f7d70 100644 --- a/java/google/registry/flows/ResourceTransferRequestFlow.java +++ b/java/google/registry/flows/ResourceTransferRequestFlow.java @@ -77,7 +77,7 @@ import org.joda.time.Duration; }}; @Override - protected final void initResourceCreateOrMutateFlow() { + protected final void initResourceCreateOrMutateFlow() throws EppException { initResourceTransferRequestFlow(); } @@ -100,7 +100,8 @@ import org.joda.time.Duration; verifyTransferRequestIsAllowed(); } - private TransferData.Builder createTransferDataBuilder(TransferStatus transferStatus) { + private TransferData.Builder + createTransferDataBuilder(TransferStatus transferStatus) throws EppException { TransferData.Builder builder = new TransferData.Builder() .setGainingClientId(gainingClient.getId()) .setTransferRequestTime(now) @@ -113,7 +114,7 @@ import org.joda.time.Duration; } private PollMessage createPollMessage( - Client client, TransferStatus transferStatus, DateTime eventTime) { + Client client, TransferStatus transferStatus, DateTime eventTime) throws EppException { ImmutableList.Builder responseData = new ImmutableList.Builder<>(); responseData.add(createTransferResponse( existingResource, createTransferDataBuilder(transferStatus).build(), now)); @@ -132,7 +133,7 @@ import org.joda.time.Duration; @Override @SuppressWarnings("unchecked") - protected final R createOrMutateResource() { + protected final R createOrMutateResource() throws EppException { // Figure out transfer expiration time once we've verified that the existingResource does in // fact exist (otherwise we won't know which TLD to get this figure off of). transferExpirationTime = now.plus(getAutomaticTransferLength()); @@ -158,7 +159,7 @@ import org.joda.time.Duration; } /** Subclasses can override this to do further initialization. */ - protected void initResourceTransferRequestFlow() {} + protected void initResourceTransferRequestFlow() throws EppException {} /** * Subclasses can override this to return the keys of any entities that need to be deleted if the @@ -173,8 +174,8 @@ import org.joda.time.Duration; protected void verifyTransferRequestIsAllowed() throws EppException {} /** Subclasses can override this to modify fields on the transfer data builder. */ - protected void setTransferDataProperties( - @SuppressWarnings("unused") TransferData.Builder builder) {} + @SuppressWarnings("unused") + protected void setTransferDataProperties(TransferData.Builder builder) throws EppException {} @Override protected final EppOutput getOutput() throws EppException { diff --git a/java/google/registry/flows/domain/BaseDomainCreateFlow.java b/java/google/registry/flows/domain/BaseDomainCreateFlow.java index f481f8d44..3b619a561 100644 --- a/java/google/registry/flows/domain/BaseDomainCreateFlow.java +++ b/java/google/registry/flows/domain/BaseDomainCreateFlow.java @@ -51,12 +51,14 @@ import google.registry.flows.EppException.StatusProhibitsOperationException; import google.registry.flows.EppException.UnimplementedOptionException; import google.registry.flows.ResourceCreateFlow; import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException; +import google.registry.flows.domain.TldSpecificLogicProxy.EppCommandOperations; import google.registry.model.domain.DomainBase; import google.registry.model.domain.DomainBase.Builder; import google.registry.model.domain.DomainCommand.Create; import google.registry.model.domain.DomainResource; import google.registry.model.domain.LrpToken; import google.registry.model.domain.fee.FeeTransformCommandExtension; +import google.registry.model.domain.flags.FlagsCreateCommandExtension; import google.registry.model.domain.launch.LaunchCreateExtension; import google.registry.model.domain.launch.LaunchNotice; import google.registry.model.domain.launch.LaunchNotice.InvalidChecksumException; @@ -67,8 +69,6 @@ import google.registry.model.registry.Registry; import google.registry.model.registry.Registry.TldState; import google.registry.model.smd.SignedMark; import google.registry.model.tmch.ClaimsListShard; -import google.registry.pricing.TldSpecificLogicProxy; -import google.registry.pricing.TldSpecificLogicProxy.EppCommandOperations; import java.util.Set; import javax.annotation.Nullable; @@ -95,16 +95,20 @@ public abstract class BaseDomainCreateFlow lrpToken; + protected Optional extraFlowLogic; + @Override public final void initResourceCreateOrMutateFlow() throws EppException { command = cloneAndLinkReferences(command, now); - registerExtensions(SecDnsCreateExtension.class); + registerExtensions(SecDnsCreateExtension.class, FlagsCreateCommandExtension.class); + registerExtensions(FEE_CREATE_COMMAND_EXTENSIONS_IN_PREFERENCE_ORDER); secDnsCreate = eppInput.getSingleExtension(SecDnsCreateExtension.class); launchCreate = eppInput.getSingleExtension(LaunchCreateExtension.class); feeCreate = eppInput.getFirstExtensionOfClasses(FEE_CREATE_COMMAND_EXTENSIONS_IN_PREFERENCE_ORDER); hasSignedMarks = launchCreate != null && !launchCreate.getSignedMarks().isEmpty(); initDomainCreateFlow(); + // We can't initialize extraFlowLogic here, because the TLD has not been checked yet. } @Override @@ -181,9 +185,19 @@ public abstract class BaseDomainCreateFlow credits; + protected Optional extraFlowLogic; + @Inject DomainDeleteFlow() {} @Override protected void initResourceCreateOrMutateFlow() throws EppException { registerExtensions(SecDnsUpdateExtension.class); + extraFlowLogic = RegistryExtraFlowLogicProxy.newInstanceForDomain(existingResource); } @Override @@ -93,7 +98,7 @@ public class DomainDeleteFlow extends ResourceSyncDeleteFlow builder, getTargetId(), existingResource.getTld(), + getClientId(), null, - now); + now, + eppInput); extensions.add(builder.build()); } // If the TLD uses the flags extension, add it to the info response. Optional extraLogicManager = - RegistryExtraFlowLogicProxy.newInstanceForTld(existingResource.getTld()); + RegistryExtraFlowLogicProxy.newInstanceForDomain(existingResource); if (extraLogicManager.isPresent()) { List flags = extraLogicManager.get().getExtensionFlags( existingResource, this.getClientId(), now); // As-of date is always now for info commands. diff --git a/java/google/registry/flows/domain/DomainRenewFlow.java b/java/google/registry/flows/domain/DomainRenewFlow.java index ff9775194..9d16f43c6 100644 --- a/java/google/registry/flows/domain/DomainRenewFlow.java +++ b/java/google/registry/flows/domain/DomainRenewFlow.java @@ -28,6 +28,7 @@ import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.pricing.PricingEngineProxy.getDomainRenewCost; import static google.registry.util.DateTimeUtils.leapSafeAddYears; +import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.googlecode.objectify.Key; @@ -84,6 +85,8 @@ public class DomainRenewFlow extends OwnedResourceMutateFlow extraFlowLogic; + @Inject DomainRenewFlow() {} @Override @@ -96,6 +99,7 @@ public class DomainRenewFlow extends OwnedResourceMutateFlowentities(explicitRenewEvent, newAutorenewEvent, newAutorenewPollMessage); return existingResource.asBuilder() .setRegistrationExpirationTime(newExpirationTime) @@ -160,6 +171,14 @@ public class DomainRenewFlow extends OwnedResourceMutateFlow extraFlowLogic; @Inject DomainRestoreRequestFlow() {} @@ -82,6 +84,7 @@ public class DomainRestoreRequestFlow extends OwnedResourceMutateFlowentities(restoreEvent, autorenewEvent, autorenewPollMessage, renewEvent); + + // Handle extra flow logic, if any. + if (extraFlowLogic.isPresent()) { + extraFlowLogic.get().performAdditionalDomainRestoreLogic( + existingResource, getClientId(), now, eppInput); + } + return existingResource.asBuilder() .setRegistrationExpirationTime(newExpirationTime) .setDeletionTime(END_OF_TIME) @@ -171,6 +181,10 @@ public class DomainRestoreRequestFlow extends OwnedResourceMutateFlow extraFlowLogic; /** * An optional extension from the client specifying how much they think the transfer should cost. @@ -101,7 +106,8 @@ public class DomainTransferRequestFlow } @Override - protected final void initResourceTransferRequestFlow() { + protected final void initResourceTransferRequestFlow() throws EppException { + registerExtensions(FlagsTransferCommandExtension.class); registerExtensions(FEE_TRANSFER_COMMAND_EXTENSIONS_IN_PREFERENCE_ORDER); feeTransfer = eppInput.getFirstExtensionOfClasses( FEE_TRANSFER_COMMAND_EXTENSIONS_IN_PREFERENCE_ORDER); @@ -146,6 +152,7 @@ public class DomainTransferRequestFlow .setMsg("Domain was auto-renewed.") .setParent(historyEntry) .build(); + extraFlowLogic = RegistryExtraFlowLogicProxy.newInstanceForDomain(existingResource); } @Override @@ -174,12 +181,18 @@ public class DomainTransferRequestFlow } @Override - protected void setTransferDataProperties(TransferData.Builder builder) { + protected void setTransferDataProperties(TransferData.Builder builder) throws EppException { builder .setServerApproveBillingEvent(Key.create(transferBillingEvent)) .setServerApproveAutorenewEvent(Key.create(gainingClientAutorenewEvent)) .setServerApproveAutorenewPollMessage(Key.create(gainingClientAutorenewPollMessage)) .setExtendedRegistrationYears(command.getPeriod().getValue()); + + // Handle extra flow logic, if any. + if (extraFlowLogic.isPresent()) { + extraFlowLogic.get().performAdditionalDomainTransferLogic( + existingResource, getClientId(), now, command.getPeriod().getValue(), eppInput); + } } /** @@ -233,6 +246,10 @@ public class DomainTransferRequestFlow // transfer occurs, then the logic in cloneProjectedAtTime() will move the // serverApproveAutoRenewEvent into the autoRenewEvent field. updateAutorenewRecurrenceEndTime(existingResource, automaticTransferTime); + + if (extraFlowLogic.isPresent()) { + extraFlowLogic.get().commitAdditionalLogicChanges(); + } } @Override diff --git a/java/google/registry/flows/domain/DomainUpdateFlow.java b/java/google/registry/flows/domain/DomainUpdateFlow.java index c1991f825..f8f22e6ef 100644 --- a/java/google/registry/flows/domain/DomainUpdateFlow.java +++ b/java/google/registry/flows/domain/DomainUpdateFlow.java @@ -138,7 +138,7 @@ public class DomainUpdateFlow extends BaseDomainUpdateFlow getExtensionFlags( DomainResource domainResource, String clientIdentifier, DateTime asOfDate); + /** Computes the expected creation fee, for use in fee challenges and the like. */ + public BaseFee getCreateFeeOrCredit( + String domainName, + String clientIdentifier, + DateTime asOfDate, + int years, + EppInput eppInput) throws EppException; + /** - * Add and remove flags passed via the EPP flags extension. Any changes should not be persisted to - * Datastore until commitAdditionalDomainUpdates is called. Name suggested by Benjamin McIlwain. + * Performs additional tasks required for a create command. Any changes should not be persisted to + * Datastore until commitAdditionalLogicChanges is called. */ - public void performAdditionalDomainUpdateLogic( - DomainResource domainResource, + public void performAdditionalDomainCreateLogic( + DomainResource domain, + String clientIdentifier, + DateTime asOfDate, + int years, + EppInput eppInput) throws EppException; + + /** + * Performs additional tasks required for a delete command. Any changes should not be persisted to + * Datastore until commitAdditionalLogicChanges is called. + */ + public void performAdditionalDomainDeleteLogic( + DomainResource domain, String clientIdentifier, DateTime asOfDate, EppInput eppInput) throws EppException; - /** Commit any changes made as a result of a call to performAdditionalDomainUpdateLogic(). */ - public void commitAdditionalDomainUpdates(); + /** Computes the expected renewal fee, for use in fee challenges and the like. */ + public BaseFee getRenewFeeOrCredit( + DomainResource domain, + String clientIdentifier, + DateTime asOfDate, + int years, + EppInput eppInput) throws EppException; + + /** + * Performs additional tasks required for a renew command. Any changes should not be persisted + * to Datastore until commitAdditionalLogicChanges is called. + */ + public void performAdditionalDomainRenewLogic( + DomainResource domain, + String clientIdentifier, + DateTime asOfDate, + int years, + EppInput eppInput) throws EppException; + + /** + * Performs additional tasks required for a restore command. Any changes should not be persisted + * to Datastore until commitAdditionalLogicChanges is called. + */ + public void performAdditionalDomainRestoreLogic( + DomainResource domain, + String clientIdentifier, + DateTime asOfDate, + EppInput eppInput) throws EppException; + + /** + * Performs additional tasks required for a transfer command. Any changes should not be persisted + * to Datastore until commitAdditionalLogicChanges is called. + */ + public void performAdditionalDomainTransferLogic( + DomainResource domain, + String clientIdentifier, + DateTime asOfDate, + int years, + EppInput eppInput) throws EppException; + + /** Computes the expected update fee, for use in fee challenges and the like. */ + public BaseFee getUpdateFeeOrCredit( + DomainResource domain, + String clientIdentifier, + DateTime asOfDate, + EppInput eppInput) throws EppException; + + /** + * Performs additional tasks required for an update command. Any changes should not be persisted + * to Datastore until commitAdditionalLogicChanges is called. + */ + public void performAdditionalDomainUpdateLogic( + DomainResource domain, + String clientIdentifier, + DateTime asOfDate, + EppInput eppInput) throws EppException; + + /** Commits any changes made as a result of a call to one of the performXXX methods. */ + public void commitAdditionalLogicChanges(); } diff --git a/java/google/registry/flows/domain/RegistryExtraFlowLogicProxy.java b/java/google/registry/flows/domain/RegistryExtraFlowLogicProxy.java index 1f5a7f148..b1f7f815e 100644 --- a/java/google/registry/flows/domain/RegistryExtraFlowLogicProxy.java +++ b/java/google/registry/flows/domain/RegistryExtraFlowLogicProxy.java @@ -15,8 +15,12 @@ package google.registry.flows.domain; import com.google.common.base.Optional; +import google.registry.flows.EppException; +import google.registry.flows.EppException.CommandFailedException; +import google.registry.model.domain.DomainBase; import google.registry.model.registry.Registry; import java.util.HashMap; +import javax.annotation.Nullable; /** * Static class to return the correct {@link RegistryExtraFlowLogic} for a particular TLD. @@ -36,12 +40,23 @@ public class RegistryExtraFlowLogicProxy { extraLogicOverrideMap.put(tld, extraLogicClass); } - public static Optional newInstanceForTld(String tld) { + public static Optional + newInstanceForDomain(@Nullable D domain) throws EppException { + if (domain == null) { + return Optional.absent(); + } else { + return newInstanceForTld(domain.getTld()); + } + } + + public static Optional + newInstanceForTld(String tld) throws EppException { if (extraLogicOverrideMap.containsKey(tld)) { try { - return Optional.of(extraLogicOverrideMap.get(tld).newInstance()); - } catch (InstantiationException | IllegalAccessException e) { - return Optional.absent(); + return Optional.of( + extraLogicOverrideMap.get(tld).getConstructor().newInstance()); + } catch (ReflectiveOperationException ex) { + throw new CommandFailedException(); } } return Optional.absent(); diff --git a/java/google/registry/flows/domain/TldSpecificLogicProxy.java b/java/google/registry/flows/domain/TldSpecificLogicProxy.java new file mode 100644 index 000000000..ee7ae0629 --- /dev/null +++ b/java/google/registry/flows/domain/TldSpecificLogicProxy.java @@ -0,0 +1,297 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.domain; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static google.registry.model.EppResourceUtils.loadByUniqueId; +import static google.registry.model.ofy.ObjectifyService.ofy; +import static google.registry.pricing.PricingEngineProxy.getPricesForDomainName; +import static google.registry.util.CollectionUtils.nullToEmpty; +import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; +import com.googlecode.objectify.Key; +import google.registry.flows.EppException; +import google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException; +import google.registry.model.ImmutableObject; +import google.registry.model.domain.DomainCommand.Create; +import google.registry.model.domain.DomainResource; +import google.registry.model.domain.LrpToken; +import google.registry.model.domain.fee.BaseFee; +import google.registry.model.domain.fee.BaseFee.FeeType; +import google.registry.model.domain.fee.Credit; +import google.registry.model.domain.fee.EapFee; +import google.registry.model.domain.fee.Fee; +import google.registry.model.eppinput.EppInput; +import google.registry.model.pricing.PremiumPricingEngine.DomainPrices; +import google.registry.model.registry.Registry; +import java.util.List; +import org.joda.money.CurrencyUnit; +import org.joda.money.Money; +import org.joda.time.DateTime; + +/** + * Provides pricing, billing, and update logic, with call-outs that can be customized by providing + * implementations on a per-TLD basis. + */ +public final class TldSpecificLogicProxy { + /** A collection of fees and credits for a specific EPP transform. */ + public static final class EppCommandOperations extends ImmutableObject { + private final CurrencyUnit currency; + private final ImmutableList fees; + private final ImmutableList credits; + + /** Constructs an EppCommandOperations object using separate lists of fees and credits. */ + EppCommandOperations( + CurrencyUnit currency, ImmutableList fees, ImmutableList credits) { + this.currency = checkArgumentNotNull( + currency, "Currency may not be null in EppCommandOperations."); + checkArgument(!fees.isEmpty(), "You must specify one or more fees."); + this.fees = checkArgumentNotNull(fees, "Fees may not be null in EppCommandOperations."); + this.credits = + checkArgumentNotNull(credits, "Credits may not be null in EppCommandOperations."); + } + + /** + * Constructs an EppCommandOperations object. The arguments are sorted into fees and credits. + */ + EppCommandOperations(CurrencyUnit currency, BaseFee... feesAndCredits) { + this.currency = checkArgumentNotNull( + currency, "Currency may not be null in EppCommandOperations."); + ImmutableList.Builder feeBuilder = new ImmutableList.Builder<>(); + ImmutableList.Builder creditBuilder = new ImmutableList.Builder<>(); + for (BaseFee feeOrCredit : feesAndCredits) { + if (feeOrCredit instanceof Credit) { + creditBuilder.add((Credit) feeOrCredit); + } else { + feeBuilder.add((Fee) feeOrCredit); + } + } + this.fees = feeBuilder.build(); + this.credits = creditBuilder.build(); + } + + private Money getTotalCostForType(FeeType type) { + Money result = Money.zero(currency); + checkArgumentNotNull(type); + for (Fee fee : fees) { + if (fee.getType() == type) { + result = result.plus(fee.getCost()); + } + } + return result; + } + + /** Returns the total cost of all fees and credits for the event. */ + public Money getTotalCost() { + Money result = Money.zero(currency); + for (Fee fee : fees) { + result = result.plus(fee.getCost()); + } + for (Credit credit : credits) { + result = result.plus(credit.getCost()); + } + return result; + } + + /** Returns the create cost for the event. */ + public Money getCreateCost() { + return getTotalCostForType(FeeType.CREATE); + } + + /** Returns the EAP cost for the event. */ + public Money getEapCost() { + return getTotalCostForType(FeeType.EAP); + } + + /** Returns the list of fees for the event. */ + public ImmutableList getFees() { + return fees; + } + + /** Returns the list of credits for the event. */ + public List getCredits() { + return nullToEmpty(credits); + } + + /** Returns the currency for all fees in the event. */ + public final CurrencyUnit getCurrency() { + return currency; + } + } + + private TldSpecificLogicProxy() {} + + /** Returns a new create price for the Pricer. */ + public static EppCommandOperations getCreatePrice( + Registry registry, + String domainName, + String clientIdentifier, + DateTime date, + int years, + EppInput eppInput) throws EppException { + CurrencyUnit currency = registry.getCurrency(); + + // Get the create cost, either from the extra flow logic or straight from PricingEngineProxy. + BaseFee createFeeOrCredit; + Optional extraFlowLogic = + RegistryExtraFlowLogicProxy.newInstanceForTld(registry.getTldStr()); + if (extraFlowLogic.isPresent()) { + createFeeOrCredit = extraFlowLogic.get() + .getCreateFeeOrCredit(domainName, clientIdentifier, date, years, eppInput); + } else { + DomainPrices prices = getPricesForDomainName(domainName, date); + createFeeOrCredit = + Fee.create(prices.getCreateCost().multipliedBy(years).getAmount(), FeeType.CREATE); + } + + // Create fees for the cost and the EAP fee, if any. + EapFee eapFee = registry.getEapFeeFor(date); + Money eapFeeCost = eapFee.getCost(); + checkState(eapFeeCost.getCurrencyUnit().equals(currency)); + if (!eapFeeCost.getAmount().equals(Money.zero(currency).getAmount())) { + return new EppCommandOperations( + currency, + createFeeOrCredit, + Fee.create(eapFeeCost.getAmount(), FeeType.EAP, eapFee.getPeriod().upperEndpoint())); + } else { + return new EppCommandOperations(currency, createFeeOrCredit); + } + } + + /** + * Computes the renew fee or credit. This is called by other methods which use the renew fee + * (renew, restore, etc). + */ + static BaseFee getRenewFeeOrCredit( + Registry registry, + String domainName, + String clientIdentifier, + DateTime date, + int years, + EppInput eppInput) throws EppException { + Optional extraFlowLogic = + RegistryExtraFlowLogicProxy.newInstanceForTld(registry.getTldStr()); + if (extraFlowLogic.isPresent()) { + // TODO: Consider changing the method definition to have the domain passed in to begin with. + DomainResource domain = loadByUniqueId(DomainResource.class, domainName, date); + if (domain == null) { + throw new ResourceToMutateDoesNotExistException(DomainResource.class, domainName); + } + return + extraFlowLogic.get().getRenewFeeOrCredit(domain, clientIdentifier, date, years, eppInput); + } else { + DomainPrices prices = getPricesForDomainName(domainName, date); + return Fee.create(prices.getRenewCost().multipliedBy(years).getAmount(), FeeType.RENEW); + } + } + + /** Returns a new renew price for the pricer. */ + public static EppCommandOperations getRenewPrice( + Registry registry, + String domainName, + String clientIdentifier, + DateTime date, + int years, + EppInput eppInput) throws EppException { + return new EppCommandOperations( + registry.getCurrency(), + getRenewFeeOrCredit(registry, domainName, clientIdentifier, date, years, eppInput)); + } + + /** Returns a new restore price for the pricer. */ + public static EppCommandOperations getRestorePrice( + Registry registry, + String domainName, + String clientIdentifier, + DateTime date, + EppInput eppInput) throws EppException { + return new EppCommandOperations( + registry.getCurrency(), + getRenewFeeOrCredit(registry, domainName, clientIdentifier, date, 1, eppInput), + Fee.create(registry.getStandardRestoreCost().getAmount(), FeeType.RESTORE)); + } + + /** Returns a new transfer price for the pricer. */ + public static EppCommandOperations getTransferPrice( + Registry registry, + String domainName, + String clientIdentifier, + DateTime transferDate, + int years, + EppInput eppInput) throws EppException { + // Currently, all transfer prices = renew prices, so just pass through. + return getRenewPrice( + registry, domainName, clientIdentifier, transferDate, years, eppInput); + } + + /** Returns a new update price for the pricer. */ + public static EppCommandOperations getUpdatePrice( + Registry registry, + String domainName, + String clientIdentifier, + 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()) { + // TODO: Consider changing the method definition to have the domain passed in to begin with. + DomainResource domain = loadByUniqueId(DomainResource.class, domainName, date); + if (domain == null) { + throw new ResourceToMutateDoesNotExistException(DomainResource.class, domainName); + } + feeOrCredit = + extraFlowLogic.get().getUpdateFeeOrCredit(domain, clientIdentifier, 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 getPricesForDomainName(domainName, date).getFeeClass(); + } + + /** + * Checks whether a {@link Create} command has a valid {@link LrpToken} for a particular TLD, and + * return that token (wrapped in an {@link Optional}) if one exists. + * + *

This method has no knowledge of whether or not an auth code (interpreted here as an LRP + * token) has already been checked against the reserved list for QLP (anchor tenant), as auth + * codes are used for both types of registrations. + */ + public static Optional getMatchingLrpToken(Create createCommand, String tld) { + // Note that until the actual per-TLD logic is built out, what's being done here is a basic + // domain-name-to-assignee match. + String lrpToken = createCommand.getAuthInfo().getPw().getValue(); + LrpToken token = ofy().load().key(Key.create(LrpToken.class, lrpToken)).now(); + if (token != null) { + if (token.getAssignee().equalsIgnoreCase(createCommand.getFullyQualifiedDomainName()) + && token.getRedemptionHistoryEntry() == null + && token.getValidTlds().contains(tld)) { + return Optional.of(token); + } + } + return Optional.absent(); + } +} diff --git a/java/google/registry/model/domain/fee/BaseFee.java b/java/google/registry/model/domain/fee/BaseFee.java index 5b10e888e..bdee83ab7 100644 --- a/java/google/registry/model/domain/fee/BaseFee.java +++ b/java/google/registry/model/domain/fee/BaseFee.java @@ -45,7 +45,9 @@ public abstract class BaseFee extends ImmutableObject { CREATE("create"), EAP("Early Access Period, fee expires: %s"), RENEW("renew"), - RESTORE("restore"); + RESTORE("restore"), + UPDATE("update"), + CREDIT("%s credit"); private final String formatString; diff --git a/java/google/registry/model/domain/fee/Credit.java b/java/google/registry/model/domain/fee/Credit.java index ce64e0aac..aa34291b3 100644 --- a/java/google/registry/model/domain/fee/Credit.java +++ b/java/google/registry/model/domain/fee/Credit.java @@ -21,11 +21,12 @@ import java.math.BigDecimal; /** A credit, in currency units specified elsewhere in the xml, and with an optional description. */ public class Credit extends BaseFee { - public static Credit create(BigDecimal cost, String description) { + public static Credit create(BigDecimal cost, FeeType type, Object... descriptionArgs) { Credit instance = new Credit(); instance.cost = checkNotNull(cost); checkArgument(instance.cost.signum() < 0); - instance.description = description; + instance.type = checkNotNull(type); + instance.generateDescription(descriptionArgs); return instance; } } diff --git a/java/google/registry/model/domain/fee/FeeTransformCommandExtensionImpl.java b/java/google/registry/model/domain/fee/FeeTransformCommandExtensionImpl.java index 10204d8c5..9d7b76561 100644 --- a/java/google/registry/model/domain/fee/FeeTransformCommandExtensionImpl.java +++ b/java/google/registry/model/domain/fee/FeeTransformCommandExtensionImpl.java @@ -14,6 +14,8 @@ package google.registry.model.domain.fee; +import static google.registry.util.CollectionUtils.nullToEmpty; + import google.registry.model.ImmutableObject; import java.util.List; import javax.xml.bind.annotation.XmlElement; @@ -51,6 +53,6 @@ public abstract class FeeTransformCommandExtensionImpl @Override public List getCredits() { - return credits; + return nullToEmpty(credits); } } diff --git a/java/google/registry/model/domain/fee/FeeTransformResponseExtension.java b/java/google/registry/model/domain/fee/FeeTransformResponseExtension.java index c75b208f9..ead5e687f 100644 --- a/java/google/registry/model/domain/fee/FeeTransformResponseExtension.java +++ b/java/google/registry/model/domain/fee/FeeTransformResponseExtension.java @@ -14,8 +14,8 @@ package google.registry.model.domain.fee; -import com.google.common.collect.ImmutableList; import google.registry.model.eppoutput.EppResponse.ResponseExtension; +import java.util.List; import org.joda.money.CurrencyUnit; /** Interface for fee extensions in Create, Renew, Transfer and Update responses. */ @@ -24,8 +24,8 @@ public interface FeeTransformResponseExtension extends ResponseExtension { /** Builder for {@link FeeTransformResponseExtension}. */ public interface Builder { Builder setCurrency(CurrencyUnit currency); - Builder setFees(ImmutableList fees); - Builder setCredits(ImmutableList credits); + Builder setFees(List fees); + Builder setCredits(List credits); FeeTransformResponseExtension build(); } } diff --git a/java/google/registry/model/domain/fee/FeeTransformResponseExtensionImpl.java b/java/google/registry/model/domain/fee/FeeTransformResponseExtensionImpl.java index 8c443c25b..855877292 100644 --- a/java/google/registry/model/domain/fee/FeeTransformResponseExtensionImpl.java +++ b/java/google/registry/model/domain/fee/FeeTransformResponseExtensionImpl.java @@ -14,7 +14,8 @@ package google.registry.model.domain.fee; -import com.google.common.collect.ImmutableList; +import static google.registry.util.CollectionUtils.forceEmptyToNull; + import google.registry.model.Buildable.GenericBuilder; import google.registry.model.ImmutableObject; import java.util.List; @@ -53,14 +54,14 @@ public class FeeTransformResponseExtensionImpl extends ImmutableObject } @Override - public B setFees(ImmutableList fees) { + public B setFees(List fees) { getInstance().fees = fees; return thisCastToDerived(); } @Override - public B setCredits(ImmutableList credits) { - getInstance().credits = credits; + public B setCredits(List credits) { + getInstance().credits = forceEmptyToNull(credits); return thisCastToDerived(); } } diff --git a/java/google/registry/model/domain/fee/FeeTransformResponseExtensionImplNoCredits.java b/java/google/registry/model/domain/fee/FeeTransformResponseExtensionImplNoCredits.java index c5f67f285..99f901057 100644 --- a/java/google/registry/model/domain/fee/FeeTransformResponseExtensionImplNoCredits.java +++ b/java/google/registry/model/domain/fee/FeeTransformResponseExtensionImplNoCredits.java @@ -14,7 +14,6 @@ package google.registry.model.domain.fee; -import com.google.common.collect.ImmutableList; import google.registry.model.Buildable.GenericBuilder; import google.registry.model.ImmutableObject; import java.util.List; @@ -54,13 +53,13 @@ public class FeeTransformResponseExtensionImplNoCredits extends ImmutableObject } @Override - public B setFees(ImmutableList fees) { + public B setFees(List fees) { getInstance().fees = fees; return thisCastToDerived(); } @Override - public B setCredits(ImmutableList credits) { + public B setCredits(List credits) { return thisCastToDerived(); } } diff --git a/java/google/registry/model/domain/flags/FlagsCreateCommandExtension.java b/java/google/registry/model/domain/flags/FlagsCreateCommandExtension.java index fadf94107..e73a618c2 100644 --- a/java/google/registry/model/domain/flags/FlagsCreateCommandExtension.java +++ b/java/google/registry/model/domain/flags/FlagsCreateCommandExtension.java @@ -30,4 +30,8 @@ import javax.xml.bind.annotation.XmlRootElement; public class FlagsCreateCommandExtension implements CommandExtension { @XmlElement(name = "flag") List flags; + + public List getFlags() { + return flags; + } } diff --git a/java/google/registry/model/domain/flags/FlagsTransferCommandExtension.java b/java/google/registry/model/domain/flags/FlagsTransferCommandExtension.java index c55ed796a..84088c365 100644 --- a/java/google/registry/model/domain/flags/FlagsTransferCommandExtension.java +++ b/java/google/registry/model/domain/flags/FlagsTransferCommandExtension.java @@ -30,4 +30,12 @@ import javax.xml.bind.annotation.XmlType; public class FlagsTransferCommandExtension implements CommandExtension { FlagsList add; // list of flags to be added (turned on) FlagsList rem; // list of flags to be removed (turned off) + + public FlagsList getAddFlags() { + return add; + } + + public FlagsList getRemoveFlags() { + return rem; + } } diff --git a/java/google/registry/pricing/TldSpecificLogicProxy.java b/java/google/registry/pricing/TldSpecificLogicProxy.java deleted file mode 100644 index 21d344822..000000000 --- a/java/google/registry/pricing/TldSpecificLogicProxy.java +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright 2016 The Domain Registry Authors. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package google.registry.pricing; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkState; -import static google.registry.model.ofy.ObjectifyService.ofy; -import static google.registry.pricing.PricingEngineProxy.getPricesForDomainName; -import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; - -import com.google.common.base.Optional; -import com.google.common.collect.ImmutableList; -import com.googlecode.objectify.Key; -import google.registry.model.ImmutableObject; -import google.registry.model.domain.DomainCommand.Create; -import google.registry.model.domain.LrpToken; -import google.registry.model.domain.fee.BaseFee.FeeType; -import google.registry.model.domain.fee.EapFee; -import google.registry.model.domain.fee.Fee; -import google.registry.model.pricing.PremiumPricingEngine.DomainPrices; -import google.registry.model.registry.Registry; -import org.joda.money.CurrencyUnit; -import org.joda.money.Money; -import org.joda.time.DateTime; - -/** - * Provides pricing, billing, and update logic, with call-outs that can be customized by providing - * implementations on a per-TLD basis. - */ -public final class TldSpecificLogicProxy { - /** - * A collection of fees for a specific event. - */ - public static final class EppCommandOperations extends ImmutableObject { - private final CurrencyUnit currency; - private final ImmutableList fees; - - EppCommandOperations(CurrencyUnit currency, ImmutableList fees) { - this.currency = checkArgumentNotNull( - currency, "Currency may not be null in EppCommandOperations."); - checkArgument(!fees.isEmpty(), "You must specify one or more fees."); - this.fees = checkArgumentNotNull(fees, "Fees may not be null in EppCommandOperations."); - } - - private Money getTotalCostForType(FeeType type) { - Money result = Money.zero(currency); - checkArgumentNotNull(type); - for (Fee fee : fees) { - if (fee.getType() == type) { - result = result.plus(fee.getCost()); - } - } - return result; - } - - /** Returns the total cost of all fees for the event. */ - public Money getTotalCost() { - Money result = Money.zero(currency); - for (Fee fee : fees) { - result = result.plus(fee.getCost()); - } - return result; - } - - /** Returns the create cost for the event. */ - public Money getCreateCost() { - return getTotalCostForType(FeeType.CREATE); - } - - /** Returns the EAP cost for the event. */ - public Money getEapCost() { - return getTotalCostForType(FeeType.EAP); - } - - /** - * Returns all costs for the event as a list of fees. - */ - public ImmutableList getFees() { - return fees; - } - - /** - * Returns the currency for all fees in the event. - */ - public final CurrencyUnit getCurrency() { - return currency; - } - } - - private TldSpecificLogicProxy() {} - - /** - * Returns a new "create" price for the Pricer. - */ - public static EppCommandOperations getCreatePrice( - Registry registry, String domainName, DateTime date, int years) { - DomainPrices prices = getPricesForDomainName(domainName, date); - CurrencyUnit currency = registry.getCurrency(); - ImmutableList.Builder feeBuilder = new ImmutableList.Builder<>(); - - // Add Create cost. - feeBuilder.add( - Fee.create(prices.getCreateCost().multipliedBy(years).getAmount(), FeeType.CREATE)); - - // Add EAP Fee. - EapFee eapFee = registry.getEapFeeFor(date); - Money eapFeeCost = eapFee.getCost(); - checkState(eapFeeCost.getCurrencyUnit().equals(currency)); - if (!eapFeeCost.getAmount().equals(Money.zero(currency).getAmount())) { - feeBuilder.add( - Fee.create( - eapFeeCost.getAmount(), FeeType.EAP, eapFee.getPeriod().upperEndpoint())); - } - - return new EppCommandOperations(currency, feeBuilder.build()); - } - - /** - * Returns a new renew price for the pricer. - */ - public static EppCommandOperations getRenewPrice( - Registry registry, String domainName, DateTime date, int years) { - DomainPrices prices = getPricesForDomainName(domainName, date); - return new EppCommandOperations( - registry.getCurrency(), - ImmutableList.of( - Fee.create( - prices.getRenewCost().multipliedBy(years).getAmount(), FeeType.RENEW))); - } - - /** - * Returns a new restore price for the pricer. - */ - public static EppCommandOperations getRestorePrice( - Registry registry, String domainName, DateTime date, int years) { - DomainPrices prices = getPricesForDomainName(domainName, date); - return new EppCommandOperations( - registry.getCurrency(), - ImmutableList.of( - Fee.create( - prices.getRenewCost().multipliedBy(years).getAmount(), FeeType.RENEW), - Fee.create(registry.getStandardRestoreCost().getAmount(), FeeType.RESTORE))); - } - - /** - * Returns a new transfer price for the pricer. - */ - public static EppCommandOperations getTransferPrice( - Registry registry, String domainName, DateTime transferDate, int additionalYears) { - // Currently, all transfer prices = renew prices, so just pass through. - return getRenewPrice(registry, domainName, transferDate, additionalYears); - } - - /** - * Returns the fee class for a given domain and date. - */ - public static Optional getFeeClass(String domainName, DateTime date) { - return getPricesForDomainName(domainName, date).getFeeClass(); - } - - /** - * Checks whether a {@link Create} command has a valid {@link LrpToken} for a particular TLD, and - * return that token (wrapped in an {@link Optional}) if one exists. - * - *

This method has no knowledge of whether or not an auth code (interpreted here as an LRP - * token) has already been checked against the reserved list for QLP (anchor tenant), as auth - * codes are used for both types of registrations. - */ - public static Optional getMatchingLrpToken(Create createCommand, String tld) { - // Note that until the actual per-TLD logic is built out, what's being done here is a basic - // domain-name-to-assignee match. - String lrpToken = createCommand.getAuthInfo().getPw().getValue(); - LrpToken token = ofy().load().key(Key.create(LrpToken.class, lrpToken)).now(); - if (token != null) { - if (token.getAssignee().equalsIgnoreCase(createCommand.getFullyQualifiedDomainName()) - && token.getRedemptionHistoryEntry() == null - && token.getValidTlds().contains(tld)) { - return Optional.of(token); - } - } - return Optional.absent(); - } -} diff --git a/javatests/google/registry/flows/domain/DomainCreateFlowTest.java b/javatests/google/registry/flows/domain/DomainCreateFlowTest.java index 00205428b..cf260dda2 100644 --- a/javatests/google/registry/flows/domain/DomainCreateFlowTest.java +++ b/javatests/google/registry/flows/domain/DomainCreateFlowTest.java @@ -21,6 +21,7 @@ import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.pricing.PricingEngineProxy.getPricesForDomainName; import static google.registry.testing.DatastoreHelper.assertBillingEvents; import static google.registry.testing.DatastoreHelper.createTld; +import static google.registry.testing.DatastoreHelper.createTlds; import static google.registry.testing.DatastoreHelper.deleteTld; import static google.registry.testing.DatastoreHelper.getHistoryEntries; import static google.registry.testing.DatastoreHelper.newContactResource; @@ -104,6 +105,7 @@ import google.registry.model.billing.BillingEvent.Reason; import google.registry.model.domain.DomainResource; import google.registry.model.domain.GracePeriod; import google.registry.model.domain.LrpToken; +import google.registry.model.domain.TestExtraLogicManager; import google.registry.model.domain.launch.ApplicationStatus; import google.registry.model.domain.launch.LaunchNotice; import google.registry.model.domain.rgp.GracePeriodStatus; @@ -134,7 +136,7 @@ public class DomainCreateFlowTest extends ResourceFlowTestCaseUSD update 1 - 11.00 + 0.00 diff --git a/javatests/google/registry/flows/domain/testdata/domain_check_fee_multiple_commands_response_v12.xml b/javatests/google/registry/flows/domain/testdata/domain_check_fee_multiple_commands_response_v12.xml index 110835458..0f105a47c 100644 --- a/javatests/google/registry/flows/domain/testdata/domain_check_fee_multiple_commands_response_v12.xml +++ b/javatests/google/registry/flows/domain/testdata/domain_check_fee_multiple_commands_response_v12.xml @@ -56,7 +56,7 @@ 1 - 11.00 + 0.00 diff --git a/javatests/google/registry/flows/domain/testdata/domain_check_fee_premium_response_v11_update.xml b/javatests/google/registry/flows/domain/testdata/domain_check_fee_premium_response_v11_update.xml index 6c99d8075..47572fef6 100644 --- a/javatests/google/registry/flows/domain/testdata/domain_check_fee_premium_response_v11_update.xml +++ b/javatests/google/registry/flows/domain/testdata/domain_check_fee_premium_response_v11_update.xml @@ -20,7 +20,7 @@ update USD 1 - 100.00 + 0.00 premium diff --git a/javatests/google/registry/flows/domain/testdata/domain_check_fee_premium_response_v12.xml b/javatests/google/registry/flows/domain/testdata/domain_check_fee_premium_response_v12.xml index c07f6ba64..eafbd5301 100644 --- a/javatests/google/registry/flows/domain/testdata/domain_check_fee_premium_response_v12.xml +++ b/javatests/google/registry/flows/domain/testdata/domain_check_fee_premium_response_v12.xml @@ -60,7 +60,7 @@ 1 - 100.00 + 0.00 premium diff --git a/javatests/google/registry/flows/domain/testdata/domain_create_flags.xml b/javatests/google/registry/flows/domain/testdata/domain_create_flags.xml new file mode 100644 index 000000000..ff08adda7 --- /dev/null +++ b/javatests/google/registry/flows/domain/testdata/domain_create_flags.xml @@ -0,0 +1,32 @@ + + + + + create-42.flags + 2 + + ns1.example.net + ns2.example.net + + jd1234 + sh8013 + sh8013 + + 2fooBAR + + + + + + USD + %FEE% + + + flag1 + flag2 + + + ABC-12345 + + diff --git a/javatests/google/registry/flows/domain/testdata/domain_delete_flags.xml b/javatests/google/registry/flows/domain/testdata/domain_delete_flags.xml new file mode 100644 index 000000000..fc32d3224 --- /dev/null +++ b/javatests/google/registry/flows/domain/testdata/domain_delete_flags.xml @@ -0,0 +1,11 @@ + + + + + example.flags + + + ABC-12345 + + diff --git a/javatests/google/registry/flows/domain/testdata/domain_renew_flags.xml b/javatests/google/registry/flows/domain/testdata/domain_renew_flags.xml new file mode 100644 index 000000000..2784dec1b --- /dev/null +++ b/javatests/google/registry/flows/domain/testdata/domain_renew_flags.xml @@ -0,0 +1,13 @@ + + + + + example.flags + 2000-04-03 + 5 + + + ABC-12345 + + diff --git a/javatests/google/registry/flows/domain/testdata/domain_transfer_request_flags.xml b/javatests/google/registry/flows/domain/testdata/domain_transfer_request_flags.xml new file mode 100644 index 000000000..cae3be4a7 --- /dev/null +++ b/javatests/google/registry/flows/domain/testdata/domain_transfer_request_flags.xml @@ -0,0 +1,27 @@ + + + + + example.flags + 1 + + 2fooBAR + + + + + + + flag1 + flag2 + + + flag3 + flag4 + + + + ABC-12345 + + diff --git a/javatests/google/registry/flows/domain/testdata/domain_update_restore_request_flags.xml b/javatests/google/registry/flows/domain/testdata/domain_update_restore_request_flags.xml new file mode 100644 index 000000000..0927462e4 --- /dev/null +++ b/javatests/google/registry/flows/domain/testdata/domain_update_restore_request_flags.xml @@ -0,0 +1,17 @@ + + + + + example.flags + + + + + + + + + ABC-12345 + + diff --git a/javatests/google/registry/model/domain/TestExtraLogicManager.java b/javatests/google/registry/model/domain/TestExtraLogicManager.java index 146162364..fbfa4fbdd 100644 --- a/javatests/google/registry/model/domain/TestExtraLogicManager.java +++ b/javatests/google/registry/model/domain/TestExtraLogicManager.java @@ -14,13 +14,24 @@ package google.registry.model.domain; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.Ascii; +import com.google.common.base.Joiner; import com.google.common.base.Splitter; import com.google.common.collect.Iterables; import google.registry.flows.EppException; -import google.registry.flows.EppException.UnimplementedExtensionException; import google.registry.flows.domain.RegistryExtraFlowLogic; +import google.registry.model.domain.fee.BaseFee; +import google.registry.model.domain.fee.BaseFee.FeeType; +import google.registry.model.domain.fee.Credit; +import google.registry.model.domain.fee.Fee; +import google.registry.model.domain.flags.FlagsCreateCommandExtension; +import google.registry.model.domain.flags.FlagsTransferCommandExtension; import google.registry.model.domain.flags.FlagsUpdateCommandExtension; import google.registry.model.eppinput.EppInput; +import java.math.BigDecimal; import java.util.List; import org.joda.time.DateTime; @@ -29,6 +40,8 @@ import org.joda.time.DateTime; */ public class TestExtraLogicManager implements RegistryExtraFlowLogic { + private String messageToThrow = null; + @Override public List getExtensionFlags( DomainResource domainResource, String clientIdentifier, DateTime asOfDate) { @@ -41,23 +54,166 @@ public class TestExtraLogicManager implements RegistryExtraFlowLogic { return components.subList(1, components.size()); } + BaseFee domainNameToFeeOrCredit(String domainName) { + // The second-level domain should be of the form "description-price", where description is the + // description string of the fee or credit, and price is the price (credit if negative, fee + // otherwise). To make sure this is a valid domain name, don't use any spaces, and limit prices + // to integers. Don't use a two-character description for credits, since it is illegal to have + // both the third and fourth characters of a domain name label be hyphens. + List components = + Splitter.on('-').limit(2).splitToList( + Iterables.getFirst(Splitter.on('.').split(domainName), "")); + checkArgument(components.size() == 2, "Domain name must be of the form description-price.tld"); + int price = Integer.parseInt(components.get(1)); + if (price < 0) { + return Credit.create( + new BigDecimal(price), FeeType.valueOf(Ascii.toUpperCase(components.get(0)))); + } else { + return Fee.create( + new BigDecimal(price), FeeType.valueOf(Ascii.toUpperCase(components.get(0)))); + } + } + + /** Computes the expected create cost, for use in fee challenges and the like. */ + @Override + public BaseFee getCreateFeeOrCredit( + String domainName, + String clientIdentifier, + DateTime asOfDate, + int years, + EppInput eppInput) throws EppException { + return domainNameToFeeOrCredit(domainName); + } + + /** + * Performs additional tasks required for a create command. Any changes should not be persisted to + * Datastore until commitAdditionalLogicChanges is called. + */ + @Override + public void performAdditionalDomainCreateLogic( + DomainResource domain, + String clientIdentifier, + DateTime asOfDate, + int years, + EppInput eppInput) throws EppException { + FlagsCreateCommandExtension flags = + eppInput.getSingleExtension(FlagsCreateCommandExtension.class); + if (flags == null) { + return; + } + messageToThrow = Joiner.on(',').join(flags.getFlags()); + } + + /** + * Performs additional tasks required for a delete command. Any changes should not be persisted to + * Datastore until commitAdditionalLogicChanges is called. + */ + @Override + public void performAdditionalDomainDeleteLogic( + DomainResource domainResource, + String clientIdentifier, + DateTime asOfDate, + EppInput eppInput) throws EppException { + messageToThrow = "deleted"; + } + + /** Computes the expected renewal cost, for use in fee challenges and the like. */ + @Override + public BaseFee getRenewFeeOrCredit( + DomainResource domain, + String clientIdentifier, + DateTime asOfDate, + int years, + EppInput eppInput) throws EppException { + return domainNameToFeeOrCredit(domain.getFullyQualifiedDomainName()); + } + + /** + * Performs additional tasks required for a renew command. Any changes should not be persisted + * to Datastore until commitAdditionalLogicChanges is called. + */ + @Override + public void performAdditionalDomainRenewLogic( + DomainResource domainResource, + String clientIdentifier, + DateTime asOfDate, + int years, + EppInput eppInput) throws EppException { + messageToThrow = "renewed"; + } + + /** + * Performs additional tasks required for a restore command. Any changes should not be persisted + * to Datastore until commitAdditionalLogicChanges is called. + */ + @Override + public void performAdditionalDomainRestoreLogic( + DomainResource domainResource, + String clientIdentifier, + DateTime asOfDate, + EppInput eppInput) throws EppException { + messageToThrow = "restored"; + } + + /** + * Performs additional tasks required for a transfer command. Any changes should not be persisted + * to Datastore until commitAdditionalLogicChanges is called. + */ + @Override + public void performAdditionalDomainTransferLogic( + DomainResource domainResource, + String clientIdentifier, + DateTime asOfDate, + int years, + EppInput eppInput) throws EppException { + FlagsTransferCommandExtension flags = + eppInput.getSingleExtension(FlagsTransferCommandExtension.class); + if (flags == null) { + return; + } + messageToThrow = + "add:" + + Joiner.on(',').join(flags.getAddFlags().getFlags()) + + ";remove:" + + Joiner.on(',').join(flags.getRemoveFlags().getFlags()); + } + + /** Computes the expected update cost, for use in fee challenges and the like. */ + @Override + public BaseFee getUpdateFeeOrCredit( + DomainResource domain, + String clientIdentifier, + DateTime asOfDate, + EppInput eppInput) throws EppException { + return domainNameToFeeOrCredit(domain.getFullyQualifiedDomainName()); + } + + /** + * Performs additional tasks required for an update command. Any changes should not be persisted + * to Datastore until commitAdditionalLogicChanges is called. + */ @Override public void performAdditionalDomainUpdateLogic( DomainResource domainResource, String clientIdentifier, DateTime asOfDate, EppInput eppInput) throws EppException { - FlagsUpdateCommandExtension updateFlags = + FlagsUpdateCommandExtension flags = eppInput.getSingleExtension(FlagsUpdateCommandExtension.class); - if (updateFlags == null) { + if (flags == null) { return; } - // Throw this exception as a signal to the test that we got this far. - throw new UnimplementedExtensionException(); + messageToThrow = + "add:" + + Joiner.on(',').join(flags.getAddFlags().getFlags()) + + ";remove:" + + Joiner.on(',').join(flags.getRemoveFlags().getFlags()); } @Override - public void commitAdditionalDomainUpdates() { - return; + public void commitAdditionalLogicChanges() { + checkNotNull(messageToThrow); + // Throw a specific exception as a signal to the test code that we made it through to here. + throw new IllegalArgumentException(messageToThrow); } } diff --git a/javatests/google/registry/pricing/TldSpecificLogicProxyTest.java b/javatests/google/registry/pricing/TldSpecificLogicProxyTest.java deleted file mode 100644 index aaa0cf8e6..000000000 --- a/javatests/google/registry/pricing/TldSpecificLogicProxyTest.java +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2016 The Domain Registry Authors. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package google.registry.pricing; - -import static com.google.common.truth.Truth.assertThat; -import static google.registry.testing.DatastoreHelper.createTld; -import static google.registry.testing.DatastoreHelper.persistResource; -import static google.registry.util.DateTimeUtils.START_OF_TIME; -import static org.joda.money.CurrencyUnit.USD; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSortedMap; -import google.registry.model.domain.fee.BaseFee.FeeType; -import google.registry.model.domain.fee.Fee; -import google.registry.model.ofy.Ofy; -import google.registry.model.registry.Registry; -import google.registry.testing.AppEngineRule; -import google.registry.testing.FakeClock; -import google.registry.testing.InjectRule; -import org.joda.money.Money; -import org.joda.time.DateTime; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class TldSpecificLogicProxyTest { - - @Rule - public final InjectRule inject = new InjectRule(); - - @Rule - public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build(); - - final FakeClock clock = new FakeClock(DateTime.parse("2010-01-01T10:00:00Z")); - - Money basicCreateCost; - - @Before - public void before() throws Exception { - inject.setStaticField(Ofy.class, "clock", clock); - createTld("tld"); - - createTld("eap"); - DateTime a = clock.nowUtc().minusDays(1); - DateTime b = clock.nowUtc().plusDays(1); - persistResource( - Registry.get("eap") - .asBuilder() - .setEapFeeSchedule( - ImmutableSortedMap.of( - START_OF_TIME, Money.of(USD, 0), - a, Money.of(USD, 100), - b, Money.of(USD, 50))) - .build()); - - basicCreateCost = - PricingEngineProxy.getPricesForDomainName("example.tld", clock.nowUtc()).getCreateCost(); - } - - @Test - public void testTldSpecificLogicEngine() { - TldSpecificLogicProxy.EppCommandOperations createPrice = - TldSpecificLogicProxy.getCreatePrice( - Registry.get("tld"), "example.tld", clock.nowUtc(), 1); - assertThat(createPrice.getTotalCost()).isEqualTo(basicCreateCost); - assertThat(createPrice.getFees()).hasSize(1); - } - - @Test - public void testTldSpecificLogicEngineEap() { - TldSpecificLogicProxy.EppCommandOperations createPrice = - TldSpecificLogicProxy.getCreatePrice( - Registry.get("eap"), "example.eap", clock.nowUtc(), 1); - assertThat(createPrice.getTotalCost()).isEqualTo(basicCreateCost.plus(Money.of(USD, 100))); - assertThat(createPrice.getCurrency()).isEqualTo(USD); - assertThat(createPrice.getFees().get(0)) - .isEqualTo(Fee.create(basicCreateCost.getAmount(), FeeType.CREATE)); - assertThat(createPrice.getFees().get(1)) - .isEqualTo( - Fee.create( - Money.of(USD, 100).getAmount(), FeeType.EAP, clock.nowUtc().plusDays(1))); - assertThat(createPrice.getFees()) - .isEqualTo( - ImmutableList.of( - Fee.create(basicCreateCost.getAmount(), FeeType.CREATE), - Fee.create( - Money.of(USD, 100).getAmount(), - FeeType.EAP, - clock.nowUtc().plusDays(1)))); - } -} From a63921350b7312e00a4b6eedbb384a68f7e91686 Mon Sep 17 00:00:00 2001 From: mountford Date: Thu, 8 Sep 2016 09:04:07 -0700 Subject: [PATCH 07/31] HistoryEntry for extra logic; update fee check While working on an implementation of TLD-specific logic, it was realized that the extra logic methods would need access to the flow's HistoryEntry, so that things like poll messages could be parented properly. Also, the update flow had not been fixed to perform the fee check. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=132561527 --- .../flows/domain/BaseDomainUpdateFlow.java | 9 +++++++ .../flows/domain/DomainCreateFlow.java | 3 ++- .../flows/domain/DomainDeleteFlow.java | 2 +- .../flows/domain/DomainFlowUtils.java | 8 +++++- .../flows/domain/DomainRenewFlow.java | 7 ++++- .../domain/DomainRestoreRequestFlow.java | 2 +- .../domain/DomainTransferRequestFlow.java | 7 ++++- .../flows/domain/DomainUpdateFlow.java | 23 +++++++++++++++- .../flows/domain/RegistryExtraFlowLogic.java | 19 ++++++++----- .../flows/domain/DomainUpdateFlowTest.java | 22 ++++++++++++++- .../domain_update_addremove_flags.xml | 2 +- .../domain_update_addremove_flags_fee.xml | 27 +++++++++++++++++++ .../model/domain/TestExtraLogicManager.java | 19 ++++++++----- 13 files changed, 129 insertions(+), 21 deletions(-) create mode 100644 javatests/google/registry/flows/domain/testdata/domain_update_addremove_flags_fee.xml diff --git a/java/google/registry/flows/domain/BaseDomainUpdateFlow.java b/java/google/registry/flows/domain/BaseDomainUpdateFlow.java index 4f2a7ec70..c6fa49374 100644 --- a/java/google/registry/flows/domain/BaseDomainUpdateFlow.java +++ b/java/google/registry/flows/domain/BaseDomainUpdateFlow.java @@ -26,6 +26,7 @@ import static google.registry.flows.domain.DomainFlowUtils.validateNoDuplicateCo import static google.registry.flows.domain.DomainFlowUtils.validateRegistrantAllowedOnTld; import static google.registry.flows.domain.DomainFlowUtils.validateRequiredContactsPresent; import static google.registry.flows.domain.DomainFlowUtils.verifyNotInPendingDelete; +import static google.registry.model.domain.fee.Fee.FEE_UPDATE_COMMAND_EXTENSIONS_IN_PREFERENCE_ORDER; import com.google.common.base.Optional; import com.google.common.collect.ImmutableSet; @@ -37,11 +38,13 @@ import google.registry.flows.ResourceUpdateFlow; import google.registry.model.domain.DomainBase; import google.registry.model.domain.DomainBase.Builder; import google.registry.model.domain.DomainCommand.Update; +import google.registry.model.domain.fee.FeeTransformCommandExtension; import google.registry.model.domain.secdns.DelegationSignerData; import google.registry.model.domain.secdns.SecDnsUpdateExtension; import google.registry.model.domain.secdns.SecDnsUpdateExtension.Add; import google.registry.model.domain.secdns.SecDnsUpdateExtension.Remove; import java.util.Set; +import javax.annotation.Nullable; /** * An EPP flow that updates a domain application or resource. @@ -52,10 +55,16 @@ import java.util.Set; public abstract class BaseDomainUpdateFlow> extends ResourceUpdateFlow { + @Nullable + protected FeeTransformCommandExtension feeUpdate; + protected Optional extraFlowLogic; @Override public final void initResourceCreateOrMutateFlow() throws EppException { + registerExtensions(FEE_UPDATE_COMMAND_EXTENSIONS_IN_PREFERENCE_ORDER); + feeUpdate = + eppInput.getFirstExtensionOfClasses(FEE_UPDATE_COMMAND_EXTENSIONS_IN_PREFERENCE_ORDER); command = cloneAndLinkReferences(command, now); initDomainUpdateFlow(); extraFlowLogic = RegistryExtraFlowLogicProxy.newInstanceForDomain(existingResource); diff --git a/java/google/registry/flows/domain/DomainCreateFlow.java b/java/google/registry/flows/domain/DomainCreateFlow.java index 705bef02f..c8925a975 100644 --- a/java/google/registry/flows/domain/DomainCreateFlow.java +++ b/java/google/registry/flows/domain/DomainCreateFlow.java @@ -195,7 +195,8 @@ public class DomainCreateFlow extends DomainCreateOrAllocateFlow { getClientId(), now, command.getPeriod().getValue(), - eppInput); + eppInput, + historyEntry); } } diff --git a/java/google/registry/flows/domain/DomainDeleteFlow.java b/java/google/registry/flows/domain/DomainDeleteFlow.java index 630c5abd0..ca77152c0 100644 --- a/java/google/registry/flows/domain/DomainDeleteFlow.java +++ b/java/google/registry/flows/domain/DomainDeleteFlow.java @@ -131,7 +131,7 @@ public class DomainDeleteFlow extends ResourceSyncDeleteFlowentities(explicitRenewEvent, newAutorenewEvent, newAutorenewPollMessage); diff --git a/java/google/registry/flows/domain/DomainRestoreRequestFlow.java b/java/google/registry/flows/domain/DomainRestoreRequestFlow.java index 353cb76e5..43fa6350a 100644 --- a/java/google/registry/flows/domain/DomainRestoreRequestFlow.java +++ b/java/google/registry/flows/domain/DomainRestoreRequestFlow.java @@ -162,7 +162,7 @@ public class DomainRestoreRequestFlow extends OwnedResourceMutateFlow - example.flags + update-13.flags diff --git a/javatests/google/registry/flows/domain/testdata/domain_update_addremove_flags_fee.xml b/javatests/google/registry/flows/domain/testdata/domain_update_addremove_flags_fee.xml new file mode 100644 index 000000000..f57e67c8f --- /dev/null +++ b/javatests/google/registry/flows/domain/testdata/domain_update_addremove_flags_fee.xml @@ -0,0 +1,27 @@ + + + + + update-13.flags + + + + + + flag1 + flag2 + + + flag3 + flag4 + + + + USD + %FEE% + + + ABC-12345 + + diff --git a/javatests/google/registry/model/domain/TestExtraLogicManager.java b/javatests/google/registry/model/domain/TestExtraLogicManager.java index fbfa4fbdd..674c1ede1 100644 --- a/javatests/google/registry/model/domain/TestExtraLogicManager.java +++ b/javatests/google/registry/model/domain/TestExtraLogicManager.java @@ -31,6 +31,7 @@ import google.registry.model.domain.flags.FlagsCreateCommandExtension; import google.registry.model.domain.flags.FlagsTransferCommandExtension; import google.registry.model.domain.flags.FlagsUpdateCommandExtension; import google.registry.model.eppinput.EppInput; +import google.registry.model.reporting.HistoryEntry; import java.math.BigDecimal; import java.util.List; import org.joda.time.DateTime; @@ -95,7 +96,8 @@ public class TestExtraLogicManager implements RegistryExtraFlowLogic { String clientIdentifier, DateTime asOfDate, int years, - EppInput eppInput) throws EppException { + EppInput eppInput, + HistoryEntry historyEntry) throws EppException { FlagsCreateCommandExtension flags = eppInput.getSingleExtension(FlagsCreateCommandExtension.class); if (flags == null) { @@ -113,7 +115,8 @@ public class TestExtraLogicManager implements RegistryExtraFlowLogic { DomainResource domainResource, String clientIdentifier, DateTime asOfDate, - EppInput eppInput) throws EppException { + EppInput eppInput, + HistoryEntry historyEntry) throws EppException { messageToThrow = "deleted"; } @@ -138,7 +141,8 @@ public class TestExtraLogicManager implements RegistryExtraFlowLogic { String clientIdentifier, DateTime asOfDate, int years, - EppInput eppInput) throws EppException { + EppInput eppInput, + HistoryEntry historyEntry) throws EppException { messageToThrow = "renewed"; } @@ -151,7 +155,8 @@ public class TestExtraLogicManager implements RegistryExtraFlowLogic { DomainResource domainResource, String clientIdentifier, DateTime asOfDate, - EppInput eppInput) throws EppException { + EppInput eppInput, + HistoryEntry historyEntry) throws EppException { messageToThrow = "restored"; } @@ -165,7 +170,8 @@ public class TestExtraLogicManager implements RegistryExtraFlowLogic { String clientIdentifier, DateTime asOfDate, int years, - EppInput eppInput) throws EppException { + EppInput eppInput, + HistoryEntry historyEntry) throws EppException { FlagsTransferCommandExtension flags = eppInput.getSingleExtension(FlagsTransferCommandExtension.class); if (flags == null) { @@ -197,7 +203,8 @@ public class TestExtraLogicManager implements RegistryExtraFlowLogic { DomainResource domainResource, String clientIdentifier, DateTime asOfDate, - EppInput eppInput) throws EppException { + EppInput eppInput, + HistoryEntry historyEntry) throws EppException { FlagsUpdateCommandExtension flags = eppInput.getSingleExtension(FlagsUpdateCommandExtension.class); if (flags == null) { From e478fd09fbe474979452711c3cba920e4d4c9a1b Mon Sep 17 00:00:00 2001 From: nickfelt Date: Thu, 8 Sep 2016 09:31:06 -0700 Subject: [PATCH 08/31] Remove straggling JarKeyring link and sort MOE-added imports Followups from [] ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=132564245 --- java/google/registry/module/backend/BackendComponent.java | 4 ++-- java/google/registry/module/frontend/FrontendComponent.java | 4 ++-- java/google/registry/module/tools/ToolsComponent.java | 4 ++-- java/google/registry/tools/RegistryToolComponent.java | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/java/google/registry/module/backend/BackendComponent.java b/java/google/registry/module/backend/BackendComponent.java index c971db980..44e1dd1e5 100644 --- a/java/google/registry/module/backend/BackendComponent.java +++ b/java/google/registry/module/backend/BackendComponent.java @@ -24,8 +24,8 @@ import google.registry.gcs.GcsServiceModule; import google.registry.groups.DirectoryModule; import google.registry.groups.GroupsModule; import google.registry.groups.GroupssettingsModule; -import google.registry.keyring.api.KeyModule; import google.registry.keyring.api.DummyKeyringModule; +import google.registry.keyring.api.KeyModule; import google.registry.monitoring.metrics.MetricReporter; import google.registry.monitoring.whitebox.StackdriverModule; import google.registry.rde.JSchModule; @@ -52,6 +52,7 @@ import javax.inject.Singleton; DatastoreServiceModule.class, DirectoryModule.class, DriveModule.class, + DummyKeyringModule.class, GcsServiceModule.class, GoogleCredentialModule.class, GroupsModule.class, @@ -68,7 +69,6 @@ import javax.inject.Singleton; UrlFetchTransportModule.class, UseAppIdentityCredentialForGoogleApisModule.class, VoidDnsWriterModule.class, - DummyKeyringModule.class, }) interface BackendComponent { BackendRequestComponent startRequest(RequestModule requestModule); diff --git a/java/google/registry/module/frontend/FrontendComponent.java b/java/google/registry/module/frontend/FrontendComponent.java index a4f5b94c2..f394a79ab 100644 --- a/java/google/registry/module/frontend/FrontendComponent.java +++ b/java/google/registry/module/frontend/FrontendComponent.java @@ -17,8 +17,8 @@ package google.registry.module.frontend; import dagger.Component; import google.registry.braintree.BraintreeModule; import google.registry.config.ConfigModule; -import google.registry.keyring.api.KeyModule; import google.registry.keyring.api.DummyKeyringModule; +import google.registry.keyring.api.KeyModule; import google.registry.monitoring.metrics.MetricReporter; import google.registry.monitoring.whitebox.StackdriverModule; import google.registry.request.Modules.AppIdentityCredentialModule; @@ -40,6 +40,7 @@ import javax.inject.Singleton; BraintreeModule.class, ConfigModule.class, ConsoleConfigModule.class, + DummyKeyringModule.class, FrontendMetricsModule.class, Jackson2Module.class, KeyModule.class, @@ -49,7 +50,6 @@ import javax.inject.Singleton; UrlFetchTransportModule.class, UseAppIdentityCredentialForGoogleApisModule.class, UserServiceModule.class, - DummyKeyringModule.class, }) interface FrontendComponent { FrontendRequestComponent startRequest(RequestModule requestModule); diff --git a/java/google/registry/module/tools/ToolsComponent.java b/java/google/registry/module/tools/ToolsComponent.java index 6a6c0753e..b1cdd3327 100644 --- a/java/google/registry/module/tools/ToolsComponent.java +++ b/java/google/registry/module/tools/ToolsComponent.java @@ -21,8 +21,8 @@ import google.registry.gcs.GcsServiceModule; import google.registry.groups.DirectoryModule; import google.registry.groups.GroupsModule; import google.registry.groups.GroupssettingsModule; -import google.registry.keyring.api.KeyModule; import google.registry.keyring.api.DummyKeyringModule; +import google.registry.keyring.api.KeyModule; import google.registry.request.Modules.AppIdentityCredentialModule; import google.registry.request.Modules.DatastoreServiceModule; import google.registry.request.Modules.GoogleCredentialModule; @@ -44,6 +44,7 @@ import javax.inject.Singleton; DatastoreServiceModule.class, DirectoryModule.class, DriveModule.class, + DummyKeyringModule.class, GcsServiceModule.class, GoogleCredentialModule.class, GroupsModule.class, @@ -55,7 +56,6 @@ import javax.inject.Singleton; UseAppIdentityCredentialForGoogleApisModule.class, SystemClockModule.class, SystemSleeperModule.class, - DummyKeyringModule.class, }) interface ToolsComponent { ToolsRequestComponent startRequest(RequestModule requestModule); diff --git a/java/google/registry/tools/RegistryToolComponent.java b/java/google/registry/tools/RegistryToolComponent.java index c5e4b1dbd..921e0369c 100644 --- a/java/google/registry/tools/RegistryToolComponent.java +++ b/java/google/registry/tools/RegistryToolComponent.java @@ -19,8 +19,8 @@ import google.registry.config.ConfigModule; import google.registry.dns.writer.VoidDnsWriterModule; import google.registry.dns.writer.clouddns.CloudDnsModule; import google.registry.dns.writer.dnsupdate.DnsUpdateWriterModule; -import google.registry.keyring.api.KeyModule; import google.registry.keyring.api.DummyKeyringModule; +import google.registry.keyring.api.KeyModule; import google.registry.request.Modules.DatastoreServiceModule; import google.registry.request.Modules.Jackson2Module; import google.registry.request.Modules.URLFetchServiceModule; @@ -38,13 +38,13 @@ import google.registry.util.SystemClock.SystemClockModule; DatastoreServiceModule.class, CloudDnsModule.class, DnsUpdateWriterModule.class, + DummyKeyringModule.class, Jackson2Module.class, KeyModule.class, RegistryToolModule.class, SystemClockModule.class, URLFetchServiceModule.class, VoidDnsWriterModule.class, - DummyKeyringModule.class, } ) interface RegistryToolComponent { From 36c6d59feeec596268a37c50c410f2fc19172496 Mon Sep 17 00:00:00 2001 From: nickfelt Date: Thu, 8 Sep 2016 12:33:00 -0700 Subject: [PATCH 09/31] Avoid mocking in tests for EppMetric Followup to [] Mocking shouldn't be used when you can use the real implementation just as easily (and more robustly) - in particular, you almost never want to mock a value type. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=132586826 --- .../registry/flows/EppControllerTest.java | 35 ++++++++++--------- .../registry/flows/EppTestComponent.java | 2 +- .../google/registry/flows/FlowRunnerTest.java | 13 +++---- .../session/LoginFlowViaConsoleTest.java | 5 ++- 4 files changed, 26 insertions(+), 29 deletions(-) diff --git a/javatests/google/registry/flows/EppControllerTest.java b/javatests/google/registry/flows/EppControllerTest.java index cf8a8ea05..b99a4c43a 100644 --- a/javatests/google/registry/flows/EppControllerTest.java +++ b/javatests/google/registry/flows/EppControllerTest.java @@ -14,6 +14,7 @@ package google.registry.flows; +import static com.google.common.truth.Truth.assertThat; import static google.registry.flows.EppXmlTransformer.marshal; import static google.registry.testing.TestDataHelper.loadFileWithSubstitutions; import static java.nio.charset.StandardCharsets.UTF_8; @@ -37,6 +38,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Matchers; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; @@ -50,8 +52,6 @@ public class EppControllerTest extends ShardableTestCase { @Mock SessionMetadata sessionMetadata; @Mock TransportCredentials transportCredentials; - @Mock EppMetric.Builder eppMetricBuilder; - @Mock EppMetric eppMetric; @Mock BigQueryMetricsEnqueuer metricsEnqueuer; @Mock FlowComponent.Builder flowComponentBuilder; @Mock FlowComponent flowComponent; @@ -64,7 +64,7 @@ public class EppControllerTest extends ShardableTestCase { @Before public void setUp() throws Exception { - when(sessionMetadata.getClientId()).thenReturn("foo"); + when(sessionMetadata.getClientId()).thenReturn("some-client"); when(flowComponentBuilder.flowModule(Matchers.any())) .thenReturn(flowComponentBuilder); when(flowComponentBuilder.build()).thenReturn(flowComponent); @@ -74,10 +74,9 @@ public class EppControllerTest extends ShardableTestCase { when(eppOutput.getResponse()).thenReturn(eppResponse); when(eppResponse.getResult()).thenReturn(result); when(result.getCode()).thenReturn(Code.SuccessWithNoMessages); - when(eppMetricBuilder.build()).thenReturn(eppMetric); eppController = new EppController(); - eppController.metric = eppMetricBuilder; + eppController.metric = new EppMetric.Builder(); eppController.bigQueryMetricsEnqueuer = metricsEnqueuer; eppController.clock = new FakeClock(); eppController.flowComponentBuilder = flowComponentBuilder; @@ -93,6 +92,7 @@ public class EppControllerTest extends ShardableTestCase { @Test public void testHandleEppCommand_unmarshallableData_exportsMetric() { + ArgumentCaptor metricCaptor = ArgumentCaptor.forClass(EppMetric.class); eppController.handleEppCommand( sessionMetadata, transportCredentials, @@ -101,15 +101,16 @@ public class EppControllerTest extends ShardableTestCase { false, new byte[0]); - verify(eppMetricBuilder).setClientId("foo"); - verify(eppMetricBuilder).setPrivilegeLevel("NORMAL"); - verify(eppMetricBuilder).setStatus(Code.SyntaxError); - verify(eppMetricBuilder).build(); - verify(metricsEnqueuer).export(eppMetric); + verify(metricsEnqueuer).export(metricCaptor.capture()); + EppMetric metric = metricCaptor.getValue(); + assertThat(metric.getClientId()).hasValue("some-client"); + assertThat(metric.getPrivilegeLevel()).hasValue("NORMAL"); + assertThat(metric.getStatus()).hasValue(Code.SyntaxError); } @Test public void testHandleEppCommand_regularEppCommand_exportsMetric() { + ArgumentCaptor metricCaptor = ArgumentCaptor.forClass(EppMetric.class); String domainCreateXml = loadFileWithSubstitutions( getClass(), "domain_create_prettyprinted.xml", ImmutableMap.of()); @@ -121,12 +122,12 @@ public class EppControllerTest extends ShardableTestCase { true, domainCreateXml.getBytes(UTF_8)); - verify(eppMetricBuilder).setClientId("foo"); - verify(eppMetricBuilder).setPrivilegeLevel("SUPERUSER"); - verify(eppMetricBuilder).setStatus(Code.SuccessWithNoMessages); - verify(eppMetricBuilder).setCommandName("Create"); - verify(eppMetricBuilder).setEppTarget("example.tld"); - verify(eppMetricBuilder).build(); - verify(metricsEnqueuer).export(eppMetric); + verify(metricsEnqueuer).export(metricCaptor.capture()); + EppMetric metric = metricCaptor.getValue(); + assertThat(metric.getClientId()).hasValue("some-client"); + assertThat(metric.getPrivilegeLevel()).hasValue("SUPERUSER"); + assertThat(metric.getStatus()).hasValue(Code.SuccessWithNoMessages); + assertThat(metric.getCommandName()).hasValue("Create"); + assertThat(metric.getEppTarget()).hasValue("example.tld"); } } diff --git a/javatests/google/registry/flows/EppTestComponent.java b/javatests/google/registry/flows/EppTestComponent.java index 5b8cd1698..af3cf6398 100644 --- a/javatests/google/registry/flows/EppTestComponent.java +++ b/javatests/google/registry/flows/EppTestComponent.java @@ -48,7 +48,7 @@ interface EppTestComponent { FakesAndMocksModule(FakeClock clock) { this.clock = clock; - this.metrics = mock(EppMetric.Builder.class); + this.metrics = new EppMetric.Builder(); this.modulesService = mock(ModulesService.class); this.metricsEnqueuer = mock(BigQueryMetricsEnqueuer.class); } diff --git a/javatests/google/registry/flows/FlowRunnerTest.java b/javatests/google/registry/flows/FlowRunnerTest.java index eeadabc7c..7141ea34b 100644 --- a/javatests/google/registry/flows/FlowRunnerTest.java +++ b/javatests/google/registry/flows/FlowRunnerTest.java @@ -19,7 +19,6 @@ import static com.google.common.truth.Truth.assertThat; import static google.registry.testing.TestDataHelper.loadFileWithSubstitutions; import static java.nio.charset.StandardCharsets.UTF_8; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.google.appengine.api.users.User; @@ -91,7 +90,7 @@ public class FlowRunnerTest extends ShardableTestCase { flowRunner.isDryRun = false; flowRunner.isSuperuser = false; flowRunner.isTransactional = false; - flowRunner.metric = mock(EppMetric.Builder.class); + flowRunner.metric = new EppMetric.Builder(); flowRunner.sessionMetadata = new StatelessRequestSessionMetadata("TheRegistrar", ImmutableSet.of()); flowRunner.trid = Trid.create("client-123", "server-456"); @@ -110,18 +109,16 @@ public class FlowRunnerTest extends ShardableTestCase { } @Test - public void testRun_notIsTransactional_callsMetricIncrementAttempts() throws Exception { + public void testRun_notIsTransactional_incrementsMetricAttempts() throws Exception { flowRunner.run(); - - verify(flowRunner.metric).incrementAttempts(); + assertThat(flowRunner.metric.build().getAttempts()).isEqualTo(1); } @Test - public void testRun_isTransactional_callsMetricIncrementAttempts() throws Exception { + public void testRun_isTransactional_incrementsMetricAttempts() throws Exception { flowRunner.isTransactional = true; flowRunner.run(); - - verify(flowRunner.metric).incrementAttempts(); + assertThat(flowRunner.metric.build().getAttempts()).isEqualTo(1); } @Test diff --git a/javatests/google/registry/flows/session/LoginFlowViaConsoleTest.java b/javatests/google/registry/flows/session/LoginFlowViaConsoleTest.java index b2985e642..8db4726d3 100644 --- a/javatests/google/registry/flows/session/LoginFlowViaConsoleTest.java +++ b/javatests/google/registry/flows/session/LoginFlowViaConsoleTest.java @@ -87,13 +87,12 @@ public class LoginFlowViaConsoleTest extends LoginFlowTestCase { } Environment login(final String name, final String authDomain, final String gaeUserId) { + oldEnv = ApiProxy.getCurrentEnvironment(); // This envAttr thing is the only way to set userId. // see https://code.google.com/p/googleappengine/issues/detail?id=3579 - final HashMap envAttr = new HashMap<>(); + final HashMap envAttr = new HashMap<>(oldEnv.getAttributes()); envAttr.put("com.google.appengine.api.users.UserService.user_id_key", gaeUserId); - // And then.. this. - oldEnv = ApiProxy.getCurrentEnvironment(); final Environment e = oldEnv; ApiProxy.setEnvironmentForCurrentThread(new Environment() { @Override From aa4ca42cdd1f3e3a5487af240959ed234271400b Mon Sep 17 00:00:00 2001 From: shikhman Date: Thu, 8 Sep 2016 13:12:49 -0700 Subject: [PATCH 10/31] Add EPP metrics to flows ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=132591518 --- java/google/registry/flows/BUILD | 1 + java/google/registry/flows/EppController.java | 20 +++--- java/google/registry/flows/EppMetrics.java | 72 +++++++++++++++++++ .../registry/flows/EppControllerTest.java | 4 +- 4 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 java/google/registry/flows/EppMetrics.java diff --git a/java/google/registry/flows/BUILD b/java/google/registry/flows/BUILD index 4ce8e5d2c..0ddfb72dd 100644 --- a/java/google/registry/flows/BUILD +++ b/java/google/registry/flows/BUILD @@ -44,6 +44,7 @@ java_library( "//java/google/registry/mapreduce", "//java/google/registry/mapreduce/inputs", "//java/google/registry/model", + "//java/google/registry/monitoring/metrics", "//java/google/registry/monitoring/whitebox", "//java/google/registry/pricing", "//java/google/registry/request", diff --git a/java/google/registry/flows/EppController.java b/java/google/registry/flows/EppController.java index c19a4f2cd..e49e52406 100644 --- a/java/google/registry/flows/EppController.java +++ b/java/google/registry/flows/EppController.java @@ -42,7 +42,8 @@ public final class EppController { @Inject Clock clock; @Inject FlowComponent.Builder flowComponentBuilder; - @Inject EppMetric.Builder metric; + @Inject EppMetric.Builder metricBuilder; + @Inject EppMetrics eppMetrics; @Inject BigQueryMetricsEnqueuer bigQueryMetricsEnqueuer; @Inject EppController() {} @@ -54,20 +55,20 @@ public final class EppController { boolean isDryRun, boolean isSuperuser, byte[] inputXmlBytes) { - metric.setClientId(sessionMetadata.getClientId()); - metric.setPrivilegeLevel(isSuperuser ? "SUPERUSER" : "NORMAL"); + metricBuilder.setClientId(sessionMetadata.getClientId()); + metricBuilder.setPrivilegeLevel(isSuperuser ? "SUPERUSER" : "NORMAL"); try { EppInput eppInput; try { eppInput = unmarshal(EppInput.class, inputXmlBytes); } catch (EppException e) { // Send the client an error message, with no clTRID since we couldn't unmarshal it. - metric.setStatus(e.getResult().getCode()); + metricBuilder.setStatus(e.getResult().getCode()); return getErrorResponse(clock, e.getResult(), Trid.create(null)); } - metric.setCommandName(eppInput.getCommandName()); + metricBuilder.setCommandName(eppInput.getCommandName()); if (!eppInput.getTargetIds().isEmpty()) { - metric.setEppTarget(Joiner.on(',').join(eppInput.getTargetIds())); + metricBuilder.setEppTarget(Joiner.on(',').join(eppInput.getTargetIds())); } EppOutput output = runFlowConvertEppErrors(flowComponentBuilder .flowModule(new FlowModule.Builder() @@ -81,11 +82,14 @@ public final class EppController { .build()) .build()); if (output.isResponse()) { - metric.setStatus(output.getResponse().getResult().getCode()); + metricBuilder.setStatus(output.getResponse().getResult().getCode()); } return output; } finally { - bigQueryMetricsEnqueuer.export(metric.build()); + EppMetric metric = metricBuilder.build(); + bigQueryMetricsEnqueuer.export(metric); + eppMetrics.incrementEppRequests(metric); + eppMetrics.recordProcessingTime(metric); } } diff --git a/java/google/registry/flows/EppMetrics.java b/java/google/registry/flows/EppMetrics.java new file mode 100644 index 000000000..c0cad474b --- /dev/null +++ b/java/google/registry/flows/EppMetrics.java @@ -0,0 +1,72 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows; + +import com.google.common.collect.ImmutableSet; +import google.registry.monitoring.metrics.EventMetric; +import google.registry.monitoring.metrics.IncrementableMetric; +import google.registry.monitoring.metrics.LabelDescriptor; +import google.registry.monitoring.metrics.MetricRegistryImpl; +import google.registry.monitoring.whitebox.EppMetric; +import javax.inject.Inject; + +/** EPP Instrumentation. */ +public class EppMetrics { + + private static final ImmutableSet LABEL_DESCRIPTORS = + ImmutableSet.of( + LabelDescriptor.create("command", "The name of the command."), + LabelDescriptor.create("client_id", "The name of the client."), + LabelDescriptor.create("status", "The return status of the command.")); + + private static final IncrementableMetric eppRequests = + MetricRegistryImpl.getDefault() + .newIncrementableMetric( + "/epp/requests", "Count of EPP Requests", "count", LABEL_DESCRIPTORS); + + private static final EventMetric processingTime = + MetricRegistryImpl.getDefault() + .newEventMetric( + "/epp/processing_time", + "EPP Processing Time", + "milliseconds", + LABEL_DESCRIPTORS, + EventMetric.DEFAULT_FITTER); + + @Inject + public EppMetrics() {} + + /** + * Increment a counter which tracks EPP requests. + * + * @see EppController + * @see FlowRunner + */ + public void incrementEppRequests(EppMetric metric) { + eppRequests.increment( + metric.getCommandName().or(""), + metric.getClientId().or(""), + metric.getStatus().isPresent() ? metric.getStatus().toString() : ""); + } + + /** Record the server-side processing time for an EPP request. */ + public void recordProcessingTime(EppMetric metric) { + processingTime.record( + metric.getEndTimestamp().getMillis() - metric.getStartTimestamp().getMillis(), + metric.getCommandName().or(""), + metric.getClientId().or(""), + metric.getStatus().isPresent() ? metric.getStatus().toString() : ""); + } +} diff --git a/javatests/google/registry/flows/EppControllerTest.java b/javatests/google/registry/flows/EppControllerTest.java index b99a4c43a..3f08decb1 100644 --- a/javatests/google/registry/flows/EppControllerTest.java +++ b/javatests/google/registry/flows/EppControllerTest.java @@ -52,6 +52,7 @@ public class EppControllerTest extends ShardableTestCase { @Mock SessionMetadata sessionMetadata; @Mock TransportCredentials transportCredentials; + @Mock EppMetrics eppMetrics; @Mock BigQueryMetricsEnqueuer metricsEnqueuer; @Mock FlowComponent.Builder flowComponentBuilder; @Mock FlowComponent flowComponent; @@ -76,10 +77,11 @@ public class EppControllerTest extends ShardableTestCase { when(result.getCode()).thenReturn(Code.SuccessWithNoMessages); eppController = new EppController(); - eppController.metric = new EppMetric.Builder(); + eppController.metricBuilder = new EppMetric.Builder(); eppController.bigQueryMetricsEnqueuer = metricsEnqueuer; eppController.clock = new FakeClock(); eppController.flowComponentBuilder = flowComponentBuilder; + eppController.eppMetrics = eppMetrics; } @Test From cb55ab4f5f0130203faf1eb4ceadab2a41401598 Mon Sep 17 00:00:00 2001 From: nickfelt Date: Fri, 9 Sep 2016 08:09:03 -0700 Subject: [PATCH 11/31] Convert straggler GAE modules to basic-scaling This fixes the proximate cause of b/31380927 and makes alpha frontend usable again. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=132675121 --- .../env/alpha/default/WEB-INF/appengine-web.xml | 13 +++++-------- .../env/crash/default/WEB-INF/appengine-web.xml | 13 +++++-------- .../env/local/default/WEB-INF/appengine-web.xml | 13 +++++-------- 3 files changed, 15 insertions(+), 24 deletions(-) diff --git a/java/google/registry/env/alpha/default/WEB-INF/appengine-web.xml b/java/google/registry/env/alpha/default/WEB-INF/appengine-web.xml index 625375fa2..7c5654228 100644 --- a/java/google/registry/env/alpha/default/WEB-INF/appengine-web.xml +++ b/java/google/registry/env/alpha/default/WEB-INF/appengine-web.xml @@ -6,14 +6,11 @@ default true true - F4_1G - - 0 - automatic - automatic - 100ms - 10 - + B4_1G + + 10 + 10m + default true true - F4_1G - - 0 - automatic - automatic - 100ms - 10 - + B4_1G + + 10 + 10m + default true true - F4_1G - - 1 - automatic - automatic - 100ms - 10 - + B4_1G + + 10 + 10m + From bd887e857eab1cd87fcfc3f6c76099910cff7f22 Mon Sep 17 00:00:00 2001 From: shikhman Date: Fri, 9 Sep 2016 10:28:07 -0700 Subject: [PATCH 12/31] Fix incorrect field name in EppMetric ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=132688882 --- .../monitoring/whitebox/EppMetric.java | 4 +- .../monitoring/whitebox/EppMetricTest.java | 83 +++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 javatests/google/registry/monitoring/whitebox/EppMetricTest.java diff --git a/java/google/registry/monitoring/whitebox/EppMetric.java b/java/google/registry/monitoring/whitebox/EppMetric.java index 7ca133f3f..6e72d48c8 100644 --- a/java/google/registry/monitoring/whitebox/EppMetric.java +++ b/java/google/registry/monitoring/whitebox/EppMetric.java @@ -123,7 +123,9 @@ public abstract class EppMetric implements BigQueryMetric { addOptional("clientId", getClientId(), map); addOptional("privilegeLevel", getPrivilegeLevel(), map); addOptional("eppTarget", getEppTarget(), map); - addOptional("status", getStatus(), map); + if (getStatus().isPresent()) { + map.put("eppStatus", Integer.toString(getStatus().get().code)); + } return map.build(); } diff --git a/javatests/google/registry/monitoring/whitebox/EppMetricTest.java b/javatests/google/registry/monitoring/whitebox/EppMetricTest.java new file mode 100644 index 000000000..69e2a1494 --- /dev/null +++ b/javatests/google/registry/monitoring/whitebox/EppMetricTest.java @@ -0,0 +1,83 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.monitoring.whitebox; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.api.services.bigquery.model.TableFieldSchema; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import google.registry.model.eppoutput.Result.Code; +import google.registry.testing.AppEngineRule; +import org.joda.time.DateTime; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link EppMetric}. */ +@RunWith(JUnit4.class) +public class EppMetricTest { + + @Rule public final AppEngineRule appEngine = AppEngineRule.builder().build(); + + @Test + public void testGetBigQueryRowEncoding_encodesCorrectly() throws Exception { + EppMetric metric = + new EppMetric.Builder(new DateTime(1337)) + .setEppTarget("target") + .setPrivilegeLevel("level") + .setCommandName("command") + .setClientId("client") + .setStatus(Code.CommandUseError) + .incrementAttempts() + .build(new DateTime(1338)); + + // The request_id is randomly generated and hard to mock without a lot of supporting code + // so we just use the tested metric's request_id verbatim. + assertThat(metric.getBigQueryRowEncoding()) + .containsExactlyEntriesIn( + new ImmutableMap.Builder() + .put("requestId", metric.getRequestId()) + .put("startTime", "1.337000") + .put("endTime", "1.338000") + .put("commandName", "command") + .put("clientId", "client") + .put("privilegeLevel", "level") + .put("eppTarget", "target") + .put("eppStatus", "2002") + .put("attempts", "1") + .build()); + } + + @Test + public void testGetBigQueryRowEncoding_hasAllSchemaFields() throws Exception { + EppMetric metric = + new EppMetric.Builder(new DateTime(1337)) + .setEppTarget("target") + .setPrivilegeLevel("level") + .setCommandName("command") + .setClientId("client") + .setStatus(Code.CommandUseError) + .incrementAttempts() + .build(new DateTime(1338)); + ImmutableSet.Builder schemaFieldNames = new ImmutableSet.Builder<>(); + for (TableFieldSchema schemaField : metric.getSchemaFields()) { + schemaFieldNames.add(schemaField.getName()); + } + + assertThat(metric.getBigQueryRowEncoding().keySet()).isEqualTo(schemaFieldNames.build()); + } +} From ceb5c2117e7309c1153d2d7fd6268b4e19cad4ff Mon Sep 17 00:00:00 2001 From: nickfelt Date: Fri, 9 Sep 2016 11:26:47 -0700 Subject: [PATCH 13/31] Decouple GaeUserCredentials from UserService and simplify tests This disentangles GaeUserCredentials and UserService, which lets us remove a bunch of hacky and brittle code from LoginFlowViaConsoleTest. Previously, GaeUserCredentials was constructed for a user, but then was still directly calling UserService to check if the user was an admin. UserService can be adjusted in tests (via AppEngineRule / LocalServiceTestHelper) but it's a pain, especially to do dynamically within a single test file. The hacky code in LoginFlowViaConsoleTest was working around that restriction. With this CL, you can pass into GaeUserCredentials whether the user is an admin or not (for testing) or construct one directly from a UserService object (for production, and for convenience in tests using an AppEngineRule user). Note that I also changed EppConsoleAction to @Inject UserService. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=132696391 --- .../registry/flows/EppConsoleAction.java | 6 +- .../registry/flows/GaeUserCredentials.java | 42 ++++++- .../registry/flows/EppConsoleActionTest.java | 5 +- .../registry/flows/EppLoginAdminUserTest.java | 2 +- .../registry/flows/EppLoginUserTest.java | 2 +- .../google/registry/flows/FlowRunnerTest.java | 5 +- .../session/LoginFlowViaConsoleTest.java | 110 +++--------------- 7 files changed, 61 insertions(+), 111 deletions(-) diff --git a/java/google/registry/flows/EppConsoleAction.java b/java/google/registry/flows/EppConsoleAction.java index 6e0061d31..8bffa4be9 100644 --- a/java/google/registry/flows/EppConsoleAction.java +++ b/java/google/registry/flows/EppConsoleAction.java @@ -14,8 +14,7 @@ package google.registry.flows; -import static com.google.appengine.api.users.UserServiceFactory.getUserService; - +import com.google.appengine.api.users.UserService; import google.registry.request.Action; import google.registry.request.Action.Method; import google.registry.request.Payload; @@ -35,13 +34,14 @@ public class EppConsoleAction implements Runnable { @Inject @Payload byte[] inputXmlBytes; @Inject HttpSession session; @Inject EppRequestHandler eppRequestHandler; + @Inject UserService userService; @Inject EppConsoleAction() {} @Override public void run() { eppRequestHandler.executeEpp( new HttpSessionMetadata(session), - new GaeUserCredentials(getUserService().getCurrentUser()), + GaeUserCredentials.forCurrentUser(userService), EppRequestSource.CONSOLE, false, // This endpoint is never a dry run. false, // This endpoint is never a superuser. diff --git a/java/google/registry/flows/GaeUserCredentials.java b/java/google/registry/flows/GaeUserCredentials.java index 2d4d91345..62908d59e 100644 --- a/java/google/registry/flows/GaeUserCredentials.java +++ b/java/google/registry/flows/GaeUserCredentials.java @@ -14,11 +14,12 @@ package google.registry.flows; -import static com.google.appengine.api.users.UserServiceFactory.getUserService; import static com.google.common.base.MoreObjects.toStringHelper; import static com.google.common.base.Strings.nullToEmpty; +import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; import com.google.appengine.api.users.User; +import com.google.appengine.api.users.UserService; import com.google.common.annotations.VisibleForTesting; import google.registry.flows.EppException.AuthenticationErrorException; import google.registry.model.registrar.Registrar; @@ -28,11 +29,41 @@ import javax.annotation.Nullable; /** Credentials provided by {@link com.google.appengine.api.users.UserService}. */ public class GaeUserCredentials implements TransportCredentials { - final User gaeUser; + private final User gaeUser; + private final Boolean isAdmin; + + /** + * Create an instance for the current user, as determined by {@code UserService}. + * + *

Note that the current user may be null (i.e. there is no logged in user). + */ + public static GaeUserCredentials forCurrentUser(UserService userService) { + User user = userService.getCurrentUser(); + return new GaeUserCredentials(user, user != null ? userService.isUserAdmin() : null); + } + + /** Create an instance that represents an explicit user (for testing purposes). */ + @VisibleForTesting + public static GaeUserCredentials forTestingUser(User gaeUser, Boolean isAdmin) { + checkArgumentNotNull(gaeUser); + checkArgumentNotNull(isAdmin); + return new GaeUserCredentials(gaeUser, isAdmin); + } + + /** Create an instance that represents a non-logged in user (for testing purposes). */ + @VisibleForTesting + public static GaeUserCredentials forLoggedOutUser() { + return new GaeUserCredentials(null, null); + } + + private GaeUserCredentials(@Nullable User gaeUser, @Nullable Boolean isAdmin) { + this.gaeUser = gaeUser; + this.isAdmin = isAdmin; + } @VisibleForTesting - public GaeUserCredentials(@Nullable User gaeUser) { - this.gaeUser = gaeUser; + User getUser() { + return gaeUser; } @Override @@ -42,7 +73,7 @@ public class GaeUserCredentials implements TransportCredentials { throw new UserNotLoggedInException(); } // Allow admins to act as any registrar. - if (getUserService().isUserAdmin()) { + if (Boolean.TRUE.equals(isAdmin)) { return; } // Check Registrar's contacts to see if any are associated with this gaeUserId. @@ -59,6 +90,7 @@ public class GaeUserCredentials implements TransportCredentials { public String toString() { return toStringHelper(getClass()) .add("gaeUser", gaeUser) + .add("isAdmin", isAdmin) .toString(); } diff --git a/javatests/google/registry/flows/EppConsoleActionTest.java b/javatests/google/registry/flows/EppConsoleActionTest.java index d03892689..daf937433 100644 --- a/javatests/google/registry/flows/EppConsoleActionTest.java +++ b/javatests/google/registry/flows/EppConsoleActionTest.java @@ -14,7 +14,7 @@ package google.registry.flows; - +import static com.google.appengine.api.users.UserServiceFactory.getUserService; import static com.google.common.truth.Truth.assertThat; import static java.nio.charset.StandardCharsets.UTF_8; import static org.mockito.Matchers.eq; @@ -49,6 +49,7 @@ public class EppConsoleActionTest extends ShardableTestCase { action.session = new FakeHttpSession(); action.session.setAttribute("CLIENT_ID", "ClientIdentifier"); action.eppRequestHandler = mock(EppRequestHandler.class); + action.userService = getUserService(); action.run(); ArgumentCaptor credentialsCaptor = ArgumentCaptor.forClass(TransportCredentials.class); @@ -60,7 +61,7 @@ public class EppConsoleActionTest extends ShardableTestCase { eq(false), eq(false), eq(INPUT_XML_BYTES)); - assertThat(((GaeUserCredentials) credentialsCaptor.getValue()).gaeUser.getEmail()) + assertThat(((GaeUserCredentials) credentialsCaptor.getValue()).getUser().getEmail()) .isEqualTo("person@example.com"); assertThat(metadataCaptor.getValue().getClientId()).isEqualTo("ClientIdentifier"); } diff --git a/javatests/google/registry/flows/EppLoginAdminUserTest.java b/javatests/google/registry/flows/EppLoginAdminUserTest.java index b7bc34a41..9ca703028 100644 --- a/javatests/google/registry/flows/EppLoginAdminUserTest.java +++ b/javatests/google/registry/flows/EppLoginAdminUserTest.java @@ -36,7 +36,7 @@ public class EppLoginAdminUserTest extends EppTestCase { @Before public void initTransportCredentials() { - setTransportCredentials(new GaeUserCredentials(getUserService().getCurrentUser())); + setTransportCredentials(GaeUserCredentials.forCurrentUser(getUserService())); } @Test diff --git a/javatests/google/registry/flows/EppLoginUserTest.java b/javatests/google/registry/flows/EppLoginUserTest.java index c10a0043b..affa90d16 100644 --- a/javatests/google/registry/flows/EppLoginUserTest.java +++ b/javatests/google/registry/flows/EppLoginUserTest.java @@ -48,7 +48,7 @@ public class EppLoginUserTest extends EppTestCase { .setGaeUserId(user.getUserId()) .setTypes(ImmutableSet.of(RegistrarContact.Type.ADMIN)) .build()); - setTransportCredentials(new GaeUserCredentials(user)); + setTransportCredentials(GaeUserCredentials.forCurrentUser(getUserService())); } @Test diff --git a/javatests/google/registry/flows/FlowRunnerTest.java b/javatests/google/registry/flows/FlowRunnerTest.java index 7141ea34b..0caf5a94e 100644 --- a/javatests/google/registry/flows/FlowRunnerTest.java +++ b/javatests/google/registry/flows/FlowRunnerTest.java @@ -178,10 +178,11 @@ public class FlowRunnerTest extends ShardableTestCase { @Test public void testRun_legacyLoggingStatement_gaeUserCredentials() throws Exception { - flowRunner.credentials = new GaeUserCredentials(new User("user@example.com", "authDomain")); + flowRunner.credentials = + GaeUserCredentials.forTestingUser(new User("user@example.com", "authDomain"), false); flowRunner.run(); assertThat(Splitter.on("\n\t").split(findLogMessageByPrefix(handler, "EPP Command\n\t"))) - .contains("GaeUserCredentials{gaeUser=user@example.com}"); + .contains("GaeUserCredentials{gaeUser=user@example.com, isAdmin=false}"); } @Test diff --git a/javatests/google/registry/flows/session/LoginFlowViaConsoleTest.java b/javatests/google/registry/flows/session/LoginFlowViaConsoleTest.java index 8db4726d3..b3c97a4a1 100644 --- a/javatests/google/registry/flows/session/LoginFlowViaConsoleTest.java +++ b/javatests/google/registry/flows/session/LoginFlowViaConsoleTest.java @@ -15,19 +15,15 @@ package google.registry.flows.session; -import static com.google.appengine.api.users.UserServiceFactory.getUserService; import static google.registry.testing.DatastoreHelper.persistResource; -import com.google.apphosting.api.ApiProxy; -import com.google.apphosting.api.ApiProxy.Environment; +import com.google.appengine.api.users.User; import com.google.common.collect.ImmutableSet; import google.registry.flows.GaeUserCredentials; import google.registry.flows.GaeUserCredentials.BadGaeUserIdException; import google.registry.flows.GaeUserCredentials.UserNotLoggedInException; import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarContact; -import java.util.HashMap; -import java.util.Map; import org.junit.Test; /** @@ -39,123 +35,43 @@ public class LoginFlowViaConsoleTest extends LoginFlowTestCase { private static final String GAE_USER_ID1 = "12345"; private static final String GAE_USER_ID2 = "54321"; - Environment oldEnv = null; - @Test public void testSuccess_withLoginAndLinkedAccount() throws Exception { persistLinkedAccount("person@example.com", GAE_USER_ID1); - login("person", "example.com", GAE_USER_ID1); - try { - doSuccessfulTest("login_valid.xml"); - } finally { - ApiProxy.setEnvironmentForCurrentThread(oldEnv); - } + credentials = + GaeUserCredentials.forTestingUser(new User("person", "example.com", GAE_USER_ID1), false); + doSuccessfulTest("login_valid.xml"); } @Test public void testFailure_withoutLoginAndLinkedAccount() throws Exception { persistLinkedAccount("person@example.com", GAE_USER_ID1); - noLogin(); + credentials = GaeUserCredentials.forLoggedOutUser(); doFailingTest("login_valid.xml", UserNotLoggedInException.class); } @Test public void testFailure_withoutLoginAndWithoutLinkedAccount() throws Exception { - noLogin(); + credentials = GaeUserCredentials.forLoggedOutUser(); doFailingTest("login_valid.xml", UserNotLoggedInException.class); } @Test public void testFailure_withLoginAndWithoutLinkedAccount() throws Exception { - login("person", "example.com", GAE_USER_ID1); - try { - doFailingTest("login_valid.xml", BadGaeUserIdException.class); - } finally { - ApiProxy.setEnvironmentForCurrentThread(oldEnv); - } + credentials = + GaeUserCredentials.forTestingUser(new User("person", "example.com", GAE_USER_ID1), false); + doFailingTest("login_valid.xml", BadGaeUserIdException.class); } @Test public void testFailure_withLoginAndNoMatchingLinkedAccount() throws Exception { persistLinkedAccount("joe@example.com", GAE_USER_ID2); - login("person", "example.com", GAE_USER_ID1); - try { - doFailingTest("login_valid.xml", BadGaeUserIdException.class); - } finally { - ApiProxy.setEnvironmentForCurrentThread(oldEnv); - } + credentials = + GaeUserCredentials.forTestingUser(new User("person", "example.com", GAE_USER_ID1), false); + doFailingTest("login_valid.xml", BadGaeUserIdException.class); } - Environment login(final String name, final String authDomain, final String gaeUserId) { - oldEnv = ApiProxy.getCurrentEnvironment(); - // This envAttr thing is the only way to set userId. - // see https://code.google.com/p/googleappengine/issues/detail?id=3579 - final HashMap envAttr = new HashMap<>(oldEnv.getAttributes()); - envAttr.put("com.google.appengine.api.users.UserService.user_id_key", gaeUserId); - - final Environment e = oldEnv; - ApiProxy.setEnvironmentForCurrentThread(new Environment() { - @Override - public String getAppId() { - return e.getAppId(); - } - - @Override - public String getModuleId() { - return e.getModuleId(); - } - - @Override - public String getVersionId() { - return e.getVersionId(); - } - - @Override - public String getEmail() { - return name + "@" + authDomain; - } - - @Override - public boolean isLoggedIn() { - return true; - } - - @Override - public boolean isAdmin() { - return e.isAdmin(); - } - - @Override - public String getAuthDomain() { - return authDomain; - } - - @Override - @SuppressWarnings("deprecation") - public String getRequestNamespace() { - return e.getRequestNamespace(); - } - - @Override - public long getRemainingMillis() { - return e.getRemainingMillis(); - } - - @Override - public Map getAttributes() { - return envAttr; - } - }); - credentials = new GaeUserCredentials(getUserService().getCurrentUser()); - return oldEnv; - } - - void noLogin() { - oldEnv = ApiProxy.getCurrentEnvironment(); - credentials = new GaeUserCredentials(getUserService().getCurrentUser()); - } - - void persistLinkedAccount(String email, String gaeUserId) { + private void persistLinkedAccount(String email, String gaeUserId) { Registrar registrar = Registrar.loadByClientId("NewRegistrar"); RegistrarContact c = new RegistrarContact.Builder() .setParent(registrar) From 2537e95de5d6cc549c13a0b327c9ad61f0c79d4e Mon Sep 17 00:00:00 2001 From: nickfelt Date: Fri, 9 Sep 2016 14:09:11 -0700 Subject: [PATCH 14/31] Change EppMetric.Builder to use @AutoValue.Builder Getting rid of builder boilerplate makes my heart sing. Since we can no longer @Inject the Builder() constructor, this change adds a provider in WhiteboxModule that calls a special builderForRequest() factory method, which gets passed a request ID and Clock and preserves the existing EppMetric magic that sets the start and end time for you. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=132714432 --- java/google/registry/flows/EppController.java | 3 +- .../frontend/FrontendRequestComponent.java | 2 + java/google/registry/module/tools/BUILD | 1 + .../module/tools/ToolsRequestComponent.java | 2 + .../monitoring/whitebox/EppMetric.java | 139 +++++++----------- .../monitoring/whitebox/WhiteboxModule.java | 18 +++ .../registry/flows/EppControllerTest.java | 16 +- .../registry/flows/EppTestComponent.java | 6 +- .../google/registry/flows/FlowRunnerTest.java | 2 +- .../monitoring/whitebox/EppMetricTest.java | 26 ++-- 10 files changed, 108 insertions(+), 107 deletions(-) diff --git a/java/google/registry/flows/EppController.java b/java/google/registry/flows/EppController.java index e49e52406..394592ebb 100644 --- a/java/google/registry/flows/EppController.java +++ b/java/google/registry/flows/EppController.java @@ -18,6 +18,7 @@ import static google.registry.flows.EppXmlTransformer.unmarshal; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; +import com.google.common.base.Optional; import google.registry.flows.FlowModule.EppExceptionInProviderException; import google.registry.model.eppcommon.Trid; import google.registry.model.eppinput.EppInput; @@ -55,7 +56,7 @@ public final class EppController { boolean isDryRun, boolean isSuperuser, byte[] inputXmlBytes) { - metricBuilder.setClientId(sessionMetadata.getClientId()); + metricBuilder.setClientId(Optional.fromNullable(sessionMetadata.getClientId())); metricBuilder.setPrivilegeLevel(isSuperuser ? "SUPERUSER" : "NORMAL"); try { EppInput eppInput; diff --git a/java/google/registry/module/frontend/FrontendRequestComponent.java b/java/google/registry/module/frontend/FrontendRequestComponent.java index 158922b1b..84da740f8 100644 --- a/java/google/registry/module/frontend/FrontendRequestComponent.java +++ b/java/google/registry/module/frontend/FrontendRequestComponent.java @@ -21,6 +21,7 @@ import google.registry.flows.EppConsoleAction; import google.registry.flows.EppTlsAction; import google.registry.flows.FlowComponent; import google.registry.flows.TlsCredentials.EppTlsModule; +import google.registry.monitoring.whitebox.WhiteboxModule; import google.registry.rdap.RdapAutnumAction; import google.registry.rdap.RdapDomainAction; import google.registry.rdap.RdapDomainSearchAction; @@ -50,6 +51,7 @@ import google.registry.whois.WhoisServer; RdapModule.class, RegistrarUserModule.class, RequestModule.class, + WhiteboxModule.class, WhoisModule.class, }) interface FrontendRequestComponent { diff --git a/java/google/registry/module/tools/BUILD b/java/google/registry/module/tools/BUILD index fc57ff329..e24b0e0e2 100644 --- a/java/google/registry/module/tools/BUILD +++ b/java/google/registry/module/tools/BUILD @@ -24,6 +24,7 @@ java_library( "//java/google/registry/keyring/api", "//java/google/registry/loadtest", "//java/google/registry/mapreduce", + "//java/google/registry/monitoring/whitebox", "//java/google/registry/request", "//java/google/registry/request:modules", "//java/google/registry/tools/server", diff --git a/java/google/registry/module/tools/ToolsRequestComponent.java b/java/google/registry/module/tools/ToolsRequestComponent.java index 7a10ff928..65b3be5f2 100644 --- a/java/google/registry/module/tools/ToolsRequestComponent.java +++ b/java/google/registry/module/tools/ToolsRequestComponent.java @@ -22,6 +22,7 @@ import google.registry.flows.FlowComponent; import google.registry.loadtest.LoadTestAction; import google.registry.loadtest.LoadTestModule; import google.registry.mapreduce.MapreduceModule; +import google.registry.monitoring.whitebox.WhiteboxModule; import google.registry.request.RequestModule; import google.registry.request.RequestScope; import google.registry.tools.server.CreateGroupsAction; @@ -54,6 +55,7 @@ import google.registry.tools.server.javascrap.RefreshAllDomainsAction; MapreduceModule.class, RequestModule.class, ToolsServerModule.class, + WhiteboxModule.class, }) interface ToolsRequestComponent { BackfillAutorenewBillingFlagAction backfillAutorenewBillingFlagAction(); diff --git a/java/google/registry/monitoring/whitebox/EppMetric.java b/java/google/registry/monitoring/whitebox/EppMetric.java index 6e72d48c8..f3824ceb4 100644 --- a/java/google/registry/monitoring/whitebox/EppMetric.java +++ b/java/google/registry/monitoring/whitebox/EppMetric.java @@ -14,21 +14,18 @@ package google.registry.monitoring.whitebox; -import static com.google.apphosting.api.ApiProxy.getCurrentEnvironment; import static google.registry.bigquery.BigqueryUtils.toBigqueryTimestamp; -import static org.joda.time.DateTimeZone.UTC; import com.google.api.services.bigquery.model.TableFieldSchema; import com.google.auto.value.AutoValue; -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import google.registry.bigquery.BigqueryUtils.FieldType; import google.registry.model.eppoutput.Result.Code; import google.registry.request.RequestScope; +import google.registry.util.Clock; import java.util.concurrent.TimeUnit; -import javax.inject.Inject; import org.joda.time.DateTime; /** @@ -53,30 +50,6 @@ public abstract class EppMetric implements BigQueryMetric { new TableFieldSchema().setName("eppStatus").setType(FieldType.INTEGER.name()), new TableFieldSchema().setName("attempts").setType(FieldType.INTEGER.name())); - private static final String REQUEST_LOG_ID = "com.google.appengine.runtime.request_log_id"; - - private static EppMetric create( - String requestId, - DateTime startTimestamp, - DateTime endTimestamp, - String commandName, - String clientId, - String privilegeLevel, - String eppTarget, - Code status, - int attempts) { - return new AutoValue_EppMetric( - requestId, - startTimestamp, - endTimestamp, - Optional.fromNullable(commandName), - Optional.fromNullable(clientId), - Optional.fromNullable(privilegeLevel), - Optional.fromNullable(eppTarget), - Optional.fromNullable(status), - attempts); - } - public abstract String getRequestId(); public abstract DateTime getStartTimestamp(); @@ -141,90 +114,78 @@ public abstract class EppMetric implements BigQueryMetric { } } - /** A builder to create instances of {@link EppMetric}. */ - public static class Builder { + /** Create an {@link EppMetric.Builder}. */ + public static Builder builder() { + return new AutoValue_EppMetric.Builder(); + } - // Required values - private final String requestId; - private final DateTime startTimestamp; + /** + * Create an {@link EppMetric.Builder} for a request context, with the given request ID and + * with start and end timestamps taken from the given clock. + * + *

The start timestamp is recorded now, and the end timestamp at {@code build()}. + */ + public static Builder builderForRequest(String requestId, Clock clock) { + return builder() + .setRequestId(requestId) + .setStartTimestamp(clock.nowUtc()) + .setClock(clock); + } + + /** A builder to create instances of {@link EppMetric}. */ + @AutoValue.Builder + public abstract static class Builder { + + /** Builder-only counter of the number of attempts, to support {@link #incrementAttempts()}. */ private int attempts = 0; - // Optional values - private String commandName; - private String clientId; - private String privilegeLevel; - private String eppTarget; - private Code status; + /** Builder-only clock to support automatic recording of endTimestamp on {@link #build()}. */ + private Clock clock = null; - /** - * Create an {@link EppMetric.Builder}. - * - *

The start timestamp of metrics created via this instance's {@link Builder#build()} will be - * the time that this builder was created. - */ - @Inject - public Builder() { - this(DateTime.now(UTC)); - } + abstract Builder setRequestId(String requestId); - @VisibleForTesting - Builder(DateTime startTimestamp) { - this.requestId = getCurrentEnvironment().getAttributes().get(REQUEST_LOG_ID).toString(); - this.startTimestamp = startTimestamp; - this.attempts = 0; - } + abstract Builder setStartTimestamp(DateTime startTimestamp); - public Builder setCommandName(String value) { - commandName = value; - return this; - } + abstract Builder setEndTimestamp(DateTime endTimestamp); - public Builder setClientId(String value) { - clientId = value; - return this; - } + public abstract Builder setCommandName(String commandName); - public Builder setPrivilegeLevel(String value) { - privilegeLevel = value; - return this; - } + public abstract Builder setClientId(String clientId); - public Builder setEppTarget(String value) { - eppTarget = value; - return this; - } + public abstract Builder setClientId(Optional clientId); - public Builder setStatus(Code value) { - status = value; - return this; - } + public abstract Builder setPrivilegeLevel(String privilegeLevel); + + public abstract Builder setEppTarget(String eppTarget); + + public abstract Builder setStatus(Code code); + + abstract Builder setAttempts(Integer attempts); public Builder incrementAttempts() { attempts++; return this; } - @VisibleForTesting - EppMetric build(DateTime endTimestamp) { - return EppMetric.create( - requestId, - startTimestamp, - endTimestamp, - commandName, - clientId, - privilegeLevel, - eppTarget, - status, - attempts); + Builder setClock(Clock clock) { + this.clock = clock; + return this; } /** * Build an instance of {@link EppMetric} using this builder. * - *

The end timestamp of the metric will be the current time. + *

If a clock was provided with {@code setClock()}, the end timestamp will be set to the + * current timestamp of the clock; otherwise end timestamp must have been previously set. */ public EppMetric build() { - return build(DateTime.now(UTC)); + setAttempts(attempts); + if (clock != null) { + setEndTimestamp(clock.nowUtc()); + } + return autoBuild(); } + + abstract EppMetric autoBuild(); } } diff --git a/java/google/registry/monitoring/whitebox/WhiteboxModule.java b/java/google/registry/monitoring/whitebox/WhiteboxModule.java index b8cff04d8..801f12b1f 100644 --- a/java/google/registry/monitoring/whitebox/WhiteboxModule.java +++ b/java/google/registry/monitoring/whitebox/WhiteboxModule.java @@ -17,6 +17,7 @@ package google.registry.monitoring.whitebox; import static google.registry.request.RequestParameters.extractRequiredParameter; import com.google.api.services.bigquery.model.TableFieldSchema; +import com.google.apphosting.api.ApiProxy; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; import dagger.Module; @@ -24,7 +25,9 @@ import dagger.Provides; import dagger.multibindings.IntoMap; import dagger.multibindings.StringKey; import google.registry.request.Parameter; +import google.registry.util.Clock; import java.util.UUID; +import javax.inject.Named; import javax.servlet.http.HttpServletRequest; /** @@ -33,6 +36,8 @@ import javax.servlet.http.HttpServletRequest; @Module public class WhiteboxModule { + private static final String REQUEST_LOG_ID = "com.google.appengine.runtime.request_log_id"; + @Provides @IntoMap @StringKey(EppMetric.TABLE_ID) @@ -68,4 +73,17 @@ public class WhiteboxModule { } }; } + + @Provides + @Named("requestLogId") + static String provideRequestLogId() { + return ApiProxy.getCurrentEnvironment().getAttributes().get(REQUEST_LOG_ID).toString(); + } + + /** Provides an EppMetric builder with the request ID and startTimestamp already initialized. */ + @Provides + static EppMetric.Builder provideEppMetricBuilder( + @Named("requestLogId") String requestLogId, Clock clock) { + return EppMetric.builderForRequest(requestLogId, clock); + } } diff --git a/javatests/google/registry/flows/EppControllerTest.java b/javatests/google/registry/flows/EppControllerTest.java index 3f08decb1..7ba6d90e9 100644 --- a/javatests/google/registry/flows/EppControllerTest.java +++ b/javatests/google/registry/flows/EppControllerTest.java @@ -32,8 +32,10 @@ import google.registry.monitoring.whitebox.EppMetric; import google.registry.testing.AppEngineRule; import google.registry.testing.FakeClock; import google.registry.testing.ShardableTestCase; +import google.registry.util.Clock; import google.registry.util.SystemClock; import google.registry.xml.ValidationMode; +import org.joda.time.DateTime; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -61,6 +63,10 @@ public class EppControllerTest extends ShardableTestCase { @Mock EppResponse eppResponse; @Mock Result result; + private static final DateTime startTime = DateTime.parse("2016-09-01T00:00:00Z"); + + private final Clock clock = new FakeClock(startTime); + private EppController eppController; @Before @@ -77,9 +83,9 @@ public class EppControllerTest extends ShardableTestCase { when(result.getCode()).thenReturn(Code.SuccessWithNoMessages); eppController = new EppController(); - eppController.metricBuilder = new EppMetric.Builder(); + eppController.metricBuilder = EppMetric.builderForRequest("request-id-1", clock); eppController.bigQueryMetricsEnqueuer = metricsEnqueuer; - eppController.clock = new FakeClock(); + eppController.clock = clock; eppController.flowComponentBuilder = flowComponentBuilder; eppController.eppMetrics = eppMetrics; } @@ -105,6 +111,9 @@ public class EppControllerTest extends ShardableTestCase { verify(metricsEnqueuer).export(metricCaptor.capture()); EppMetric metric = metricCaptor.getValue(); + assertThat(metric.getRequestId()).isEqualTo("request-id-1"); + assertThat(metric.getStartTimestamp()).isEqualTo(startTime); + assertThat(metric.getEndTimestamp()).isEqualTo(clock.nowUtc()); assertThat(metric.getClientId()).hasValue("some-client"); assertThat(metric.getPrivilegeLevel()).hasValue("NORMAL"); assertThat(metric.getStatus()).hasValue(Code.SyntaxError); @@ -126,6 +135,9 @@ public class EppControllerTest extends ShardableTestCase { verify(metricsEnqueuer).export(metricCaptor.capture()); EppMetric metric = metricCaptor.getValue(); + assertThat(metric.getRequestId()).isEqualTo("request-id-1"); + assertThat(metric.getStartTimestamp()).isEqualTo(startTime); + assertThat(metric.getEndTimestamp()).isEqualTo(clock.nowUtc()); assertThat(metric.getClientId()).hasValue("some-client"); assertThat(metric.getPrivilegeLevel()).hasValue("SUPERUSER"); assertThat(metric.getStatus()).hasValue(Code.SuccessWithNoMessages); diff --git a/javatests/google/registry/flows/EppTestComponent.java b/javatests/google/registry/flows/EppTestComponent.java index af3cf6398..fd8062c7a 100644 --- a/javatests/google/registry/flows/EppTestComponent.java +++ b/javatests/google/registry/flows/EppTestComponent.java @@ -42,13 +42,13 @@ interface EppTestComponent { @Module static class FakesAndMocksModule { final FakeClock clock; - final EppMetric.Builder metrics; + final EppMetric.Builder metricBuilder; final BigQueryMetricsEnqueuer metricsEnqueuer; final ModulesService modulesService; FakesAndMocksModule(FakeClock clock) { this.clock = clock; - this.metrics = new EppMetric.Builder(); + this.metricBuilder = EppMetric.builderForRequest("request-id-1", clock); this.modulesService = mock(ModulesService.class); this.metricsEnqueuer = mock(BigQueryMetricsEnqueuer.class); } @@ -60,7 +60,7 @@ interface EppTestComponent { @Provides EppMetric.Builder provideMetrics() { - return metrics; + return metricBuilder; } @Provides diff --git a/javatests/google/registry/flows/FlowRunnerTest.java b/javatests/google/registry/flows/FlowRunnerTest.java index 0caf5a94e..36aa64a17 100644 --- a/javatests/google/registry/flows/FlowRunnerTest.java +++ b/javatests/google/registry/flows/FlowRunnerTest.java @@ -90,7 +90,7 @@ public class FlowRunnerTest extends ShardableTestCase { flowRunner.isDryRun = false; flowRunner.isSuperuser = false; flowRunner.isTransactional = false; - flowRunner.metric = new EppMetric.Builder(); + flowRunner.metric = EppMetric.builderForRequest("request-id-1", flowRunner.clock); flowRunner.sessionMetadata = new StatelessRequestSessionMetadata("TheRegistrar", ImmutableSet.of()); flowRunner.trid = Trid.create("client-123", "server-456"); diff --git a/javatests/google/registry/monitoring/whitebox/EppMetricTest.java b/javatests/google/registry/monitoring/whitebox/EppMetricTest.java index 69e2a1494..a6ca39f83 100644 --- a/javatests/google/registry/monitoring/whitebox/EppMetricTest.java +++ b/javatests/google/registry/monitoring/whitebox/EppMetricTest.java @@ -36,21 +36,22 @@ public class EppMetricTest { @Test public void testGetBigQueryRowEncoding_encodesCorrectly() throws Exception { EppMetric metric = - new EppMetric.Builder(new DateTime(1337)) - .setEppTarget("target") - .setPrivilegeLevel("level") + EppMetric.builder() + .setRequestId("request-id-1") + .setStartTimestamp(new DateTime(1337)) + .setEndTimestamp(new DateTime(1338)) .setCommandName("command") .setClientId("client") + .setPrivilegeLevel("level") + .setEppTarget("target") .setStatus(Code.CommandUseError) .incrementAttempts() - .build(new DateTime(1338)); + .build(); - // The request_id is randomly generated and hard to mock without a lot of supporting code - // so we just use the tested metric's request_id verbatim. assertThat(metric.getBigQueryRowEncoding()) .containsExactlyEntriesIn( new ImmutableMap.Builder() - .put("requestId", metric.getRequestId()) + .put("requestId", "request-id-1") .put("startTime", "1.337000") .put("endTime", "1.338000") .put("commandName", "command") @@ -65,14 +66,17 @@ public class EppMetricTest { @Test public void testGetBigQueryRowEncoding_hasAllSchemaFields() throws Exception { EppMetric metric = - new EppMetric.Builder(new DateTime(1337)) - .setEppTarget("target") - .setPrivilegeLevel("level") + EppMetric.builder() + .setRequestId("request-id-1") + .setStartTimestamp(new DateTime(1337)) + .setEndTimestamp(new DateTime(1338)) .setCommandName("command") .setClientId("client") + .setPrivilegeLevel("level") + .setEppTarget("target") .setStatus(Code.CommandUseError) .incrementAttempts() - .build(new DateTime(1338)); + .build(); ImmutableSet.Builder schemaFieldNames = new ImmutableSet.Builder<>(); for (TableFieldSchema schemaField : metric.getSchemaFields()) { schemaFieldNames.add(schemaField.getName()); From 9dffd64dc76726e9fbbc2800cacf6a89906b68ca Mon Sep 17 00:00:00 2001 From: nickfelt Date: Fri, 9 Sep 2016 15:11:09 -0700 Subject: [PATCH 15/31] Cleanup minor things in whitebox metrics code Specifically: - remove @RequestScope from EppMetric since it's only for components - fix to call the better overload of toBigqueryTimestamp - use the same UUID provider for BigQueryMetricsEnqueuer that already exists for the VerifyEntityIntegrityStreamer - minor cleanup in VerifyEntityIntegrityStreamer (inject projectId vs whole env) ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=132721794 --- .../whitebox/BigQueryMetricsEnqueuer.java | 18 +++++--------- .../monitoring/whitebox/EppMetric.java | 11 ++------- .../VerifyEntityIntegrityStreamer.java | 24 +++++++++---------- .../monitoring/whitebox/WhiteboxModule.java | 3 ++- .../whitebox/BigQueryMetricsEnqueuerTest.java | 4 +++- .../VerifyEntityIntegrityActionTest.java | 3 +-- 6 files changed, 25 insertions(+), 38 deletions(-) diff --git a/java/google/registry/monitoring/whitebox/BigQueryMetricsEnqueuer.java b/java/google/registry/monitoring/whitebox/BigQueryMetricsEnqueuer.java index 5bbb9077a..5e444f3dc 100644 --- a/java/google/registry/monitoring/whitebox/BigQueryMetricsEnqueuer.java +++ b/java/google/registry/monitoring/whitebox/BigQueryMetricsEnqueuer.java @@ -20,11 +20,11 @@ import static com.google.appengine.api.taskqueue.TaskOptions.Builder.withUrl; import com.google.appengine.api.modules.ModulesService; import com.google.appengine.api.taskqueue.TaskOptions; import com.google.appengine.api.taskqueue.TransientFailureException; -import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Supplier; import google.registry.util.FormattingLogger; import java.util.Map.Entry; -import java.util.UUID; import javax.inject.Inject; +import javax.inject.Named; /** * A collector of metric information. Enqueues collected metrics to a task queue to be written to @@ -39,18 +39,17 @@ public class BigQueryMetricsEnqueuer { public static final String QUEUE = "bigquery-streaming-metrics"; @Inject ModulesService modulesService; + @Inject @Named("insertIdGenerator") Supplier idGenerator; - @Inject - BigQueryMetricsEnqueuer() {} + @Inject BigQueryMetricsEnqueuer() {} - @VisibleForTesting - void export(BigQueryMetric metric, String insertId) { + public void export(BigQueryMetric metric) { try { String hostname = modulesService.getVersionHostname("backend", null); TaskOptions opts = withUrl(MetricsExportAction.PATH) .header("Host", hostname) - .param("insertId", insertId); + .param("insertId", idGenerator.get()); for (Entry entry : metric.getBigQueryRowEncoding().entrySet()) { opts.param(entry.getKey(), entry.getValue()); } @@ -61,9 +60,4 @@ public class BigQueryMetricsEnqueuer { logger.info(e, e.getMessage()); } } - - /** Enqueue a metric to be exported to BigQuery. */ - public void export(BigQueryMetric metric) { - export(metric, UUID.randomUUID().toString()); - } } diff --git a/java/google/registry/monitoring/whitebox/EppMetric.java b/java/google/registry/monitoring/whitebox/EppMetric.java index f3824ceb4..a4e80df7f 100644 --- a/java/google/registry/monitoring/whitebox/EppMetric.java +++ b/java/google/registry/monitoring/whitebox/EppMetric.java @@ -23,9 +23,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import google.registry.bigquery.BigqueryUtils.FieldType; import google.registry.model.eppoutput.Result.Code; -import google.registry.request.RequestScope; import google.registry.util.Clock; -import java.util.concurrent.TimeUnit; import org.joda.time.DateTime; /** @@ -34,7 +32,6 @@ import org.joda.time.DateTime; * @see BigQueryMetricsEnqueuer */ @AutoValue -@RequestScope public abstract class EppMetric implements BigQueryMetric { static final String TABLE_ID = "eppMetrics"; @@ -84,12 +81,8 @@ public abstract class EppMetric implements BigQueryMetric { ImmutableMap.Builder map = ImmutableMap.builder() .put("requestId", getRequestId()) - .put( - "startTime", - toBigqueryTimestamp(getStartTimestamp().getMillis(), TimeUnit.MILLISECONDS)) - .put( - "endTime", - toBigqueryTimestamp(getEndTimestamp().getMillis(), TimeUnit.MILLISECONDS)) + .put("startTime", toBigqueryTimestamp(getStartTimestamp())) + .put("endTime", toBigqueryTimestamp(getEndTimestamp())) .put("attempts", getAttempts().toString()); // Populate optional values, if present addOptional("commandName", getCommandName(), map); diff --git a/java/google/registry/monitoring/whitebox/VerifyEntityIntegrityStreamer.java b/java/google/registry/monitoring/whitebox/VerifyEntityIntegrityStreamer.java index 4476983f6..f9b35ca76 100644 --- a/java/google/registry/monitoring/whitebox/VerifyEntityIntegrityStreamer.java +++ b/java/google/registry/monitoring/whitebox/VerifyEntityIntegrityStreamer.java @@ -37,13 +37,14 @@ import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import google.registry.bigquery.BigqueryFactory; -import google.registry.config.RegistryEnvironment; +import google.registry.config.ConfigModule.Config; import google.registry.util.Retrier; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import javax.annotation.Nullable; +import javax.inject.Named; import org.joda.time.DateTime; /** @@ -52,22 +53,21 @@ import org.joda.time.DateTime; @AutoFactory(allowSubclasses = true) public class VerifyEntityIntegrityStreamer { + private final String projectId; + private final BigqueryFactory bigqueryFactory; + private final Supplier idGenerator; + private final Retrier retrier; private final DateTime scanTime; private Bigquery bigquery; - BigqueryFactory bigqueryFactory; - RegistryEnvironment environment; - Retrier retrier; - Supplier idGenerator; - public VerifyEntityIntegrityStreamer( + @Provided @Config("projectId") String projectId, @Provided BigqueryFactory bigqueryFactory, - @Provided RegistryEnvironment environment, @Provided Retrier retrier, - @Provided Supplier idGenerator, + @Provided @Named("insertIdGenerator") Supplier idGenerator, DateTime scanTime) { + this.projectId = projectId; this.bigqueryFactory = bigqueryFactory; - this.environment = environment; this.retrier = retrier; this.idGenerator = idGenerator; this.scanTime = scanTime; @@ -78,9 +78,7 @@ public class VerifyEntityIntegrityStreamer { // BigQuery. private Bigquery getBigquery() throws IOException { if (bigquery == null) { - bigquery = - bigqueryFactory.create( - environment.config().getProjectId(), DATASET, TABLE_ID); + bigquery = bigqueryFactory.create(projectId, DATASET, TABLE_ID); } return bigquery; } @@ -179,7 +177,7 @@ public class VerifyEntityIntegrityStreamer { getBigquery() .tabledata() .insertAll( - environment.config().getProjectId(), + projectId, DATASET, TABLE_ID, new TableDataInsertAllRequest().setRows(rows)); diff --git a/java/google/registry/monitoring/whitebox/WhiteboxModule.java b/java/google/registry/monitoring/whitebox/WhiteboxModule.java index 801f12b1f..6d08f7412 100644 --- a/java/google/registry/monitoring/whitebox/WhiteboxModule.java +++ b/java/google/registry/monitoring/whitebox/WhiteboxModule.java @@ -65,7 +65,8 @@ public class WhiteboxModule { } @Provides - static Supplier provideIdGenerator() { + @Named("insertIdGenerator") + static Supplier provideInsertIdGenerator() { return new Supplier() { @Override public String get() { diff --git a/javatests/google/registry/monitoring/whitebox/BigQueryMetricsEnqueuerTest.java b/javatests/google/registry/monitoring/whitebox/BigQueryMetricsEnqueuerTest.java index 46129d44a..7ce1fe169 100644 --- a/javatests/google/registry/monitoring/whitebox/BigQueryMetricsEnqueuerTest.java +++ b/javatests/google/registry/monitoring/whitebox/BigQueryMetricsEnqueuerTest.java @@ -21,6 +21,7 @@ import static org.mockito.Mockito.when; import com.google.api.services.bigquery.model.TableFieldSchema; import com.google.appengine.api.modules.ModulesService; import com.google.auto.value.AutoValue; +import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import google.registry.testing.AppEngineRule; @@ -56,6 +57,7 @@ public class BigQueryMetricsEnqueuerTest { @Before public void setUp() { enqueuer = new BigQueryMetricsEnqueuer(); + enqueuer.idGenerator = Suppliers.ofInstance("laffo"); enqueuer.modulesService = modulesService; when(modulesService.getVersionHostname(Matchers.anyString(), Matchers.anyString())) .thenReturn("1.backend.test.localhost"); @@ -67,7 +69,7 @@ public class BigQueryMetricsEnqueuerTest { TestMetric.create( DateTime.parse("1984-12-18TZ"), DateTime.parse("1984-12-18TZ").plusMillis(1)); - enqueuer.export(metric, "laffo"); + enqueuer.export(metric); assertTasksEnqueued("bigquery-streaming-metrics", new TaskMatcher() diff --git a/javatests/google/registry/monitoring/whitebox/VerifyEntityIntegrityActionTest.java b/javatests/google/registry/monitoring/whitebox/VerifyEntityIntegrityActionTest.java index ae61bded9..19dc2e7ae 100644 --- a/javatests/google/registry/monitoring/whitebox/VerifyEntityIntegrityActionTest.java +++ b/javatests/google/registry/monitoring/whitebox/VerifyEntityIntegrityActionTest.java @@ -43,7 +43,6 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.googlecode.objectify.Key; import google.registry.bigquery.BigqueryFactory; -import google.registry.config.RegistryEnvironment; import google.registry.mapreduce.MapreduceRunner; import google.registry.model.contact.ContactResource; import google.registry.model.domain.DomainResource; @@ -108,8 +107,8 @@ public class VerifyEntityIntegrityActionTest inject.setStaticField(VerifyEntityIntegrityAction.class, "component", component); integrity = new VerifyEntityIntegrityStreamer( + "project-id", bigqueryFactory, - RegistryEnvironment.UNITTEST, new Retrier(new FakeSleeper(new FakeClock()), 1), Suppliers.ofInstance("rowid"), now); From d7443f2eee937c0741efed5efb5c60af8c7a3415 Mon Sep 17 00:00:00 2001 From: cgoldfeder Date: Mon, 12 Sep 2016 08:08:32 -0700 Subject: [PATCH 16/31] Fix copy/paste javadoc erroc ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=132875203 --- java/google/registry/flows/EppException.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/google/registry/flows/EppException.java b/java/google/registry/flows/EppException.java index 345e8ca4d..a2f21428b 100644 --- a/java/google/registry/flows/EppException.java +++ b/java/google/registry/flows/EppException.java @@ -251,7 +251,7 @@ public abstract class EppException extends Exception { } } - /** Specified protocol version is not implemented. */ + /** Command failed. */ @EppResultCode(Code.CommandFailed) public static class CommandFailedException extends EppException { public CommandFailedException() { From b9b2829f7cd2d42b26d79b6cc9725bab24b3a56d Mon Sep 17 00:00:00 2001 From: cgoldfeder Date: Mon, 12 Sep 2016 09:49:00 -0700 Subject: [PATCH 17/31] Log the class names of unimplemented extensions before throwing the generic user-visible error. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=132884249 --- java/google/registry/flows/LoggedInFlow.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/java/google/registry/flows/LoggedInFlow.java b/java/google/registry/flows/LoggedInFlow.java index f797d46f0..77a273c16 100644 --- a/java/google/registry/flows/LoggedInFlow.java +++ b/java/google/registry/flows/LoggedInFlow.java @@ -101,7 +101,10 @@ public abstract class LoggedInFlow extends Flow { allowedTlds = registrar.getAllowedTlds(); } initLoggedInFlow(); - if (!difference(extensionClasses, getValidRequestExtensions()).isEmpty()) { + Set> unimplementedExtensions = + difference(extensionClasses, getValidRequestExtensions()); + if (!unimplementedExtensions.isEmpty()) { + logger.infofmt("Unimplemented extensions: %s", unimplementedExtensions); throw new UnimplementedExtensionException(); } } From cee08d48f233683137ca0b7ec25b3c66fcd5dfaa Mon Sep 17 00:00:00 2001 From: shikhman Date: Mon, 12 Sep 2016 09:49:42 -0700 Subject: [PATCH 18/31] Remove unused dns-cron queue ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=132884314 --- g3doc/app-engine-architecture.md | 143 +++++++++--------- .../env/common/default/WEB-INF/queue.xml | 12 -- 2 files changed, 72 insertions(+), 83 deletions(-) diff --git a/g3doc/app-engine-architecture.md b/g3doc/app-engine-architecture.md index cd6594525..20229dbd3 100644 --- a/g3doc/app-engine-architecture.md +++ b/g3doc/app-engine-architecture.md @@ -105,77 +105,78 @@ query string parameter "queue" in the url specification for the cron task. Here are the task queues in use by the system. All are push queues unless explicitly marked as otherwise. -* `bigquery-streaming-metrics` -- Queue for metrics that are asynchronously - streamed to BigQuery in the `Metrics` class. Tasks are enqueued during EPP - flows in `EppController`. This means that there is a lag of a few seconds to - a few minutes between when metrics are generated and when they are queryable - in BigQuery, but this is preferable to slowing all EPP flows down and blocking - them on BigQuery streaming. -* `brda` -- Queue for tasks to upload weekly Bulk Registration Data Access - (BRDA) files to a location where they are available to ICANN. The - `RdeStagingReducer` (part of the RDE MapReduce) creates these tasks at the end - of generating an RDE dump. -* `delete-commits` -- Cron queue for tasks to regularly delete commit logs that - are more than thirty days stale. These tasks execute the - `DeleteOldCommitLogsAction`. -* `dns-cron` (cron queue) and `dns-pull` (pull queue) -- A push/pull pair of - queues. Cron regularly enqueues tasks in dns-cron each minute, which are then - executed by `ReadDnsQueueAction`, which leases a batch of tasks from the pull - queue, groups them by TLD, and writes them as a single task to `dns-publish` - to be published to the configured DNS writer for the TLD. -* `dns-publish` -- Queue for batches of DNS updates to be pushed to DNS writers. -* `export-bigquery-poll` -- Queue for tasks to query the success/failure of a - given BigQuery export job. Tasks are enqueued by `BigqueryPollJobAction`. -* `export-commits` -- Queue for tasks to export commit log checkpoints. Tasks - are enqueued by `CommitLogCheckpointAction` (which is run every minute by - cron) and executed by `ExportCommitLogDiffAction`. -* `export-reserved-terms` -- Cron queue for tasks to export the list of reserved - terms for each TLD. The tasks are executed by `ExportReservedTermsAction`. -* `export-snapshot` -- Cron and push queue for tasks to load a Datastore - snapshot that was stored in Google Cloud Storage and export it to BigQuery. - Tasks are enqueued by both cron and `CheckSnapshotServlet` and are executed by - both `ExportSnapshotServlet` and `LoadSnapshotAction`. -* `export-snapshot-poll` -- Queue for tasks to check that a Datastore snapshot - has been successfully uploaded to Google Cloud Storage (this is an - asynchronous background operation that can take an indeterminate amount of - time). Once the snapshot is successfully uploaded, it is imported into - BigQuery. Tasks are enqueued by `ExportSnapshotServlet` and executed by - `CheckSnapshotServlet`. -* `export-snapshot-update-view` -- Queue for tasks to update the BigQuery views - to point to the most recently uploaded snapshot. Tasks are enqueued by - `LoadSnapshotAction` and executed by `UpdateSnapshotViewAction`. -* `flows-async` -- Queue for asynchronous tasks that are enqueued during EPP - command flows. Currently all of these tasks correspond to invocations of any - of the following three MapReduces: `DnsRefreshForHostRenameAction`, - `DeleteHostResourceAction`, or `DeleteContactResourceAction`. -* `group-members-sync` -- Cron queue for tasks to sync registrar contacts (not - domain contacts!) to Google Groups. Tasks are executed by - `SyncGroupMembersAction`. -* `load[0-9]` -- Queues used to load-test the system by `LoadTestAction`. These - queues don't need to exist except when actively running load tests (which is - not recommended on production environments). There are ten of these queues to - provide simple sharding, because the Domain Registry system is capable of - handling significantly more Queries Per Second than the highest throttle limit - available on task queues (which is 500 qps). -* `lordn-claims` and `lordn-sunrise` -- Pull queues for handling LORDN exports. - Tasks are enqueued synchronously during EPP commands depending on whether the - domain name in question has a claims notice ID. -* `marksdb` -- Queue for tasks to verify that an upload to NORDN was - successfully received and verified. These tasks are enqueued by - `NordnUploadAction` following an upload and are executed by - `NordnVerifyAction`. -* `nordn` -- Cron queue used for NORDN exporting. Tasks are executed by - `NordnUploadAction`, which pulls LORDN data from the `lordn-claims` and - `lordn-sunrise` pull queues (above). -* `rde-report` -- Queue for tasks to upload RDE reports to ICANN following - successful upload of full RDE files to the escrow provider. Tasks are - enqueued by `RdeUploadAction` and executed by `RdeReportAction`. -* `rde-upload` -- Cron queue for tasks to upload already-generated RDE files - from Cloud Storage to the escrow provider. Tasks are executed by - `RdeUploadAction`. -* `sheet` -- Queue for tasks to sync registrar updates to a Google Sheets - spreadsheet. Tasks are enqueued by `RegistrarServlet` when changes are made - to registrar fields and are executed by `SyncRegistrarsSheetAction`. +* `bigquery-streaming-metrics` -- Queue for metrics that are asynchronously + streamed to BigQuery in the `Metrics` class. Tasks are enqueued during EPP + flows in `EppController`. This means that there is a lag of a few seconds to + a few minutes between when metrics are generated and when they are queryable + in BigQuery, but this is preferable to slowing all EPP flows down and + blocking them on BigQuery streaming. +* `brda` -- Queue for tasks to upload weekly Bulk Registration Data Access + (BRDA) files to a location where they are available to ICANN. The + `RdeStagingReducer` (part of the RDE MapReduce) creates these tasks at the + end of generating an RDE dump. +* `delete-commits` -- Cron queue for tasks to regularly delete commit logs + that are more than thirty days stale. These tasks execute the + `DeleteOldCommitLogsAction`. +* `dns-pull` -- A pull queue to enqueue DNS modifications. Cron regularly runs + `ReadDnsQueueAction`, which drains the queue, batches modifications by TLD, + and writes the batches to `dns-publish` to be published to the configured + `DnsWriter` for the TLD. +* `dns-publish` -- Queue for batches of DNS updates to be pushed to DNS + writers. +* `export-bigquery-poll` -- Queue for tasks to query the success/failure of a + given BigQuery export job. Tasks are enqueued by `BigqueryPollJobAction`. +* `export-commits` -- Queue for tasks to export commit log checkpoints. Tasks + are enqueued by `CommitLogCheckpointAction` (which is run every minute by + cron) and executed by `ExportCommitLogDiffAction`. +* `export-reserved-terms` -- Cron queue for tasks to export the list of + reserved terms for each TLD. The tasks are executed by + `ExportReservedTermsAction`. +* `export-snapshot` -- Cron and push queue for tasks to load a Datastore + snapshot that was stored in Google Cloud Storage and export it to BigQuery. + Tasks are enqueued by both cron and `CheckSnapshotServlet` and are executed + by both `ExportSnapshotServlet` and `LoadSnapshotAction`. +* `export-snapshot-poll` -- Queue for tasks to check that a Datastore snapshot + has been successfully uploaded to Google Cloud Storage (this is an + asynchronous background operation that can take an indeterminate amount of + time). Once the snapshot is successfully uploaded, it is imported into + BigQuery. Tasks are enqueued by `ExportSnapshotServlet` and executed by + `CheckSnapshotServlet`. +* `export-snapshot-update-view` -- Queue for tasks to update the BigQuery + views to point to the most recently uploaded snapshot. Tasks are enqueued by + `LoadSnapshotAction` and executed by `UpdateSnapshotViewAction`. +* `flows-async` -- Queue for asynchronous tasks that are enqueued during EPP + command flows. Currently all of these tasks correspond to invocations of any + of the following three MapReduces: `DnsRefreshForHostRenameAction`, + `DeleteHostResourceAction`, or `DeleteContactResourceAction`. +* `group-members-sync` -- Cron queue for tasks to sync registrar contacts (not + domain contacts!) to Google Groups. Tasks are executed by + `SyncGroupMembersAction`. +* `load[0-9]` -- Queues used to load-test the system by `LoadTestAction`. + These queues don't need to exist except when actively running load tests + (which is not recommended on production environments). There are ten of + these queues to provide simple sharding, because the Domain Registry system + is capable of handling significantly more Queries Per Second than the + highest throttle limit available on task queues (which is 500 qps). +* `lordn-claims` and `lordn-sunrise` -- Pull queues for handling LORDN + exports. Tasks are enqueued synchronously during EPP commands depending on + whether the domain name in question has a claims notice ID. +* `marksdb` -- Queue for tasks to verify that an upload to NORDN was + successfully received and verified. These tasks are enqueued by + `NordnUploadAction` following an upload and are executed by + `NordnVerifyAction`. +* `nordn` -- Cron queue used for NORDN exporting. Tasks are executed by + `NordnUploadAction`, which pulls LORDN data from the `lordn-claims` and + `lordn-sunrise` pull queues (above). +* `rde-report` -- Queue for tasks to upload RDE reports to ICANN following + successful upload of full RDE files to the escrow provider. Tasks are + enqueued by `RdeUploadAction` and executed by `RdeReportAction`. +* `rde-upload` -- Cron queue for tasks to upload already-generated RDE files + from Cloud Storage to the escrow provider. Tasks are executed by + `RdeUploadAction`. +* `sheet` -- Queue for tasks to sync registrar updates to a Google Sheets + spreadsheet. Tasks are enqueued by `RegistrarServlet` when changes are made + to registrar fields and are executed by `SyncRegistrarsSheetAction`. ## Environments diff --git a/java/google/registry/env/common/default/WEB-INF/queue.xml b/java/google/registry/env/common/default/WEB-INF/queue.xml index 5e3c384fe..891e35428 100644 --- a/java/google/registry/env/common/default/WEB-INF/queue.xml +++ b/java/google/registry/env/common/default/WEB-INF/queue.xml @@ -7,18 +7,6 @@ 5 - - dns-cron - - 10/s - 100 - - 1 - - - dns-pull pull From 85eb641ca8a7a0a9c9b93e8b3a16cb77353ed21e Mon Sep 17 00:00:00 2001 From: mcilwain Date: Mon, 12 Sep 2016 10:03:50 -0700 Subject: [PATCH 19/31] Autoformat all Markdown documentation ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=132885981 --- README.md | 130 +++++---- g3doc/app-engine-architecture.md | 456 ++++++++++++++++--------------- g3doc/configuration.md | 78 +++--- g3doc/install.md | 127 ++++----- g3doc/registry-tool.md | 63 ++--- 5 files changed, 436 insertions(+), 418 deletions(-) diff --git a/README.md b/README.md index c1becf530..8939b29e1 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,10 @@ the Markdown documents in the `docs` directory. When it comes to internet land, ownership flows down the following hierarchy: -1. [ICANN][icann] -2. [Registries][registry] (e.g. Google Registry) -3. [Registrars][registrar] (e.g. Google Domains) -4. Registrants (e.g. you) +1. [ICANN][icann] +2. [Registries][registry] (e.g. Google Registry) +3. [Registrars][registrar] (e.g. Google Domains) +4. Registrants (e.g. you) A registry is any organization that operates an entire top-level domain. For example, Verisign controls all the .COM domains and Affilias controls all the @@ -50,9 +50,9 @@ are limited to four minutes and ten megabytes in size. Furthermore, queries and indexes that span entity groups are always eventually consistent, which means they could take seconds, and very rarely, days to update. While most online services find eventual consistency useful, it is not appropriate for a service -conducting financial exchanges. Therefore Domain Registry has been engineered -to employ performance and complexity tradeoffs that allow strong consistency to -be applied throughout the codebase. +conducting financial exchanges. Therefore Domain Registry has been engineered to +employ performance and complexity tradeoffs that allow strong consistency to be +applied throughout the codebase. Domain Registry has a commit log system. Commit logs are retained in datastore for thirty days. They are also streamed to Cloud Storage for backup purposes. @@ -63,8 +63,8 @@ order to do restores. Each EPP resource entity also stores a map of its past mutations with 24-hour granularity. This makes it possible to have point-in-time projection queries with effectively no overhead. -The Registry Data Escrow (RDE) system is also built with reliability in mind. -It executes on top of App Engine task queues, which can be double-executed and +The Registry Data Escrow (RDE) system is also built with reliability in mind. It +executes on top of App Engine task queues, which can be double-executed and therefore require operations to be idempotent. RDE isn't idempotent. To work around this, RDE uses datastore transactions to achieve mutual exclusion and serialization. We call this the "Locking Rolling Cursor Pattern." One benefit of @@ -94,14 +94,15 @@ proxy listening on port 700. Poll message support is also included. To supplement EPP, Domain Registry also provides a public API for performing domain availability checks. This service listens on the `/check` path. -* [RFC 5730: EPP](http://tools.ietf.org/html/rfc5730) -* [RFC 5731: EPP Domain Mapping](http://tools.ietf.org/html/rfc5731) -* [RFC 5732: EPP Host Mapping](http://tools.ietf.org/html/rfc5732) -* [RFC 5733: EPP Contact Mapping](http://tools.ietf.org/html/rfc5733) -* [RFC 3915: EPP Grace Period Mapping](http://tools.ietf.org/html/rfc3915) -* [RFC 5734: EPP Transport over TCP](http://tools.ietf.org/html/rfc5734) -* [RFC 5910: EPP DNSSEC Mapping](http://tools.ietf.org/html/rfc5910) -* [Draft: EPP Launch Phase Mapping (Proposed)](http://tools.ietf.org/html/draft-tan-epp-launchphase-11) +* [RFC 5730: EPP](http://tools.ietf.org/html/rfc5730) +* [RFC 5731: EPP Domain Mapping](http://tools.ietf.org/html/rfc5731) +* [RFC 5732: EPP Host Mapping](http://tools.ietf.org/html/rfc5732) +* [RFC 5733: EPP Contact Mapping](http://tools.ietf.org/html/rfc5733) +* [RFC 3915: EPP Grace Period Mapping](http://tools.ietf.org/html/rfc3915) +* [RFC 5734: EPP Transport over TCP](http://tools.ietf.org/html/rfc5734) +* [RFC 5910: EPP DNSSEC Mapping](http://tools.ietf.org/html/rfc5910) +* [Draft: EPP Launch Phase Mapping (Proposed)] + (http://tools.ietf.org/html/draft-tan-epp-launchphase-11) ### Registry Data Escrow (RDE) @@ -114,17 +115,22 @@ This service exists for ICANN regulatory purposes. ICANN needs to know that, should a registry business ever implode, that they can quickly migrate their TLDs to a different company so that they'll continue to operate. -* [Draft: Registry Data Escrow Specification](http://tools.ietf.org/html/draft-arias-noguchi-registry-data-escrow-06) -* [Draft: Domain Name Registration Data (DNRD) Objects Mapping](http://tools.ietf.org/html/draft-arias-noguchi-dnrd-objects-mapping-05) -* [Draft: ICANN Registry Interfaces](http://tools.ietf.org/html/draft-lozano-icann-registry-interfaces-05) +* [Draft: Registry Data Escrow Specification] + (http://tools.ietf.org/html/draft-arias-noguchi-registry-data-escrow-06) +* [Draft: Domain Name Registration Data (DNRD) Objects Mapping] + (http://tools.ietf.org/html/draft-arias-noguchi-dnrd-objects-mapping-05) +* [Draft: ICANN Registry Interfaces] + (http://tools.ietf.org/html/draft-lozano-icann-registry-interfaces-05) ### Trademark Clearing House (TMCH) Domain Registry integrates with ICANN and IBM's MarksDB in order to protect trademark holders, when new TLDs are being launched. -* [Draft: TMCH Functional Spec](http://tools.ietf.org/html/draft-lozano-tmch-func-spec-08) -* [Draft: Mark and Signed Mark Objects Mapping](https://tools.ietf.org/html/draft-lozano-tmch-smd-02) +* [Draft: TMCH Functional Spec] + (http://tools.ietf.org/html/draft-lozano-tmch-func-spec-08) +* [Draft: Mark and Signed Mark Objects Mapping] + (https://tools.ietf.org/html/draft-lozano-tmch-smd-02) ### WHOIS @@ -134,8 +140,10 @@ internal HTTP endpoint running on `/_dr/whois`. A separate proxy running on port 43 forwards requests to that path. Domain Registry also implements a public HTTP endpoint that listens on the `/whois` path. -* [RFC 3912: WHOIS Protocol Specification](https://tools.ietf.org/html/rfc3912) -* [RFC 7485: Inventory and Analysis of Registration Objects](http://tools.ietf.org/html/rfc7485) +* [RFC 3912: WHOIS Protocol Specification] + (https://tools.ietf.org/html/rfc3912) +* [RFC 7485: Inventory and Analysis of Registration Objects] + (http://tools.ietf.org/html/rfc7485) ### Registration Data Access Protocol (RDAP) @@ -143,23 +151,24 @@ RDAP is the new standard for WHOIS. It provides much richer functionality, such as the ability to perform wildcard searches. Domain Registry makes this HTTP service available under the `/rdap/...` path. -* [RFC 7480: RDAP HTTP Usage](http://tools.ietf.org/html/rfc7480) -* [RFC 7481: RDAP Security Services](http://tools.ietf.org/html/rfc7481) -* [RFC 7482: RDAP Query Format](http://tools.ietf.org/html/rfc7482) -* [RFC 7483: RDAP JSON Responses](http://tools.ietf.org/html/rfc7483) -* [RFC 7484: RDAP Finding the Authoritative Registration Data](http://tools.ietf.org/html/rfc7484) +* [RFC 7480: RDAP HTTP Usage](http://tools.ietf.org/html/rfc7480) +* [RFC 7481: RDAP Security Services](http://tools.ietf.org/html/rfc7481) +* [RFC 7482: RDAP Query Format](http://tools.ietf.org/html/rfc7482) +* [RFC 7483: RDAP JSON Responses](http://tools.ietf.org/html/rfc7483) +* [RFC 7484: RDAP Finding the Authoritative Registration Data] + (http://tools.ietf.org/html/rfc7484) ### Backups The registry provides a system for generating and restoring from backups with -strong point-in-time consistency. Datastore backups are written out once daily +strong point-in-time consistency. Datastore backups are written out once daily to Cloud Storage using the built-in Datastore snapshot export functionality. Separately, entities called commit logs are continuously exported to track changes that occur in between the regularly scheduled backups. A restore involves wiping out all entities in Datastore, importing the most recent complete daily backup snapshot, then replaying all of the commit logs -since that snapshot. This yields a system state that is guaranteed +since that snapshot. This yields a system state that is guaranteed transactionally consistent. ### Billing @@ -173,24 +182,26 @@ monthly invoices per registrar. Because the registry runs on the Google Cloud Platform stack, it benefits from high availability, automatic fail-over, and horizontal auto-scaling of compute -and database resources. This makes it quite flexible for running TLDs of any +and database resources. This makes it quite flexible for running TLDs of any size. ### Automated tests The registry codebase includes ~400 test classes with ~4,000 total unit and -integration tests. This limits regressions, ensures correct system +integration tests. This limits regressions, ensures correct system functionality, and allows for easy continued future development and refactoring. ### DNS An interface for DNS operations is provided, along with a sample implementation -that uses the [Google Cloud DNS](https://cloud.google.com/dns/) API. A bulk +that uses the [Google Cloud DNS](https://cloud.google.com/dns/) API. A bulk export tool is also provided to export a zone file for an entire TLD in BIND format. -* [RFC 1034: Domain Names - Concepts and Facilities](https://www.ietf.org/rfc/rfc1034.txt) -* [RFC 1035: Domain Names - Implementation and Specification](https://www.ietf.org/rfc/rfc1034.txt) +* [RFC 1034: Domain Names - Concepts and Facilities] + (https://www.ietf.org/rfc/rfc1034.txt) +* [RFC 1035: Domain Names - Implementation and Specification] + (https://www.ietf.org/rfc/rfc1034.txt) ### Exports @@ -202,21 +213,20 @@ ICANN-mandated reports, database snapshots, and reserved terms. ### Metrics and reporting The registry records metrics and regularly exports them to BigQuery so that -analyses can be run on them using full SQL queries. Metrics include which EPP +analyses can be run on them using full SQL queries. Metrics include which EPP commands were run and when and by whom, information on failed commands, activity per registrar, and length of each request. [BigQuery][bigquery] reporting scripts are provided to generate the required -per-TLD monthly -[registry reports](https://www.icann.org/resources/pages/registry-reports) for -ICANN. +per-TLD monthly [registry reports] +(https://www.icann.org/resources/pages/registry-reports) for ICANN. ### Registrar console The registry includes a web-based registrar console that registrars can access -in a browser. It provides the ability for registrars to view their billing +in a browser. It provides the ability for registrars to view their billing invoices in Google Drive, contact the registry provider, and modify WHOIS, -security (including SSL certificates), and registrar contact settings. Main +security (including SSL certificates), and registrar contact settings. Main registry commands such as creating domains, hosts, and contacts must go through EPP and are not provided in the console. @@ -231,7 +241,7 @@ system, and creating new TLDs. ### Plug-and-play pricing engines The registry has the ability to configure per-TLD pricing engines to -programmatically determine the price of domain names on the fly. An +programmatically determine the price of domain names on the fly. An implementation is provided that uses the contents of a static list of prices (this being by far the most common type of premium pricing used for TLDs). @@ -240,23 +250,23 @@ implementation is provided that uses the contents of a static list of prices There are a few things that the registry cannot currently do, and a few things that are out of scope that it will never do. -* You will need a DNS system in order to run a fully-fledged registry. If you - are planning on using anything other than Google Cloud DNS you will need to - provide an implementation. -* You will need an invoicing system to convert the internal registry billing - events into registrar invoices using whatever accounts receivable setup you - already have. A partial implementation is provided that generates generic CSV - invoices (see `MakeBillingTablesCommand`), but you will need to integrate it - with your payments system. -* You will likely need monitoring to continuously monitor the status of the - system. Any of a large variety of tools can be used for this, or you can - write your own. -* You will need a proxy to forward traffic on EPP and WHOIS ports to the HTTPS - endpoint on App Engine, as App Engine only allows incoming traffic on - HTTP/HTTPS ports. Similarly, App Engine does not yet support IPv6, so your - proxy would have to support that as well if you need IPv6 support. Future - versions of [App Engine Flexible][flex] should provide these out of the box, - but they aren't ready yet. +* You will need a DNS system in order to run a fully-fledged registry. If you + are planning on using anything other than Google Cloud DNS you will need to + provide an implementation. +* You will need an invoicing system to convert the internal registry billing + events into registrar invoices using whatever accounts receivable setup you + already have. A partial implementation is provided that generates generic + CSV invoices (see `MakeBillingTablesCommand`), but you will need to + integrate it with your payments system. +* You will likely need monitoring to continuously monitor the status of the + system. Any of a large variety of tools can be used for this, or you can + write your own. +* You will need a proxy to forward traffic on EPP and WHOIS ports to the HTTPS + endpoint on App Engine, as App Engine only allows incoming traffic on + HTTP/HTTPS ports. Similarly, App Engine does not yet support IPv6, so your + proxy would have to support that as well if you need IPv6 support. Future + versions of [App Engine Flexible][flex] should provide these out of the box, + but they aren't ready yet. [bigquery]: https://cloud.google.com/bigquery/ [datastore]: https://cloud.google.com/datastore/docs/concepts/overview diff --git a/g3doc/app-engine-architecture.md b/g3doc/app-engine-architecture.md index 20229dbd3..6b37500ed 100644 --- a/g3doc/app-engine-architecture.md +++ b/g3doc/app-engine-architecture.md @@ -5,19 +5,19 @@ Registry project as it is implemented in App Engine. ## Services -The Domain Registry contains three -[services](https://cloud.google.com/appengine/docs/python/an-overview-of-app-engine), -which were previously called modules in earlier versions of App Engine. The -services are: default (also called front-end), backend, and tools. Each service +The Domain Registry contains three [services] +(https://cloud.google.com/appengine/docs/python/an-overview-of-app-engine), +which were previously called modules in earlier versions of App Engine. The +services are: default (also called front-end), backend, and tools. Each service runs independently in a lot of ways, including that they can be upgraded individually, their log outputs are separate, and their servers and configured scaling are separate as well. Once you have your app deployed and running, the default service can be accessed at `https://project-id.appspot.com`, substituting whatever your App Engine app -is named for "project-id". Note that that is the URL for the production -instance of your app; other environments will have the environment name appended -with a hyphen in the hostname, e.g. `https://project-id-sandbox.appspot.com`. +is named for "project-id". Note that that is the URL for the production instance +of your app; other environments will have the environment name appended with a +hyphen in the hostname, e.g. `https://project-id-sandbox.appspot.com`. The URL for the backend service is `https://backend-dot-project-id.appspot.com` and the URL for the tools service is `https://tools-dot-project-id.appspot.com`. @@ -27,32 +27,32 @@ wild-cards). ### Default service -The default service is responsible for all registrar-facing -[EPP](https://en.wikipedia.org/wiki/Extensible_Provisioning_Protocol) command +The default service is responsible for all registrar-facing [EPP] +(https://en.wikipedia.org/wiki/Extensible_Provisioning_Protocol) command traffic, all user-facing WHOIS and RDAP traffic, and the admin and registrar web -consoles, and is thus the most important service. If the service has any +consoles, and is thus the most important service. If the service has any problems and goes down or stops servicing requests in a timely manner, it will -begin to impact users immediately. Requests to the default service are handled +begin to impact users immediately. Requests to the default service are handled by the `FrontendServlet`, which provides all of the endpoints exposed in `FrontendRequestComponent`. ### Backend service The backend service is responsible for executing all regularly scheduled -background tasks (using cron) as well as all asynchronous tasks. Requests to -the backend service are handled by the `BackendServlet`, which provides all of -the endpoints exposed in `BackendRequestComponent`. These include tasks for +background tasks (using cron) as well as all asynchronous tasks. Requests to the +backend service are handled by the `BackendServlet`, which provides all of the +endpoints exposed in `BackendRequestComponent`. These include tasks for generating/exporting RDE, syncing the trademark list from TMDB, exporting backups, writing out DNS updates, handling asynchronous contact and host deletions, writing out commit logs, exporting metrics to BigQuery, and many -more. Issues in the backend service will not immediately be apparent to end +more. Issues in the backend service will not immediately be apparent to end users, but the longer it is down, the more obvious it will become that user-visible tasks such as DNS and deletion are not being handled in a timely manner. The backend service is also where all MapReduces run, which includes some of the aforementioned tasks such as RDE and asynchronous resource deletion, as well as -any one-off data migration MapReduces. Consequently, the backend service should +any one-off data migration MapReduces. Consequently, the backend service should be sized to support not just the normal ongoing DNS load but also the load incurred by MapReduces, both scheduled (such as RDE) and on-demand (asynchronous contact/host deletion). @@ -61,48 +61,48 @@ contact/host deletion). The tools service is responsible for servicing requests from the `registry_tool` command line tool, which provides administrative-level functionality for -developers and tech support employees of the registry. It is thus the least -critical of the three services. Requests to the tools service are handled by -the `ToolsServlet`, which provides all of the endpoints exposed in -`ToolsRequestComponent`. Some example functionality that this service provides +developers and tech support employees of the registry. It is thus the least +critical of the three services. Requests to the tools service are handled by the +`ToolsServlet`, which provides all of the endpoints exposed in +`ToolsRequestComponent`. Some example functionality that this service provides includes the server-side code to update premium lists, run EPP commands from the -tool, and manually modify contacts/hosts/domains/and other resources. Problems +tool, and manually modify contacts/hosts/domains/and other resources. Problems with the tools service are not visible to users. ## Task queues [Task queues](https://cloud.google.com/appengine/docs/java/taskqueue/) in App Engine provide an asynchronous way to enqueue tasks and then execute them on -some kind of schedule. There are two types of queues, push queues and pull -queues. Tasks in push queues are always executing up to some throttlable limit. +some kind of schedule. There are two types of queues, push queues and pull +queues. Tasks in push queues are always executing up to some throttlable limit. Tasks in pull queues remain there indefinitely until the queue is polled by code -that is running for some other reason. Essentially, push queues run their own -tasks while pull queues just enqueue data that is used by something else. Many -other parts of App Engine are implemented using task queues. For example, -[App Engine cron](https://cloud.google.com/appengine/docs/java/config/cron) adds -tasks to push queues at regularly scheduled intervals, and the -[MapReduce framework](https://cloud.google.com/appengine/docs/java/dataprocessing/) -adds tasks for each phase of the MapReduce algorithm. +that is running for some other reason. Essentially, push queues run their own +tasks while pull queues just enqueue data that is used by something else. Many +other parts of App Engine are implemented using task queues. For example, [App +Engine cron](https://cloud.google.com/appengine/docs/java/config/cron) adds +tasks to push queues at regularly scheduled intervals, and the [MapReduce +framework](https://cloud.google.com/appengine/docs/java/dataprocessing/) adds +tasks for each phase of the MapReduce algorithm. The Domain Registry project uses a particular pattern of paired push/pull queues -that is worth explaining in detail. Push queues are essential because App +that is worth explaining in detail. Push queues are essential because App Engine's architecture does not support long-running background processes, and so push queues are thus the fundamental building block that allows asynchronous and background execution of code that is not in response to incoming web requests. However, they also have limitations in that they do not allow batch processing -or grouping. That's where the pull queue comes in. Regularly scheduled tasks -in the push queue will, upon execution, poll the corresponding pull queue for a -specified number of tasks and execute them in a batch. This allows the code to +or grouping. That's where the pull queue comes in. Regularly scheduled tasks in +the push queue will, upon execution, poll the corresponding pull queue for a +specified number of tasks and execute them in a batch. This allows the code to execute in the background while taking advantage of batch processing. Particulars on the task queues in use by the Domain Registry project are -specified in the `queue.xml` file. Note that many push queues have a direct +specified in the `queue.xml` file. Note that many push queues have a direct one-to-one correspondence with entries in `cron.xml` because they need to be fanned-out on a per-TLD or other basis (see the Cron section below for more -explanation). The exact queue that a given cron task will use is passed as the +explanation). The exact queue that a given cron task will use is passed as the query string parameter "queue" in the url specification for the cron task. -Here are the task queues in use by the system. All are push queues unless +Here are the task queues in use by the system. All are push queues unless explicitly marked as otherwise. * `bigquery-streaming-metrics` -- Queue for metrics that are asynchronously @@ -181,245 +181,249 @@ explicitly marked as otherwise. ## Environments The domain registry codebase comes pre-configured with support for a number of -different environments, all of which are used in Google's registry system. -Other registry operators may choose to user more or fewer environments, -depending on their needs. +different environments, all of which are used in Google's registry system. Other +registry operators may choose to user more or fewer environments, depending on +their needs. -The different environments are specified in `RegistryEnvironment`. Most +The different environments are specified in `RegistryEnvironment`. Most correspond to a separate App Engine app except for `UNITTEST` and `LOCAL`, which -by their nature do not use real environments running in the cloud. The +by their nature do not use real environments running in the cloud. The recommended naming scheme for the App Engine apps that has the best possible compatibility with the codebase and thus requires the least configuration is to pick a name for the production app and then suffix it for the other -environments. E.g., if the production app is to be named 'registry-platform', +environments. E.g., if the production app is to be named 'registry-platform', then the sandbox app would be named 'registry-platform-sandbox'. The full list of environments supported out-of-the-box, in descending order from real to not, is: -* `PRODUCTION` -- The real production environment that is actually running live - TLDs. Since the Domain Registry is a shared registry platform, there need - only ever be one of these. -* `SANDBOX` -- A playground environment for external users to test commands in - without the possibility of affecting production data. This is the environment - new registrars go through - [OT&E](https://www.icann.org/resources/unthemed-pages/registry-agmt-appc-e-2001-04-26-en) - in. Sandbox is also useful as a final sanity check to push a new prospective - build to and allow it to "bake" before pushing it to production. -* `QA` -- An internal environment used by business users to play with and sign - off on new features to be released. This environment can be pushed to - frequently and is where manual testers should be spending the majority of - their time. -* `CRASH` -- Another environment similar to QA, except with no expectations of - data preservation. Crash is used for testing of backup/restore (which brings - the entire system down until it is completed) without affecting the QA - environment. -* `ALPHA` -- The developers' playground. Experimental builds are routinely - pushed here in order to test them on a real app running on App Engine. You - may end up wanting multiple environments like Alpha if you regularly - experience contention (i.e. developers being blocked from testing their code - on Alpha because others are already using it). -* `LOCAL` -- A fake environment that is used when running the app locally on a - simulated App Engine instance. -* `UNITTEST` -- A fake environment that is used in unit tests, where everything - in the App Engine stack is simulated or mocked. +* `PRODUCTION` -- The real production environment that is actually running + live TLDs. Since the Domain Registry is a shared registry platform, there + need only ever be one of these. +* `SANDBOX` -- A playground environment for external users to test commands in + without the possibility of affecting production data. This is the + environment new registrars go through [OT&E] + (https://www.icann.org/resources/unthemed-pages/registry-agmt-appc-e-2001-04-26-en) + in. Sandbox is also useful as a final sanity check to push a new prospective + build to and allow it to "bake" before pushing it to production. +* `QA` -- An internal environment used by business users to play with and sign + off on new features to be released. This environment can be pushed to + frequently and is where manual testers should be spending the majority of + their time. +* `CRASH` -- Another environment similar to QA, except with no expectations of + data preservation. Crash is used for testing of backup/restore (which brings + the entire system down until it is completed) without affecting the QA + environment. +* `ALPHA` -- The developers' playground. Experimental builds are routinely + pushed here in order to test them on a real app running on App Engine. You + may end up wanting multiple environments like Alpha if you regularly + experience contention (i.e. developers being blocked from testing their code + on Alpha because others are already using it). +* `LOCAL` -- A fake environment that is used when running the app locally on a + simulated App Engine instance. +* `UNITTEST` -- A fake environment that is used in unit tests, where + everything in the App Engine stack is simulated or mocked. ## Release process The following is a recommended release process based on Google's several years of experience running a production registry using this codebase. -1. Developers write code and associated unit tests verifying that the new code - works properly. -2. New features or potentially risky bug fixes are pushed to Alpha and tested by - the developers before being committed to the source code repository. -3. New builds are cut and first pushed to Sandbox. -4. Once a build has been running successfully in Sandbox for a day with no - errors, it can be pushed to Production. -5. Repeat once weekly, or potentially more often. +1. Developers write code and associated unit tests verifying that the new code + works properly. +2. New features or potentially risky bug fixes are pushed to Alpha and tested + by the developers before being committed to the source code repository. +3. New builds are cut and first pushed to Sandbox. +4. Once a build has been running successfully in Sandbox for a day with no + errors, it can be pushed to Production. +5. Repeat once weekly, or potentially more often. ## Cron tasks All [cron tasks](https://cloud.google.com/appengine/docs/java/config/cron) are -specified in `cron.xml` files, with one per environment. There are more tasks +specified in `cron.xml` files, with one per environment. There are more tasks that execute in Production than in other environments, because tasks like -uploading RDE dumps are only done for the live system. Cron tasks execute on -the `backend` service. +uploading RDE dumps are only done for the live system. Cron tasks execute on the +`backend` service. Most cron tasks use the `TldFanoutAction` which is accessed via the -`/_dr/cron/fanout` URL path. This action, which is run by the BackendServlet on +`/_dr/cron/fanout` URL path. This action, which is run by the BackendServlet on the backend service, fans out a given cron task for each TLD that exists in the registry system, using the queue that is specified in the `cron.xml` entry. Because some tasks may be computationally intensive and could risk spiking system latency if all start executing immediately at the same time, there is a `jitterSeconds` parameter that spreads out tasks over the given number of -seconds. This is used with DNS updates and commit log deletion. +seconds. This is used with DNS updates and commit log deletion. The reason the `TldFanoutAction` exists is that a lot of tasks need to be done -separately for each TLD, such as RDE exports and NORDN uploads. It's simpler to +separately for each TLD, such as RDE exports and NORDN uploads. It's simpler to have a single cron entry that will create tasks for all TLDs than to have to specify a separate cron task for each action for each TLD (though that is still -an option). Task queues also provide retry semantics in the event of transient -failures that a raw cron task does not. This is why there are some tasks that -do not fan out across TLDs that still use `TldFanoutAction` -- it's so that the +an option). Task queues also provide retry semantics in the event of transient +failures that a raw cron task does not. This is why there are some tasks that do +not fan out across TLDs that still use `TldFanoutAction` -- it's so that the tasks retry in the face of transient errors. The full list of URL parameters to `TldFanoutAction` that can be specified in cron.xml is: -* `endpoint` -- The path of the action that should be executed (see `web.xml`). -* `queue` -- The cron queue to enqueue tasks in. -* `forEachRealTld` -- Specifies that the task should be run in each TLD of type - `REAL`. This can be combined with `forEachTestTld`. -* `forEachTestTld` -- Specifies that the task should be run in each TLD of type - `TEST`. This can be combined with `forEachRealTld`. -* `runInEmpty` -- Specifies that the task should be run globally, i.e. just - once, rather than individually per TLD. This is provided to allow tasks to - retry. It is called "`runInEmpty`" for historical reasons. -* `excludes` -- A list of TLDs to exclude from processing. -* `jitterSeconds` -- The execution of each per-TLD task is delayed by a - different random number of seconds between zero and this max value. + +* `endpoint` -- The path of the action that should be executed (see + `web.xml`). +* `queue` -- The cron queue to enqueue tasks in. +* `forEachRealTld` -- Specifies that the task should be run in each TLD of + type `REAL`. This can be combined with `forEachTestTld`. +* `forEachTestTld` -- Specifies that the task should be run in each TLD of + type `TEST`. This can be combined with `forEachRealTld`. +* `runInEmpty` -- Specifies that the task should be run globally, i.e. just + once, rather than individually per TLD. This is provided to allow tasks to + retry. It is called "`runInEmpty`" for historical reasons. +* `excludes` -- A list of TLDs to exclude from processing. +* `jitterSeconds` -- The execution of each per-TLD task is delayed by a + different random number of seconds between zero and this max value. ## Cloud Datastore -The Domain Registry platform uses -[Cloud Datastore](https://cloud.google.com/appengine/docs/java/datastore/) as -its primary database. Cloud Datastore is a NoSQL document database that -provides automatic horizontal scaling, high performance, and high availability. -All information that is persisted to Cloud Datastore takes the form of Java -classes annotated with `@Entity` that are located in the `model` package. The -[Objectify library](https://cloud.google.com/appengine/docs/java/gettingstarted/using-datastore-objectify) +The Domain Registry platform uses [Cloud Datastore] +(https://cloud.google.com/appengine/docs/java/datastore/) as its primary +database. Cloud Datastore is a NoSQL document database that provides automatic +horizontal scaling, high performance, and high availability. All information +that is persisted to Cloud Datastore takes the form of Java classes annotated +with `@Entity` that are located in the `model` package. The [Objectify library] +(https://cloud.google.com/appengine/docs/java/gettingstarted/using-datastore-objectify) is used to persist instances of these classes in a format that Datastore understands. A brief overview of the different entity types found in the App Engine Datastore -Viewer may help administrators understand what they are seeing. Note that some +Viewer may help administrators understand what they are seeing. Note that some of these entities are part of App Engine tools that are outside of the domain registry codebase: -* `_AE_*` -- These entities are created by App Engine. -* `_ah_SESSION` -- These entities track App Engine client sessions. -* `_GAE_MR_*` -- These entities are generated by App Engine while running - MapReduces. -* `BackupStatus` -- There should only be one of these entities, used to maintain - the state of the backup process. -* `Cancellation` -- A cancellation is a special type of billing event which - represents the cancellation of another billing event such as a OneTime or - Recurring. -* `ClaimsList`, `ClaimsListShard`, and `ClaimsListSingleton` -- These entities - store the TMCH claims list, for use in trademark processing. -* `CommitLog*` -- These entities store the commit log information. -* `ContactResource` -- These hold the ICANN contact information (but not - registrar contacts, who have a separate entity type). -* `Cursor` -- We use Cursor entities to maintain state about daily processes, - remembering which dates have been processed. For instance, for the RDE export, - Cursor entities maintain the date up to which each TLD has been exported. -* `DomainApplicationIndex` -- These hold domain applications received during the - sunrise period. -* `DomainBase` -- These hold the ICANN domain information. -* `DomainRecord` -- These are used during the DNS update process. -* `EntityGroupRoot` -- There is only one EntityGroupRoot entity, which serves as - the Datastore parent of many other entities. -* `EppResourceIndex` -- These entities allow enumeration of EPP resources (such - as domains, hosts and contacts), which would otherwise be difficult to do in - Datastore. -* `ExceptionReportEntity` -- These entities are generated automatically by - ECatcher, a Google-internal logging and debugging tool. Non-Google users - should not encounter these entries. -* `ForeignKeyContactIndex`, `ForeignKeyDomainIndex`, and `ForeignKeyHostIndex` - -- These act as a unique index on contacts, domains and hosts, allowing - transactional lookup by foreign key. -* `HistoryEntry` -- A HistoryEntry is the record of a command which mutated an - EPP resource. It serves as the parent of BillingEvents and PollMessages. -* `HostRecord` -- These are used during the DNS update process. -* `HostResource` -- These hold the ICANN host information. -* `Lock` -- Lock entities are used to control access to a shared resource such - as an App Engine queue. Under ordinary circumstances, these locks will be - cleaned up automatically, and should not accumulate. -* `LogsExportCursor` -- This is a single entity which maintains the state of log - export. -* `MR-*` -- These entities are generated by the App Engine MapReduce library in - the course of running MapReduces. -* `Modification` -- A Modification is a special type of billing event which - represents the modification of a OneTime billing event. -* `OneTime` -- A OneTime is a billing event which represents a one-time charge - or credit to the client (as opposed to Recurring). -* `pipeline-*` -- These entities are also generated by the App Engine MapReduce - library. -* `PollMessage` -- PollMessages are generated by the system to notify registrars - of asynchronous responses and status changes. -* `PremiumList`, `PremiumListEntry`, and `PremiumListRevision` -- The standard - method for determining which domain names receive premium pricing is to - maintain a static list of premium names. Each PremiumList contains some number - of PremiumListRevisions, each of which in turn contains a PremiumListEntry for - each premium name. -* `RdeRevision` -- These entities are used by the RDE subsystem in the process - of generating files. -* `Recurring` -- A Recurring is a billing event which represents a recurring - charge to the client (as opposed to OneTime). -* `Registrar` -- These hold information about client registrars. -* `RegistrarContact` -- Registrars have contacts just as domains do. These are - stored in a special RegistrarContact entity. -* `RegistrarCredit` and `RegistrarCreditBalance` -- The system supports the - concept of a registrar credit balance, which is a pool of credit that the - registrar can use to offset amounts they owe. This might come from promotions, - for instance. These entities maintain registrars' balances. -* `Registry` -- These hold information about the TLDs supported by the Registry - system. -* `RegistryCursor` -- These entities are the predecessor to the Cursor - entities. We are no longer using them, and will be deleting them soon. -* `ReservedList` -- Each ReservedList entity represents an entire list of - reserved names which cannot be registered. Each TLD can have one or more - attached reserved lists. -* `ServerSecret` -- this is a single entity containing the secret numbers used - for generating tokens such as XSRF tokens. -* `SignedMarkRevocationList` -- The entities together contain the Signed Mark - Data Revocation List file downloaded from the TMCH MarksDB each day. Each - entity contains up to 10,000 rows of the file, so depending on the size of the - file, there will be some handful of entities. -* `TmchCrl` -- This is a single entity containing ICANN's TMCH CA Certificate - Revocation List. +* `_AE_*` -- These entities are created by App Engine. +* `_ah_SESSION` -- These entities track App Engine client sessions. +* `_GAE_MR_*` -- These entities are generated by App Engine while running + MapReduces. +* `BackupStatus` -- There should only be one of these entities, used to + maintain the state of the backup process. +* `Cancellation` -- A cancellation is a special type of billing event which + represents the cancellation of another billing event such as a OneTime or + Recurring. +* `ClaimsList`, `ClaimsListShard`, and `ClaimsListSingleton` -- These entities + store the TMCH claims list, for use in trademark processing. +* `CommitLog*` -- These entities store the commit log information. +* `ContactResource` -- These hold the ICANN contact information (but not + registrar contacts, who have a separate entity type). +* `Cursor` -- We use Cursor entities to maintain state about daily processes, + remembering which dates have been processed. For instance, for the RDE + export, Cursor entities maintain the date up to which each TLD has been + exported. +* `DomainApplicationIndex` -- These hold domain applications received during + the sunrise period. +* `DomainBase` -- These hold the ICANN domain information. +* `DomainRecord` -- These are used during the DNS update process. +* `EntityGroupRoot` -- There is only one EntityGroupRoot entity, which serves + as the Datastore parent of many other entities. +* `EppResourceIndex` -- These entities allow enumeration of EPP resources + (such as domains, hosts and contacts), which would otherwise be difficult to + do in Datastore. +* `ExceptionReportEntity` -- These entities are generated automatically by + ECatcher, a Google-internal logging and debugging tool. Non-Google users + should not encounter these entries. +* `ForeignKeyContactIndex`, `ForeignKeyDomainIndex`, and + `ForeignKeyHostIndex` -- These act as a unique index on contacts, domains + and hosts, allowing transactional lookup by foreign key. +* `HistoryEntry` -- A HistoryEntry is the record of a command which mutated an + EPP resource. It serves as the parent of BillingEvents and PollMessages. +* `HostRecord` -- These are used during the DNS update process. +* `HostResource` -- These hold the ICANN host information. +* `Lock` -- Lock entities are used to control access to a shared resource such + as an App Engine queue. Under ordinary circumstances, these locks will be + cleaned up automatically, and should not accumulate. +* `LogsExportCursor` -- This is a single entity which maintains the state of + log export. +* `MR-*` -- These entities are generated by the App Engine MapReduce library + in the course of running MapReduces. +* `Modification` -- A Modification is a special type of billing event which + represents the modification of a OneTime billing event. +* `OneTime` -- A OneTime is a billing event which represents a one-time charge + or credit to the client (as opposed to Recurring). +* `pipeline-*` -- These entities are also generated by the App Engine + MapReduce library. +* `PollMessage` -- PollMessages are generated by the system to notify + registrars of asynchronous responses and status changes. +* `PremiumList`, `PremiumListEntry`, and `PremiumListRevision` -- The standard + method for determining which domain names receive premium pricing is to + maintain a static list of premium names. Each PremiumList contains some + number of PremiumListRevisions, each of which in turn contains a + PremiumListEntry for each premium name. +* `RdeRevision` -- These entities are used by the RDE subsystem in the process + of generating files. +* `Recurring` -- A Recurring is a billing event which represents a recurring + charge to the client (as opposed to OneTime). +* `Registrar` -- These hold information about client registrars. +* `RegistrarContact` -- Registrars have contacts just as domains do. These are + stored in a special RegistrarContact entity. +* `RegistrarCredit` and `RegistrarCreditBalance` -- The system supports the + concept of a registrar credit balance, which is a pool of credit that the + registrar can use to offset amounts they owe. This might come from + promotions, for instance. These entities maintain registrars' balances. +* `Registry` -- These hold information about the TLDs supported by the + Registry system. +* `RegistryCursor` -- These entities are the predecessor to the Cursor + entities. We are no longer using them, and will be deleting them soon. +* `ReservedList` -- Each ReservedList entity represents an entire list of + reserved names which cannot be registered. Each TLD can have one or more + attached reserved lists. +* `ServerSecret` -- this is a single entity containing the secret numbers used + for generating tokens such as XSRF tokens. +* `SignedMarkRevocationList` -- The entities together contain the Signed Mark + Data Revocation List file downloaded from the TMCH MarksDB each day. Each + entity contains up to 10,000 rows of the file, so depending on the size of + the file, there will be some handful of entities. +* `TmchCrl` -- This is a single entity containing ICANN's TMCH CA Certificate + Revocation List. ## Cloud Storage buckets -The Domain Registry platform uses -[Cloud Storage](https://cloud.google.com/storage/) for bulk storage of large -flat files that aren't suitable for Datastore. These files include backups, RDE -exports, Datastore snapshots (for ingestion into BigQuery), and reports. Each -bucket name must be unique across all of Google Cloud Storage, so we use the -common recommended pattern of prefixing all buckets with the name of the App -Engine app (which is itself globally unique). Most of the bucket names are -configurable, but the defaults are as follows, with PROJECT standing in as a -placeholder for the App Engine app name: +The Domain Registry platform uses [Cloud Storage] +(https://cloud.google.com/storage/) for bulk storage of large flat files that +aren't suitable for Datastore. These files include backups, RDE exports, +Datastore snapshots (for ingestion into BigQuery), and reports. Each bucket name +must be unique across all of Google Cloud Storage, so we use the common +recommended pattern of prefixing all buckets with the name of the App Engine app +(which is itself globally unique). Most of the bucket names are configurable, +but the defaults are as follows, with PROJECT standing in as a placeholder for +the App Engine app name: -* `PROJECT-billing` -- Monthly invoice files for each registrar. -* `PROJECT-commits` -- Daily exports of commit logs that are needed for - potentially performing a restore. -* `PROJECT-domain-lists` -- Daily exports of all registered domain names per - TLD. -* `PROJECT-gcs-logs` -- This bucket is used at Google to store the GCS access - logs and storage data. This bucket is not required by the Registry system, - but can provide useful logging information. For instructions on setup, see - the - [Cloud Storage documentation](https://cloud.google.com/storage/docs/access-logs). -* `PROJECT-icann-brda` -- This bucket contains the weekly ICANN BRDA files. - There is no lifecycle expiration; we keep a history of all the files. This - bucket must exist for the BRDA process to function. -* `PROJECT-icann-zfa` -- This bucket contains the most recent ICANN ZFA - files. No lifecycle is needed, because the files are overwritten each time. -* `PROJECT-rde` -- This bucket contains RDE exports, which should then be - regularly uploaded to the escrow provider. Lifecycle is set to 90 days. The - bucket must exist. -* `PROJECT-reporting` -- Contains monthly ICANN reporting files. -* `PROJECT-snapshots` -- Contains daily exports of Datastore entities of types - defined in `ExportConstants.java`. These are imported into BigQuery daily to - allow for in-depth querying. -* `PROJECT.appspot.com` -- Temporary MapReduce files are stored here. By - default, the App Engine MapReduce library places its temporary files in a - bucket named {project}.appspot.com. This bucket must exist. To keep temporary - files from building up, a 90-day or 180-day lifecycle should be applied to the - bucket, depending on how long you want to be able to go back and debug - MapReduce problems. At 30 GB per day of generate temporary files, this bucket - may be the largest consumer of storage, so only save what you actually use. +* `PROJECT-billing` -- Monthly invoice files for each registrar. +* `PROJECT-commits` -- Daily exports of commit logs that are needed for + potentially performing a restore. +* `PROJECT-domain-lists` -- Daily exports of all registered domain names per + TLD. +* `PROJECT-gcs-logs` -- This bucket is used at Google to store the GCS access + logs and storage data. This bucket is not required by the Registry system, + but can provide useful logging information. For instructions on setup, see + the [Cloud Storage documentation] + (https://cloud.google.com/storage/docs/access-logs). +* `PROJECT-icann-brda` -- This bucket contains the weekly ICANN BRDA files. + There is no lifecycle expiration; we keep a history of all the files. This + bucket must exist for the BRDA process to function. +* `PROJECT-icann-zfa` -- This bucket contains the most recent ICANN ZFA files. + No lifecycle is needed, because the files are overwritten each time. +* `PROJECT-rde` -- This bucket contains RDE exports, which should then be + regularly uploaded to the escrow provider. Lifecycle is set to 90 days. The + bucket must exist. +* `PROJECT-reporting` -- Contains monthly ICANN reporting files. +* `PROJECT-snapshots` -- Contains daily exports of Datastore entities of types + defined in `ExportConstants.java`. These are imported into BigQuery daily to + allow for in-depth querying. +* `PROJECT.appspot.com` -- Temporary MapReduce files are stored here. By + default, the App Engine MapReduce library places its temporary files in a + bucket named {project}.appspot.com. This bucket must exist. To keep + temporary files from building up, a 90-day or 180-day lifecycle should be + applied to the bucket, depending on how long you want to be able to go back + and debug MapReduce problems. At 30 GB per day of generate temporary files, + this bucket may be the largest consumer of storage, so only save what you + actually use. ## Commit logs diff --git a/g3doc/configuration.md b/g3doc/configuration.md index ab7b288b2..6070e695b 100644 --- a/g3doc/configuration.md +++ b/g3doc/configuration.md @@ -1,19 +1,19 @@ # Configuration There are multiple different kinds of configuration that go into getting a -working registry system up and running. Broadly speaking, configuration works -in two ways -- globally, for the entire sytem, and per-TLD. Global -configuration is managed by editing code and deploying a new version, whereas -per-TLD configuration is data that lives in Datastore in `Registry` entities, -and is updated by running `registry_tool` commands without having to deploy a -new version. +working registry system up and running. Broadly speaking, configuration works in +two ways -- globally, for the entire sytem, and per-TLD. Global configuration is +managed by editing code and deploying a new version, whereas per-TLD +configuration is data that lives in Datastore in `Registry` entities, and is +updated by running `registry_tool` commands without having to deploy a new +version. ## Environments Before getting into the details of configuration, it's important to note that a -lot of configuration is environment-dependent. It is common to see `switch` +lot of configuration is environment-dependent. It is common to see `switch` statements that operate on the current `RegistryEnvironment`, and return -different values for different environments. This is especially pronounced in +different values for different environments. This is especially pronounced in the `UNITTEST` and `LOCAL` environments, which don't run on App Engine at all. As an example, some timeouts may be long in production and short in unit tests. @@ -27,34 +27,34 @@ thoroughly documented in the [App Engine configuration docs][app-engine-config]. The main files of note that come pre-configured along with the domain registry are: -* `cron.xml` -- Configuration of cronjobs -* `web.xml` -- Configuration of URL paths on the webserver -* `appengine-web.xml` -- Overall App Engine settings including number and type - of instances -* `datastore-indexes.xml` -- Configuration of entity indexes in Datastore -* `queue.xml` -- Configuration of App Engine task queues -* `application.xml` -- Configuration of the application name and its services +* `cron.xml` -- Configuration of cronjobs +* `web.xml` -- Configuration of URL paths on the webserver +* `appengine-web.xml` -- Overall App Engine settings including number and type + of instances +* `datastore-indexes.xml` -- Configuration of entity indexes in Datastore +* `queue.xml` -- Configuration of App Engine task queues +* `application.xml` -- Configuration of the application name and its services Cron, web, and queue are covered in more detail in the "App Engine architecture" doc, and the rest are covered in the general App Engine documentation. If you are not writing new code to implement custom features, is unlikely that you will need to make any modifications beyond simple changes to -`application.xml` and `appengine-web.xml`. If you are writing new features, -it's likely you'll need to add cronjobs, URL paths, Datastore indexes, and task +`application.xml` and `appengine-web.xml`. If you are writing new features, it's +likely you'll need to add cronjobs, URL paths, Datastore indexes, and task queues, and thus edit those associated XML files. ## Global configuration There are two different mechanisms by which global configuration is managed: -`RegistryConfig` (the old way) and `ConfigModule` (the new way). Ideally there +`RegistryConfig` (the old way) and `ConfigModule` (the new way). Ideally there would just be one, but the required code cleanup that hasn't been completed yet. If you are adding new options, prefer adding them to `ConfigModule`. **`RegistryConfig`** is an interface, of which you write an implementing class -containing the configuration values. `RegistryConfigLoader` is the class that +containing the configuration values. `RegistryConfigLoader` is the class that provides the instance of `RegistryConfig`, and defaults to returning -`ProductionRegistryConfigExample`. In order to create a configuration specific +`ProductionRegistryConfigExample`. In order to create a configuration specific to your registry, we recommend copying the `ProductionRegistryConfigExample` class to a new class that will not be shared publicly, setting the `com.google.domain.registry.config` system property in `appengine-web.xml` to @@ -64,16 +64,16 @@ configuration options. The `RegistryConfig` class has documentation on all of the methods that should be sufficient to explain what each option is, and -`ProductionRegistryConfigExample` provides an example value for each one. Some +`ProductionRegistryConfigExample` provides an example value for each one. Some example configuration options in this interface include the App Engine project ID, the number of days to retain commit logs, the names of various Cloud Storage bucket names, and URLs for some required services both external and internal. **`ConfigModule`** is a Dagger module that provides injectable configuration options (some of which come from `RegistryConfig` above, but most of which do -not). This is preferred over `RegistryConfig` for new configuration options +not). This is preferred over `RegistryConfig` for new configuration options because being able to inject configuration options is a nicer pattern that makes -for cleaner code. Some configuration options that can be changed in this class +for cleaner code. Some configuration options that can be changed in this class include timeout lengths and buffer sizes for various tasks, email addresses and URLs to use for various services, more Cloud Storage bucket names, and WHOIS disclaimer text. @@ -83,39 +83,39 @@ disclaimer text. Some configuration values, such as PGP private keys, are so sensitive that they should not be written in code as per the configuration methods above, as that would pose too high a risk of them accidentally being leaked, e.g. in a source -control mishap. We use a secret store to persist these values in a secure +control mishap. We use a secret store to persist these values in a secure manner, and abstract access to them using the `Keyring` interface. The `Keyring` interface contains methods for all sensitive configuration values, which are primarily credentials used to access various ICANN and ICANN- -affiliated services (such as RDE). These values are only needed for real -production registries and PDT environments. If you are just playing around with +affiliated services (such as RDE). These values are only needed for real +production registries and PDT environments. If you are just playing around with the platform at first, it is OK to put off defining these values until -necessary. To that end, a `DummyKeyringModule` is included that simply provides -an `InMemoryKeyring` populated with dummy values for all secret keys. This +necessary. To that end, a `DummyKeyringModule` is included that simply provides +an `InMemoryKeyring` populated with dummy values for all secret keys. This allows the codebase to compile and run, but of course any actions that attempt to connect to external services will fail because none of the keys are real. To configure a production registry system, you will need to write a replacement module for `DummyKeyringModule` that loads the credentials in a secure way, and provides them using either an instance of `InMemoryKeyring` or your own custom -implementation of `Keyring`. You then need to replace all usages of +implementation of `Keyring`. You then need to replace all usages of `DummyKeyringModule` with your own module in all of the per-service components -in which it is referenced. The functions in `PgpHelper` will likely prove -useful for loading keys stored in PGP format into the PGP key classes that -you'll need to provide from `Keyring`, and you can see examples of them in -action in `DummyKeyringModule`. +in which it is referenced. The functions in `PgpHelper` will likely prove useful +for loading keys stored in PGP format into the PGP key classes that you'll need +to provide from `Keyring`, and you can see examples of them in action in +`DummyKeyringModule`. ## Per-TLD configuration `Registry` entities, which are persisted to Datastore, are used for per-TLD -configuration. They contain any kind of configuration that is specific to a -TLD, such as the create/renew price of a domain name, the pricing engine +configuration. They contain any kind of configuration that is specific to a TLD, +such as the create/renew price of a domain name, the pricing engine implementation, the DNS writer implementation, whether escrow exports are -enabled, the default currency, the reserved label lists, and more. The -`update_tld` command in `registry_tool` is used to set all of these options. -See the "Registry tool" documentation for more information, as well as the -command-line help for the `update_tld` command. Unlike global configuration +enabled, the default currency, the reserved label lists, and more. The +`update_tld` command in `registry_tool` is used to set all of these options. See +the "Registry tool" documentation for more information, as well as the +command-line help for the `update_tld` command. Unlike global configuration above, per-TLD configuration options are stored as data in the running system, and thus do not require code pushes to update. diff --git a/g3doc/install.md b/g3doc/install.md index 4712b280c..eedb04b9c 100644 --- a/g3doc/install.md +++ b/g3doc/install.md @@ -5,25 +5,27 @@ working running instance. ## Prerequisites -* A recent version of the -[Java 7 JDK](http://www.oracle.com/technetwork/java/javase/downloads/jdk7-downloads-1880260.html) -(note that Java 8 support should be coming to App Engine soon). -* [Bazel](http://bazel.io/), which is the buld system that -the Domain Registry project uses. The minimum required version is 0.3.1. -* [Google App Engine SDK for Java](https://cloud.google.com/appengine/downloads#Google_App_Engine_SDK_for_Java), -especially `appcfg`, which is a command-line tool that runs locally that is used -to communicate with the App Engine cloud. -* [Create an application](https://cloud.google.com/appengine/docs/java/quickstart) - on App Engine to deploy to, and set up `appcfg` to connect to it. +* A recent version of the [Java 7 JDK] + (http://www.oracle.com/technetwork/java/javase/downloads/jdk7-downloads-1880260.html) + (note that Java 8 support should be coming to App Engine soon). +* [Bazel](http://bazel.io/), which is the buld system that the Domain Registry + project uses. The minimum required version is 0.3.1. +* [Google App Engine SDK for Java] + (https://cloud.google.com/appengine/downloads#Google_App_Engine_SDK_for_Java), + especially `appcfg`, which is a command-line tool that runs locally that is + used to communicate with the App Engine cloud. +* [Create an application] + (https://cloud.google.com/appengine/docs/java/quickstart) on App Engine to + deploy to, and set up `appcfg` to connect to it. ## Downloading the code -Start off by grabbing the latest version from the -[Domain Registry project on GitHub](https://github.com/google/domain-registry). -This can be done either by cloning the Git repo (if you expect to make code -changes to contribute back), or simply by downloading the latest release as a -zip file. This guide will cover cloning from Git, but should work almost -identically for downloading the zip file. +Start off by grabbing the latest version from the [Domain Registry project on +GitHub](https://github.com/google/domain-registry). This can be done either by +cloning the Git repo (if you expect to make code changes to contribute back), or +simply by downloading the latest release as a zip file. This guide will cover +cloning from Git, but should work almost identically for downloading the zip +file. $ git clone git@github.com:google/domain-registry.git Cloning into 'domain-registry'... @@ -36,19 +38,19 @@ identically for downloading the zip file. The most important directories are: -* `docs` -- the documentation (including this install guide) -* `java/google/registry` -- all of the source code of the main project -* `javatests/google/registry` -- all of the tests for the project -* `python` -- Some Python reporting scripts -* `scripts` -- Scripts for configuring development environments +* `docs` -- the documentation (including this install guide) +* `java/google/registry` -- all of the source code of the main project +* `javatests/google/registry` -- all of the tests for the project +* `python` -- Some Python reporting scripts +* `scripts` -- Scripts for configuring development environments Everything else, especially `third_party`, contains dependencies that are used by the project. ## Building and verifying the code -The first step is to verify that the project successfully builds. This will -also download and install dependencies. +The first step is to verify that the project successfully builds. This will also +download and install dependencies. $ bazel --batch build //java{,tests}/google/registry/... INFO: Found 584 targets... @@ -56,7 +58,7 @@ also download and install dependencies. INFO: Elapsed time: 124.433s, Critical Path: 116.92s There may be some warnings thrown, but if there are no errors, then you are good -to go. Next, run the tests to verify that everything works properly. The tests +to go. Next, run the tests to verify that everything works properly. The tests can be pretty resource intensive, so experiment with different values of parameters to optimize between low running time and not slowing down your computer too badly. @@ -68,10 +70,10 @@ computer too badly. ## Running a development instance locally `RegistryTestServer` is a lightweight test server for the registry that is -suitable for running locally for development. It uses local versions of all -Google Cloud Platform dependencies, when available. Correspondingly, its +suitable for running locally for development. It uses local versions of all +Google Cloud Platform dependencies, when available. Correspondingly, its functionality is limited compared to a Domain Registry instance running on an -actual App Engine instance. To see its command-line parameters, run: +actual App Engine instance. To see its command-line parameters, run: $ bazel run //javatests/google/registry/server -- --help @@ -86,13 +88,13 @@ http://localhost:8080/registrar . ## Deploying the code You are going to need to configure a variety of things before a working -installation can be deployed (see the Configuration guide for that). It's +installation can be deployed (see the Configuration guide for that). It's recommended to at least confirm that the default version of the code can be pushed at all first before diving into that, with the expectation that things won't work properly until they are configured. -All of the [EAR](https://en.wikipedia.org/wiki/EAR_(file_format)) and -[WAR](https://en.wikipedia.org/wiki/WAR_(file_format)) files for the different +All of the [EAR](https://en.wikipedia.org/wiki/EAR_\(file_format\)) and [WAR] +(https://en.wikipedia.org/wiki/WAR_\(file_format\)) files for the different environments, which were built in the previous step, are outputted to the `bazel-genfiles` directory as follows: @@ -115,7 +117,8 @@ an environment in the file name), whereas there is one WAR file per service per environment, with there being three services in total: default, backend, and tools. -Then, use `appcfg` to [deploy the WAR files](https://cloud.google.com/appengine/docs/java/tools/uploadinganapp): +Then, use `appcfg` to [deploy the WAR files] +(https://cloud.google.com/appengine/docs/java/tools/uploadinganapp): $ cd /path/to/downloaded/appengine/app $ /path/to/appcfg.sh update /path/to/registry_default.war @@ -126,15 +129,15 @@ Then, use `appcfg` to [deploy the WAR files](https://cloud.google.com/appengine/ Once the code is deployed, the next step is to play around with creating some entities in the registry, including a TLD, a registrar, a domain, a contact, and -a host. Note: Do this on a non-production environment! All commands below use +a host. Note: Do this on a non-production environment! All commands below use `registry_tool` to interact with the running registry system; see the -documentation on `registry_tool` for additional information on it. We'll assume +documentation on `registry_tool` for additional information on it. We'll assume that all commands below are running in the `alpha` environment; if you named your environment differently, then use that everywhere that `alpha` appears. ### Create a TLD -Pick the name of a TLD to create. For the purposes of this example we'll use +Pick the name of a TLD to create. For the purposes of this example we'll use "example", which conveniently happens to be an ICANN reserved string, meaning it'll never be created for real on the Internet at large. @@ -144,25 +147,25 @@ it'll never be created for real on the Internet at large. Perform this command? (y/N): y Updated 1 entities. -The name of the TLD is the main parameter passed to the command. The initial -TLD state is set here to general availability, bypassing sunrise and landrush, -so that domain names can be created immediately in the following steps. The TLD +The name of the TLD is the main parameter passed to the command. The initial TLD +state is set here to general availability, bypassing sunrise and landrush, so +that domain names can be created immediately in the following steps. The TLD type is set to `TEST` (the other alternative being `REAL`) for obvious reasons. `roid_suffix` is the suffix that will be used for repository ids of domains on the TLD -- it must be all uppercase and a maximum of eight ASCII characters. -ICANN -[recommends](https://www.icann.org/resources/pages/correction-non-compliant-roids-2015-08-26-en) -a unique ROID suffix per TLD. The easiest way to come up with one is to simply +ICANN [recommends] +(https://www.icann.org/resources/pages/correction-non-compliant-roids-2015-08-26-en) +a unique ROID suffix per TLD. The easiest way to come up with one is to simply use the entire uppercased TLD string if it is eight characters or fewer, or -abbreviate it in some sensible way down to eight if it is longer. The full repo -id of a domain resource is a hex string followed by the suffix, -e.g. `12F7CDF3-EXAMPLE` for our example TLD. +abbreviate it in some sensible way down to eight if it is longer. The full repo +id of a domain resource is a hex string followed by the suffix, e.g. +`12F7CDF3-EXAMPLE` for our example TLD. ### Create a registrar Now we need to create a registrar and give it access to operate on the example -TLD. For the purposes of our example we'll name the registrar "Acme". +TLD. For the purposes of our example we'll name the registrar "Acme". $ registry_tool -e alpha create_registrar acme --name 'ACME Corp' \ --registrar_type TEST --password hunter2 \ @@ -175,27 +178,27 @@ TLD. For the purposes of our example we'll name the registrar "Acme". support it. In the command above, "acme" is the internal registrar id that is the primary -key used to refer to the registrar. The `name` is the display name that is used -less often, primarily in user interfaces. We again set the type of the resource -here to `TEST`. The `password` is the EPP password that the registrar uses to -log in with. The `icann_referral_email` is the email address associated with -the initial creation of the registrar -- note that the registrar cannot change -it later. The address fields are self-explanatory (note that other parameters -are available for international addresses). The `allowed_tlds` parameter is a +key used to refer to the registrar. The `name` is the display name that is used +less often, primarily in user interfaces. We again set the type of the resource +here to `TEST`. The `password` is the EPP password that the registrar uses to +log in with. The `icann_referral_email` is the email address associated with the +initial creation of the registrar -- note that the registrar cannot change it +later. The address fields are self-explanatory (note that other parameters are +available for international addresses). The `allowed_tlds` parameter is a comma-delimited list of TLDs that the registrar has access to, and here is set to the example TLD. ### Create a contact Now we want to create a contact, as a contact is required before a domain can be -created. Contacts can be used on any number of domains across any number of +created. Contacts can be used on any number of domains across any number of TLDs, and contain the information on who owns or provides technical support for -a TLD. These details will appear in WHOIS queries. Note the `-c` parameter, +a TLD. These details will appear in WHOIS queries. Note the `-c` parameter, which stands for client identifier: This is used on most `registry_tool` commands, and is used to specify the id of the registrar that the command will -be executed using. Contact, domain, and host creation all work by constructing +be executed using. Contact, domain, and host creation all work by constructing an EPP message that is sent to the registry, and EPP commands need to run under -the context of a registrar. The "acme" registrar that was created above is used +the context of a registrar. The "acme" registrar that was created above is used for this purpose. $ registry_tool -e alpha create_contact -c acme --id abcd1234 \ @@ -204,24 +207,24 @@ for this purpose. [ ... snip EPP response ... ] The `id` is the contact id, and is referenced elsewhere in the system (e.g. when -a domain is created and the admin contact is specified). The `name` is the +a domain is created and the admin contact is specified). The `name` is the display name of the contact, which is usually the name of a company or of a -person. Again, the address fields are required, along with an `email`. +person. Again, the address fields are required, along with an `email`. ### Create a host Hosts are used to specify the IP addresses (either v4 or v6) that are associated -with a given nameserver. Note that hosts may either be in-bailiwick (on a TLD -that this registry runs) or out-of-bailiwick. In-bailiwick hosts may +with a given nameserver. Note that hosts may either be in-bailiwick (on a TLD +that this registry runs) or out-of-bailiwick. In-bailiwick hosts may additionally be subordinate (a subdomain of a domain name that is on this -registry). Let's create an out-of-bailiwick nameserver, which is the simplest +registry). Let's create an out-of-bailiwick nameserver, which is the simplest type. $ my_registry_tool -e alpha create_host -c acme --host ns1.google.com [ ... snip EPP response ... ] Note that hosts are required to have IP addresses if they are subordinate, and -must not have IP addresses if they are not subordinate. Use the `--addresses` +must not have IP addresses if they are not subordinate. Use the `--addresses` parameter to set the IP addresses on a host, passing in a comma-delimited list of IP addresses in either IPv4 or IPv6 format. @@ -236,7 +239,7 @@ and host. [ ... snip EPP response ... ] Note how the same contact id (from above) is used for the administrative, -technical, and registrant contact. This is quite common on domain names. +technical, and registrant contact. This is quite common on domain names. To verify that everything worked, let's query the WHOIS information for fake.example: diff --git a/g3doc/registry-tool.md b/g3doc/registry-tool.md index 9a701b50e..52998f9e1 100644 --- a/g3doc/registry-tool.md +++ b/g3doc/registry-tool.md @@ -1,11 +1,11 @@ # Registry tool The registry tool is a command-line registry administration tool that is invoked -using the `registry_tool` command. It has the ability to view and change a -large number of things in a running domain registry environment, including -creating registrars, updating premium and reserved lists, running an EPP command -from a given XML file, and performing various backend tasks like re-running RDE -if the most recent export failed. Its code lives inside the tools package +using the `registry_tool` command. It has the ability to view and change a large +number of things in a running domain registry environment, including creating +registrars, updating premium and reserved lists, running an EPP command from a +given XML file, and performing various backend tasks like re-running RDE if the +most recent export failed. Its code lives inside the tools package (`java/google/registry/tools`), and is compiled by building the `registry_tool` target in the Bazel BUILD file in that package. @@ -15,11 +15,11 @@ To build the tool and display its command-line help, execute this command: For future invocations you should alias the compiled binary in the `bazel-genfiles/java/google/registry` directory or add it to your path so that -you can run it more easily. The rest of this guide assumes that it has been +you can run it more easily. The rest of this guide assumes that it has been aliased to `registry_tool`. The registry tool is always called with a specific environment to run in using -the -e parameter. This looks like: +the -e parameter. This looks like: $ registry_tool -e production {command name} {command parameters} @@ -37,7 +37,7 @@ There are actually two separate tools, `gtech_tool`, which is a collection of lower impact commands intended to be used by tech support personnel, and `registry_tool`, which is a superset of `gtech_tool` that contains additional commands that are potentially more destructive and can change more aspects of -the system. A full list of `gtech_tool` commands can be found in +the system. A full list of `gtech_tool` commands can be found in `GtechTool.java`, and the additional commands that only `registry_tool` has access to are in `RegistryTool.java`. @@ -47,7 +47,7 @@ There are two broad ways that commands are implemented: some that send requests to `ToolsServlet` to execute the action on the server (these commands implement `ServerSideCommand`), and others that execute the command locally using the [Remote API](https://cloud.google.com/appengine/docs/java/tools/remoteapi) -(these commands implement `RemoteApiCommand`). Server-side commands take more +(these commands implement `RemoteApiCommand`). Server-side commands take more work to implement because they require both a client and a server-side component, e.g. `CreatePremiumListCommand.java` and `CreatePremiumListAction.java` respectively for creating a premium list. @@ -56,35 +56,36 @@ Engine, including running a large MapReduce, because they execute on the tools service in the App Engine cloud. Local commands, by contrast, are easier to implement, because there is only a -local component to write, but they aren't as powerful. A general rule of thumb +local component to write, but they aren't as powerful. A general rule of thumb for making this determination is to use a local command if possible, or a server-side command otherwise. ## Common tool patterns All tools ultimately implement the `Command` interface located in the `tools` -package. If you use an IDE such as Eclipse to view the type hierarchy of that +package. If you use an IDE such as Eclipse to view the type hierarchy of that interface, you'll see all of the commands that exist, as well as how a lot of them are grouped using sub-interfaces or abstract classes that provide -additional functionality. The most common patterns that are used by a large +additional functionality. The most common patterns that are used by a large number of other tools are: -* **`BigqueryCommand`** -- Provides a connection to BigQuery for tools that need - it. -* **`ConfirmingCommand`** -- Provides the methods `prompt()` and `execute()` to - override. `prompt()` outputs a message (usually what the command is going to - do) and prompts the user to confirm execution of the command, and then - `execute()` actually does it. -* **`EppToolCommand`** -- Commands that work by executing EPP commands against - the server, usually by filling in a template with parameters that were passed - on the command-line. -* **`MutatingEppToolCommand`** -- A sub-class of `EppToolCommand` that provides - a `--dry_run` flag, that, if passed, will display the output from the server - of what the command would've done without actually committing those changes. -* **`GetEppResourceCommand`** -- Gets individual EPP resources from the server - and outputs them. -* **`ListObjectsCommand`** -- Lists all objects of a specific type from the - server and outputs them. -* **`MutatingCommand`** -- Provides a facility to create or update entities in - Datastore, and uses a diff algorithm to display the changes that will be made - before committing them. +* **`BigqueryCommand`** -- Provides a connection to BigQuery for tools that + need it. +* **`ConfirmingCommand`** -- Provides the methods `prompt()` and `execute()` + to override. `prompt()` outputs a message (usually what the command is going + to do) and prompts the user to confirm execution of the command, and then + `execute()` actually does it. +* **`EppToolCommand`** -- Commands that work by executing EPP commands against + the server, usually by filling in a template with parameters that were + passed on the command-line. +* **`MutatingEppToolCommand`** -- A sub-class of `EppToolCommand` that + provides a `--dry_run` flag, that, if passed, will display the output from + the server of what the command would've done without actually committing + those changes. +* **`GetEppResourceCommand`** -- Gets individual EPP resources from the server + and outputs them. +* **`ListObjectsCommand`** -- Lists all objects of a specific type from the + server and outputs them. +* **`MutatingCommand`** -- Provides a facility to create or update entities in + Datastore, and uses a diff algorithm to display the changes that will be + made before committing them. From 5fca35a8eb7feff3d5633f029f6e04e9afcd0d5e Mon Sep 17 00:00:00 2001 From: mcilwain Date: Mon, 12 Sep 2016 11:09:26 -0700 Subject: [PATCH 20/31] Move public Markdown documentation to a subdirectory ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=132894271 --- {g3doc => docs}/app-engine-architecture.md | 0 {g3doc => docs}/code-structure.md | 0 {g3doc => docs}/configuration.md | 0 {g3doc => docs}/developing.md | 0 {g3doc => docs}/extension-points.md | 0 {g3doc => docs}/install.md | 0 {g3doc => docs}/registry-tool.md | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename {g3doc => docs}/app-engine-architecture.md (100%) rename {g3doc => docs}/code-structure.md (100%) rename {g3doc => docs}/configuration.md (100%) rename {g3doc => docs}/developing.md (100%) rename {g3doc => docs}/extension-points.md (100%) rename {g3doc => docs}/install.md (100%) rename {g3doc => docs}/registry-tool.md (100%) diff --git a/g3doc/app-engine-architecture.md b/docs/app-engine-architecture.md similarity index 100% rename from g3doc/app-engine-architecture.md rename to docs/app-engine-architecture.md diff --git a/g3doc/code-structure.md b/docs/code-structure.md similarity index 100% rename from g3doc/code-structure.md rename to docs/code-structure.md diff --git a/g3doc/configuration.md b/docs/configuration.md similarity index 100% rename from g3doc/configuration.md rename to docs/configuration.md diff --git a/g3doc/developing.md b/docs/developing.md similarity index 100% rename from g3doc/developing.md rename to docs/developing.md diff --git a/g3doc/extension-points.md b/docs/extension-points.md similarity index 100% rename from g3doc/extension-points.md rename to docs/extension-points.md diff --git a/g3doc/install.md b/docs/install.md similarity index 100% rename from g3doc/install.md rename to docs/install.md diff --git a/g3doc/registry-tool.md b/docs/registry-tool.md similarity index 100% rename from g3doc/registry-tool.md rename to docs/registry-tool.md From 68cdd04124603b813bb19a3953500d55101ed3b7 Mon Sep 17 00:00:00 2001 From: cgoldfeder Date: Tue, 13 Sep 2016 07:53:50 -0700 Subject: [PATCH 21/31] Duplicate EppExceptions outside of the flow hierarchy By duplicating rather than moving them, I can keep both versions around while I port the flows over to the new flat form. I could have made these g4 moves from the original sources, but there's barely anything in these files and it didn't really seem like it was adding anything useful. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=132999387 --- .../AddRemoveSameValueEppException.java | 24 ++++++++++++ .../AlreadyPendingTransferException.java | 24 ++++++++++++ .../BadCommandForRegistryPhaseException.java | 24 ++++++++++++ ...ssingTransferRequestAuthInfoException.java | 24 ++++++++++++ .../NoTransferHistoryToQueryException.java | 24 ++++++++++++ .../NotAuthorizedToViewTransferException.java | 24 ++++++++++++ .../NotPendingTransferException.java | 24 ++++++++++++ .../NotTransferInitiatorException.java | 24 ++++++++++++ .../ObjectAlreadySponsoredException.java | 24 ++++++++++++ .../OnlyToolCanPassMetadataException.java | 24 ++++++++++++ .../ResourceAlreadyExistsException.java | 39 +++++++++++++++++++ ...rceHasClientUpdateProhibitedException.java | 24 ++++++++++++ ...urceStatusProhibitsOperationException.java | 28 +++++++++++++ ...ResourceToDeleteIsReferencedException.java | 24 ++++++++++++ ...ResourceToMutateDoesNotExistException.java | 24 ++++++++++++ .../ResourceToQueryDoesNotExistException.java | 24 ++++++++++++ .../StatusNotClientSettableException.java | 24 ++++++++++++ .../TooManyResourceChecksException.java | 24 ++++++++++++ 18 files changed, 451 insertions(+) create mode 100644 java/google/registry/flows/exceptions/AddRemoveSameValueEppException.java create mode 100644 java/google/registry/flows/exceptions/AlreadyPendingTransferException.java create mode 100644 java/google/registry/flows/exceptions/BadCommandForRegistryPhaseException.java create mode 100644 java/google/registry/flows/exceptions/MissingTransferRequestAuthInfoException.java create mode 100644 java/google/registry/flows/exceptions/NoTransferHistoryToQueryException.java create mode 100644 java/google/registry/flows/exceptions/NotAuthorizedToViewTransferException.java create mode 100644 java/google/registry/flows/exceptions/NotPendingTransferException.java create mode 100644 java/google/registry/flows/exceptions/NotTransferInitiatorException.java create mode 100644 java/google/registry/flows/exceptions/ObjectAlreadySponsoredException.java create mode 100644 java/google/registry/flows/exceptions/OnlyToolCanPassMetadataException.java create mode 100644 java/google/registry/flows/exceptions/ResourceAlreadyExistsException.java create mode 100644 java/google/registry/flows/exceptions/ResourceHasClientUpdateProhibitedException.java create mode 100644 java/google/registry/flows/exceptions/ResourceStatusProhibitsOperationException.java create mode 100644 java/google/registry/flows/exceptions/ResourceToDeleteIsReferencedException.java create mode 100644 java/google/registry/flows/exceptions/ResourceToMutateDoesNotExistException.java create mode 100644 java/google/registry/flows/exceptions/ResourceToQueryDoesNotExistException.java create mode 100644 java/google/registry/flows/exceptions/StatusNotClientSettableException.java create mode 100644 java/google/registry/flows/exceptions/TooManyResourceChecksException.java diff --git a/java/google/registry/flows/exceptions/AddRemoveSameValueEppException.java b/java/google/registry/flows/exceptions/AddRemoveSameValueEppException.java new file mode 100644 index 000000000..63ee173e4 --- /dev/null +++ b/java/google/registry/flows/exceptions/AddRemoveSameValueEppException.java @@ -0,0 +1,24 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.exceptions; + +import google.registry.flows.EppException.ParameterValuePolicyErrorException; + +/** Cannot add and remove the same value. */ +public class AddRemoveSameValueEppException extends ParameterValuePolicyErrorException { + public AddRemoveSameValueEppException() { + super("Cannot add and remove the same value"); + } +} diff --git a/java/google/registry/flows/exceptions/AlreadyPendingTransferException.java b/java/google/registry/flows/exceptions/AlreadyPendingTransferException.java new file mode 100644 index 000000000..45aa3ff34 --- /dev/null +++ b/java/google/registry/flows/exceptions/AlreadyPendingTransferException.java @@ -0,0 +1,24 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.exceptions; + +import google.registry.flows.EppException.ObjectPendingTransferException; + +/** The resource is already pending transfer. */ +public class AlreadyPendingTransferException extends ObjectPendingTransferException { + public AlreadyPendingTransferException(String targetId) { + super(targetId); + } +} diff --git a/java/google/registry/flows/exceptions/BadCommandForRegistryPhaseException.java b/java/google/registry/flows/exceptions/BadCommandForRegistryPhaseException.java new file mode 100644 index 000000000..944d4b332 --- /dev/null +++ b/java/google/registry/flows/exceptions/BadCommandForRegistryPhaseException.java @@ -0,0 +1,24 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.exceptions; + +import google.registry.flows.EppException.CommandUseErrorException; + +/** Command is not allowed in the current registry phase. */ +public class BadCommandForRegistryPhaseException extends CommandUseErrorException { + public BadCommandForRegistryPhaseException() { + super("Command is not allowed in the current registry phase"); + } +} diff --git a/java/google/registry/flows/exceptions/MissingTransferRequestAuthInfoException.java b/java/google/registry/flows/exceptions/MissingTransferRequestAuthInfoException.java new file mode 100644 index 000000000..562d7b922 --- /dev/null +++ b/java/google/registry/flows/exceptions/MissingTransferRequestAuthInfoException.java @@ -0,0 +1,24 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.exceptions; + +import google.registry.flows.EppException.AuthorizationErrorException; + +/** Authorization info is required to request a transfer. */ +public class MissingTransferRequestAuthInfoException extends AuthorizationErrorException { + public MissingTransferRequestAuthInfoException() { + super("Authorization info is required to request a transfer"); + } +} diff --git a/java/google/registry/flows/exceptions/NoTransferHistoryToQueryException.java b/java/google/registry/flows/exceptions/NoTransferHistoryToQueryException.java new file mode 100644 index 000000000..60fea0177 --- /dev/null +++ b/java/google/registry/flows/exceptions/NoTransferHistoryToQueryException.java @@ -0,0 +1,24 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.exceptions; + +import google.registry.flows.EppException.CommandUseErrorException; + +/** Object has no transfer history. */ +public class NoTransferHistoryToQueryException extends CommandUseErrorException { + public NoTransferHistoryToQueryException() { + super("Object has no transfer history"); + } +} diff --git a/java/google/registry/flows/exceptions/NotAuthorizedToViewTransferException.java b/java/google/registry/flows/exceptions/NotAuthorizedToViewTransferException.java new file mode 100644 index 000000000..65457405d --- /dev/null +++ b/java/google/registry/flows/exceptions/NotAuthorizedToViewTransferException.java @@ -0,0 +1,24 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.exceptions; + +import google.registry.flows.EppException.AuthorizationErrorException; + +/** Registrar is not authorized to view transfer status. */ +public class NotAuthorizedToViewTransferException extends AuthorizationErrorException { + public NotAuthorizedToViewTransferException() { + super("Registrar is not authorized to view transfer status"); + } +} diff --git a/java/google/registry/flows/exceptions/NotPendingTransferException.java b/java/google/registry/flows/exceptions/NotPendingTransferException.java new file mode 100644 index 000000000..74381b03d --- /dev/null +++ b/java/google/registry/flows/exceptions/NotPendingTransferException.java @@ -0,0 +1,24 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.exceptions; + +import google.registry.flows.EppException.ObjectNotPendingTransferException; + +/** The resource does not have a pending transfer. */ +public class NotPendingTransferException extends ObjectNotPendingTransferException { + public NotPendingTransferException(String objectId) { + super(objectId); + } +} diff --git a/java/google/registry/flows/exceptions/NotTransferInitiatorException.java b/java/google/registry/flows/exceptions/NotTransferInitiatorException.java new file mode 100644 index 000000000..5f049ed6e --- /dev/null +++ b/java/google/registry/flows/exceptions/NotTransferInitiatorException.java @@ -0,0 +1,24 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.exceptions; + +import google.registry.flows.EppException.AuthorizationErrorException; + +/** Registrar is not the initiator of this transfer. */ +public class NotTransferInitiatorException extends AuthorizationErrorException { + public NotTransferInitiatorException() { + super("Registrar is not the initiator of this transfer"); + } +} diff --git a/java/google/registry/flows/exceptions/ObjectAlreadySponsoredException.java b/java/google/registry/flows/exceptions/ObjectAlreadySponsoredException.java new file mode 100644 index 000000000..345ccc9cb --- /dev/null +++ b/java/google/registry/flows/exceptions/ObjectAlreadySponsoredException.java @@ -0,0 +1,24 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.exceptions; + +import google.registry.flows.EppException.CommandUseErrorException; + +/** Registrar already sponsors the object of this transfer request. */ +public class ObjectAlreadySponsoredException extends CommandUseErrorException { + public ObjectAlreadySponsoredException() { + super("Registrar already sponsors the object of this transfer request"); + } +} diff --git a/java/google/registry/flows/exceptions/OnlyToolCanPassMetadataException.java b/java/google/registry/flows/exceptions/OnlyToolCanPassMetadataException.java new file mode 100644 index 000000000..8d1e82b14 --- /dev/null +++ b/java/google/registry/flows/exceptions/OnlyToolCanPassMetadataException.java @@ -0,0 +1,24 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.exceptions; + +import google.registry.flows.EppException.AuthorizationErrorException; + +/** Only a tool can pass a metadata extension. */ +public class OnlyToolCanPassMetadataException extends AuthorizationErrorException { + public OnlyToolCanPassMetadataException() { + super("Metadata extensions can only be passed by tools."); + } +} diff --git a/java/google/registry/flows/exceptions/ResourceAlreadyExistsException.java b/java/google/registry/flows/exceptions/ResourceAlreadyExistsException.java new file mode 100644 index 000000000..3f014ad29 --- /dev/null +++ b/java/google/registry/flows/exceptions/ResourceAlreadyExistsException.java @@ -0,0 +1,39 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.exceptions; + +import com.google.common.annotations.VisibleForTesting; +import google.registry.flows.EppException.ObjectAlreadyExistsException; + +/** Resource with this id already exists. */ +public class ResourceAlreadyExistsException extends ObjectAlreadyExistsException { + + /** Whether this was thrown from a "failfast" context. Useful for testing. */ + final boolean failfast; + + public ResourceAlreadyExistsException(String resourceId, boolean failfast) { + super(String.format("Object with given ID (%s) already exists", resourceId)); + this.failfast = failfast; + } + + public ResourceAlreadyExistsException(String resourceId) { + this(resourceId, false); + } + + @VisibleForTesting + public boolean isFailfast() { + return failfast; + } +} diff --git a/java/google/registry/flows/exceptions/ResourceHasClientUpdateProhibitedException.java b/java/google/registry/flows/exceptions/ResourceHasClientUpdateProhibitedException.java new file mode 100644 index 000000000..e12c03b54 --- /dev/null +++ b/java/google/registry/flows/exceptions/ResourceHasClientUpdateProhibitedException.java @@ -0,0 +1,24 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.exceptions; + +import google.registry.flows.EppException.StatusProhibitsOperationException; + +/** This resource has clientUpdateProhibited on it, and the update does not clear that status. */ +public class ResourceHasClientUpdateProhibitedException extends StatusProhibitsOperationException { + public ResourceHasClientUpdateProhibitedException() { + super("Operation disallowed by status: clientUpdateProhibited"); + } +} diff --git a/java/google/registry/flows/exceptions/ResourceStatusProhibitsOperationException.java b/java/google/registry/flows/exceptions/ResourceStatusProhibitsOperationException.java new file mode 100644 index 000000000..834ca9f94 --- /dev/null +++ b/java/google/registry/flows/exceptions/ResourceStatusProhibitsOperationException.java @@ -0,0 +1,28 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.exceptions; + +import com.google.common.base.Joiner; +import google.registry.flows.EppException.StatusProhibitsOperationException; +import google.registry.model.eppcommon.StatusValue; +import java.util.Set; + +/** Resource status prohibits this operation. */ +public class ResourceStatusProhibitsOperationException + extends StatusProhibitsOperationException { + public ResourceStatusProhibitsOperationException(Set status) { + super("Operation disallowed by status: " + Joiner.on(", ").join(status)); + } +} diff --git a/java/google/registry/flows/exceptions/ResourceToDeleteIsReferencedException.java b/java/google/registry/flows/exceptions/ResourceToDeleteIsReferencedException.java new file mode 100644 index 000000000..cdc185947 --- /dev/null +++ b/java/google/registry/flows/exceptions/ResourceToDeleteIsReferencedException.java @@ -0,0 +1,24 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.exceptions; + +import google.registry.flows.EppException.AssociationProhibitsOperationException; + +/** Resource to be deleted has active incoming references. */ +public class ResourceToDeleteIsReferencedException extends AssociationProhibitsOperationException { + public ResourceToDeleteIsReferencedException() { + super("Resource to be deleted has active incoming references"); + } +} diff --git a/java/google/registry/flows/exceptions/ResourceToMutateDoesNotExistException.java b/java/google/registry/flows/exceptions/ResourceToMutateDoesNotExistException.java new file mode 100644 index 000000000..f4df5ada8 --- /dev/null +++ b/java/google/registry/flows/exceptions/ResourceToMutateDoesNotExistException.java @@ -0,0 +1,24 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.exceptions; + +import google.registry.flows.EppException.ObjectDoesNotExistException; + +/** Resource with this id does not exist. */ +public class ResourceToMutateDoesNotExistException extends ObjectDoesNotExistException { + public ResourceToMutateDoesNotExistException(Class type, String targetId) { + super(type, targetId); + } +} diff --git a/java/google/registry/flows/exceptions/ResourceToQueryDoesNotExistException.java b/java/google/registry/flows/exceptions/ResourceToQueryDoesNotExistException.java new file mode 100644 index 000000000..4aad6c2cb --- /dev/null +++ b/java/google/registry/flows/exceptions/ResourceToQueryDoesNotExistException.java @@ -0,0 +1,24 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.exceptions; + +import google.registry.flows.EppException.ObjectDoesNotExistException; + +/** Resource with this id does not exist. */ +public class ResourceToQueryDoesNotExistException extends ObjectDoesNotExistException { + public ResourceToQueryDoesNotExistException(Class type, String targetId) { + super(type, targetId); + } +} diff --git a/java/google/registry/flows/exceptions/StatusNotClientSettableException.java b/java/google/registry/flows/exceptions/StatusNotClientSettableException.java new file mode 100644 index 000000000..218be3a9d --- /dev/null +++ b/java/google/registry/flows/exceptions/StatusNotClientSettableException.java @@ -0,0 +1,24 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.exceptions; + +import google.registry.flows.EppException.ParameterValueRangeErrorException; + +/** The specified status value cannot be set by clients. */ +public class StatusNotClientSettableException extends ParameterValueRangeErrorException { + public StatusNotClientSettableException(String statusValue) { + super(String.format("Status value %s cannot be set by clients", statusValue)); + } +} diff --git a/java/google/registry/flows/exceptions/TooManyResourceChecksException.java b/java/google/registry/flows/exceptions/TooManyResourceChecksException.java new file mode 100644 index 000000000..686cf50cc --- /dev/null +++ b/java/google/registry/flows/exceptions/TooManyResourceChecksException.java @@ -0,0 +1,24 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.exceptions; + +import google.registry.flows.EppException.ParameterValuePolicyErrorException; + +/** Too many resource checks requested in one check command. */ +public class TooManyResourceChecksException extends ParameterValuePolicyErrorException { + public TooManyResourceChecksException(int maxChecks) { + super(String.format("No more than %s resources may be checked at a time", maxChecks)); + } +} From 04fd14995e93db4d679d9d73a40d46019d146608 Mon Sep 17 00:00:00 2001 From: cgoldfeder Date: Tue, 13 Sep 2016 08:08:20 -0700 Subject: [PATCH 22/31] Add a missing test to ContactUpdateFlow ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=133000632 --- .../flows/contact/ContactUpdateFlow.java | 1 + .../flows/contact/ContactUpdateFlowTest.java | 9 +++++ .../contact_update_add_remove_same.xml | 39 +++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 javatests/google/registry/flows/contact/testdata/contact_update_add_remove_same.xml diff --git a/java/google/registry/flows/contact/ContactUpdateFlow.java b/java/google/registry/flows/contact/ContactUpdateFlow.java index 5d37d77b0..de0c854e4 100644 --- a/java/google/registry/flows/contact/ContactUpdateFlow.java +++ b/java/google/registry/flows/contact/ContactUpdateFlow.java @@ -29,6 +29,7 @@ import javax.inject.Inject; * An EPP flow that updates a contact resource. * * @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException} + * @error {@link google.registry.flows.ResourceUpdateFlow.AddRemoveSameValueEppException} * @error {@link google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException} * @error {@link google.registry.flows.ResourceUpdateFlow.ResourceHasClientUpdateProhibitedException} * @error {@link google.registry.flows.ResourceUpdateFlow.StatusNotClientSettableException} diff --git a/javatests/google/registry/flows/contact/ContactUpdateFlowTest.java b/javatests/google/registry/flows/contact/ContactUpdateFlowTest.java index 764a2e005..b8f5752b0 100644 --- a/javatests/google/registry/flows/contact/ContactUpdateFlowTest.java +++ b/javatests/google/registry/flows/contact/ContactUpdateFlowTest.java @@ -26,6 +26,7 @@ import com.google.common.collect.ImmutableSet; import google.registry.flows.ResourceFlowTestCase; import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException; import google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException; +import google.registry.flows.ResourceUpdateFlow.AddRemoveSameValueEppException; import google.registry.flows.ResourceUpdateFlow.ResourceHasClientUpdateProhibitedException; import google.registry.flows.ResourceUpdateFlow.StatusNotClientSettableException; import google.registry.flows.SingleResourceFlow.ResourceStatusProhibitsOperationException; @@ -258,4 +259,12 @@ public class ContactUpdateFlowTest persistActiveContact(getUniqueIdFromCommand()); runFlow(); } + + @Test + public void testFailure_addRemoveSameValue() throws Exception { + thrown.expect(AddRemoveSameValueEppException.class); + setEppInput("contact_update_add_remove_same.xml"); + persistActiveContact(getUniqueIdFromCommand()); + runFlow(); + } } diff --git a/javatests/google/registry/flows/contact/testdata/contact_update_add_remove_same.xml b/javatests/google/registry/flows/contact/testdata/contact_update_add_remove_same.xml new file mode 100644 index 000000000..66087fa96 --- /dev/null +++ b/javatests/google/registry/flows/contact/testdata/contact_update_add_remove_same.xml @@ -0,0 +1,39 @@ + + + + + sh8013 + + + + + + + + + + + 124 Example Dr. + Suite 200 + Dulles + VA + 20166-6503 + US + + + +1.7034444444 + + + 2fooBAR + + + + + + + + + ABC-12345 + + From efd3424849e1a944e2a1f12832249414b85e2e94 Mon Sep 17 00:00:00 2001 From: cgoldfeder Date: Tue, 13 Sep 2016 10:18:33 -0700 Subject: [PATCH 23/31] Change to FlowModule to support the new flat flows This factors out a huge chunk of boilerplate that would otherwise be in every single flow. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=133014837 --- java/google/registry/flows/FlowModule.java | 43 ++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/java/google/registry/flows/FlowModule.java b/java/google/registry/flows/FlowModule.java index baacfeac1..0d1dfe6ff 100644 --- a/java/google/registry/flows/FlowModule.java +++ b/java/google/registry/flows/FlowModule.java @@ -18,9 +18,14 @@ import static com.google.common.base.Preconditions.checkState; import dagger.Module; import dagger.Provides; +import google.registry.flows.exceptions.OnlyToolCanPassMetadataException; import google.registry.flows.picker.FlowPicker; +import google.registry.model.domain.metadata.MetadataExtension; import google.registry.model.eppcommon.Trid; import google.registry.model.eppinput.EppInput; +import google.registry.model.eppinput.EppInput.ResourceCommandWrapper; +import google.registry.model.eppinput.ResourceCommand; +import google.registry.model.reporting.HistoryEntry; import java.lang.annotation.Documented; import javax.annotation.Nullable; import javax.inject.Qualifier; @@ -164,6 +169,44 @@ public class FlowModule { } } + @Provides + @FlowScope + static ResourceCommand provideResourceCommand(EppInput eppInput) { + return ((ResourceCommandWrapper) eppInput.getCommandWrapper().getCommand()) + .getResourceCommand(); + } + + /** + * Provides a partially filled in {@link HistoryEntry} builder. + * + *

This is not marked with {@link FlowScope} so that each retry gets a fresh one. Otherwise, + * the fact that the builder is one-use would cause NPEs. + */ + @Provides + static HistoryEntry.Builder provideHistoryEntryBuilder( + Trid trid, + @InputXml byte[] inputXmlBytes, + @Superuser boolean isSuperuser, + @ClientId @Nullable String clientId, + EppRequestSource eppRequestSource, + EppInput eppInput) { + HistoryEntry.Builder historyBuilder = new HistoryEntry.Builder() + .setTrid(trid) + .setXmlBytes(inputXmlBytes) + .setBySuperuser(isSuperuser) + .setClientId(clientId); + MetadataExtension metadataExtension = eppInput.getSingleExtension(MetadataExtension.class); + if (metadataExtension != null) { + if (!eppRequestSource.equals(EppRequestSource.TOOL)) { + throw new EppExceptionInProviderException(new OnlyToolCanPassMetadataException()); + } + historyBuilder + .setReason(metadataExtension.getReason()) + .setRequestedByRegistrar(metadataExtension.getRequestedByRegistrar()); + } + return historyBuilder; + } + /** Wrapper class to carry an {@link EppException} to the calling code. */ static class EppExceptionInProviderException extends RuntimeException { EppExceptionInProviderException(EppException exception) { From 1a050554fe02353bbf6b7ad24009c482bc6d7698 Mon Sep 17 00:00:00 2001 From: mountford Date: Tue, 13 Sep 2016 13:36:05 -0700 Subject: [PATCH 24/31] Move flags extension exceptions to separate classes The exceptions created for generic problems with the flags extension (invalid flag, etc.) should be in a common location, so they can be used by all interested TLDs. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=133040831 --- ...tensionFlagDomainPolicyErrorException.java | 24 ++++++++++++++++ .../flags/ExtensionFlagMissingException.java | 28 +++++++++++++++++++ ...sionFlagRegistrarPolicyErrorException.java | 24 ++++++++++++++++ ...sionFlagSetDomainPolicyErrorException.java | 24 ++++++++++++++++ .../flags/InvalidExtensionFlagException.java | 24 ++++++++++++++++ ...uallyExclusiveExtensionFlagsException.java | 24 ++++++++++++++++ .../domain/flags/NonClientFlagException.java | 24 ++++++++++++++++ .../SameFlagAddedAndRemovedException.java | 24 ++++++++++++++++ 8 files changed, 196 insertions(+) create mode 100644 java/google/registry/flows/domain/flags/ExtensionFlagDomainPolicyErrorException.java create mode 100644 java/google/registry/flows/domain/flags/ExtensionFlagMissingException.java create mode 100644 java/google/registry/flows/domain/flags/ExtensionFlagRegistrarPolicyErrorException.java create mode 100644 java/google/registry/flows/domain/flags/ExtensionFlagSetDomainPolicyErrorException.java create mode 100644 java/google/registry/flows/domain/flags/InvalidExtensionFlagException.java create mode 100644 java/google/registry/flows/domain/flags/MutuallyExclusiveExtensionFlagsException.java create mode 100644 java/google/registry/flows/domain/flags/NonClientFlagException.java create mode 100644 java/google/registry/flows/domain/flags/SameFlagAddedAndRemovedException.java diff --git a/java/google/registry/flows/domain/flags/ExtensionFlagDomainPolicyErrorException.java b/java/google/registry/flows/domain/flags/ExtensionFlagDomainPolicyErrorException.java new file mode 100644 index 000000000..2c95ea2af --- /dev/null +++ b/java/google/registry/flows/domain/flags/ExtensionFlagDomainPolicyErrorException.java @@ -0,0 +1,24 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.domain.flags; + +import google.registry.flows.EppException.StatusProhibitsOperationException; + +/** Extension flag is not currently valid for this domain. */ +public class ExtensionFlagDomainPolicyErrorException extends StatusProhibitsOperationException { + public ExtensionFlagDomainPolicyErrorException(String flag) { + super(String.format("Extension flag %s is not valid for this domain", flag)); + } +} diff --git a/java/google/registry/flows/domain/flags/ExtensionFlagMissingException.java b/java/google/registry/flows/domain/flags/ExtensionFlagMissingException.java new file mode 100644 index 000000000..ca593b53e --- /dev/null +++ b/java/google/registry/flows/domain/flags/ExtensionFlagMissingException.java @@ -0,0 +1,28 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.domain.flags; + +import google.registry.flows.EppException.RequiredParameterMissingException; + +/** Required extension flag missing. */ +public class ExtensionFlagMissingException extends RequiredParameterMissingException { + public ExtensionFlagMissingException(String flag) { + super(String.format("Flag %s must be specified", flag)); + } + + public ExtensionFlagMissingException(String flag1, String flag2) { + super(String.format("Either %s or %s must be specified", flag1, flag2)); + } +} diff --git a/java/google/registry/flows/domain/flags/ExtensionFlagRegistrarPolicyErrorException.java b/java/google/registry/flows/domain/flags/ExtensionFlagRegistrarPolicyErrorException.java new file mode 100644 index 000000000..933eaf16d --- /dev/null +++ b/java/google/registry/flows/domain/flags/ExtensionFlagRegistrarPolicyErrorException.java @@ -0,0 +1,24 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.domain.flags; + +import google.registry.flows.EppException.ParameterValuePolicyErrorException; + +/** Extension flag is not currently valid for this registrar. */ +public class ExtensionFlagRegistrarPolicyErrorException extends ParameterValuePolicyErrorException { + public ExtensionFlagRegistrarPolicyErrorException(String flag) { + super(String.format("Extension flag %s is not valid for this registrar", flag)); + } +} diff --git a/java/google/registry/flows/domain/flags/ExtensionFlagSetDomainPolicyErrorException.java b/java/google/registry/flows/domain/flags/ExtensionFlagSetDomainPolicyErrorException.java new file mode 100644 index 000000000..1c53d8307 --- /dev/null +++ b/java/google/registry/flows/domain/flags/ExtensionFlagSetDomainPolicyErrorException.java @@ -0,0 +1,24 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.domain.flags; + +import google.registry.flows.EppException.StatusProhibitsOperationException; + +/** Extension flag cannot currently be set for this domain. */ +public class ExtensionFlagSetDomainPolicyErrorException extends StatusProhibitsOperationException { + public ExtensionFlagSetDomainPolicyErrorException(String flag) { + super(String.format("Extension flag %s cannot be set for this domain", flag)); + } +} diff --git a/java/google/registry/flows/domain/flags/InvalidExtensionFlagException.java b/java/google/registry/flows/domain/flags/InvalidExtensionFlagException.java new file mode 100644 index 000000000..abfcd3f0b --- /dev/null +++ b/java/google/registry/flows/domain/flags/InvalidExtensionFlagException.java @@ -0,0 +1,24 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.domain.flags; + +import google.registry.flows.EppException.ParameterValueRangeErrorException; + +/** Extension flag is not valid. */ +public class InvalidExtensionFlagException extends ParameterValueRangeErrorException { + public InvalidExtensionFlagException(String flag) { + super(String.format("Extension flag %s is not defined", flag)); + } +} diff --git a/java/google/registry/flows/domain/flags/MutuallyExclusiveExtensionFlagsException.java b/java/google/registry/flows/domain/flags/MutuallyExclusiveExtensionFlagsException.java new file mode 100644 index 000000000..bd302969d --- /dev/null +++ b/java/google/registry/flows/domain/flags/MutuallyExclusiveExtensionFlagsException.java @@ -0,0 +1,24 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.domain.flags; + +import google.registry.flows.EppException.ParameterValuePolicyErrorException; + +/** Specified extension flags are mutually exclusive. */ +public class MutuallyExclusiveExtensionFlagsException extends ParameterValuePolicyErrorException { + public MutuallyExclusiveExtensionFlagsException(String flag1, String flag2) { + super(String.format("Extension flags %s and %s are mutually exclusive", flag1, flag2)); + } +} diff --git a/java/google/registry/flows/domain/flags/NonClientFlagException.java b/java/google/registry/flows/domain/flags/NonClientFlagException.java new file mode 100644 index 000000000..f6c9a572e --- /dev/null +++ b/java/google/registry/flows/domain/flags/NonClientFlagException.java @@ -0,0 +1,24 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.domain.flags; + +import google.registry.flows.EppException.ParameterValuePolicyErrorException; + +/** Only client flags can be updated. */ +public class NonClientFlagException extends ParameterValuePolicyErrorException { + public NonClientFlagException() { + super("Non-client flags cannot be added or removed"); + } +} diff --git a/java/google/registry/flows/domain/flags/SameFlagAddedAndRemovedException.java b/java/google/registry/flows/domain/flags/SameFlagAddedAndRemovedException.java new file mode 100644 index 000000000..b13f391f6 --- /dev/null +++ b/java/google/registry/flows/domain/flags/SameFlagAddedAndRemovedException.java @@ -0,0 +1,24 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.flows.domain.flags; + +import google.registry.flows.EppException.ParameterValuePolicyErrorException; + +/** The same flag was specified in both add and remove lists. */ +public class SameFlagAddedAndRemovedException extends ParameterValuePolicyErrorException { + public SameFlagAddedAndRemovedException() { + super("An extension flag cannot be both added and removed in the same command"); + } +} From 75203918a9d130bf32b8a34604ce0cb022a22051 Mon Sep 17 00:00:00 2001 From: ctingue Date: Tue, 13 Sep 2016 14:32:03 -0700 Subject: [PATCH 25/31] Add command for creating LRP tokens Command allows for both one-off creation and bulk import of assignees via file (the latter will be used for the initial import from Play Store). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=133048360 --- .../tools/CreateLrpTokensCommand.java | 152 +++++++++++++ java/google/registry/tools/GtechTool.java | 1 + .../registry/tools/StringGenerator.java | 13 ++ javatests/google/registry/tools/BUILD | 1 + .../tools/CreateLrpTokensCommandTest.java | 204 ++++++++++++++++++ .../tools/DeterministicStringGenerator.java | 35 ++- .../tools/GetLrpTokenCommandTest.java | 3 +- 7 files changed, 404 insertions(+), 5 deletions(-) create mode 100644 java/google/registry/tools/CreateLrpTokensCommand.java create mode 100644 javatests/google/registry/tools/CreateLrpTokensCommandTest.java diff --git a/java/google/registry/tools/CreateLrpTokensCommand.java b/java/google/registry/tools/CreateLrpTokensCommand.java new file mode 100644 index 000000000..db80e9831 --- /dev/null +++ b/java/google/registry/tools/CreateLrpTokensCommand.java @@ -0,0 +1,152 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.tools; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Strings.isNullOrEmpty; +import static com.google.common.collect.Sets.difference; +import static google.registry.model.ofy.ObjectifyService.ofy; +import static google.registry.model.registry.Registries.assertTldExists; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; + +import com.google.common.base.Function; +import com.google.common.base.Splitter; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.Files; +import com.google.common.io.LineReader; + +import com.googlecode.objectify.Key; +import com.googlecode.objectify.Work; + +import google.registry.model.domain.LrpToken; +import google.registry.tools.Command.GtechCommand; +import google.registry.tools.Command.RemoteApiCommand; +import google.registry.tools.params.PathParameter; + +import java.io.StringReader; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Set; + +import javax.inject.Inject; + +/** + * Command to create one or more LRP tokens, given assignee(s) as either a parameter or a text file. + */ +@Parameters( + separators = " =", + commandDescription = "Create an LRP token for a given assignee (using -a) or import a text" + + " file of assignees for bulk token creation (using -i). Assignee/token pairs are printed" + + " to stdout, and should be piped to a file for distribution to assignees or for cleanup" + + " in the event of a command interruption.") +public final class CreateLrpTokensCommand implements RemoteApiCommand, GtechCommand { + + @Parameter( + names = {"-a", "--assignee"}, + description = "LRP token assignee") + private String assignee; + + @Parameter( + names = {"-t", "--tlds"}, + description = "Comma-delimited list of TLDs that the tokens to create will be valid on", + required = true) + private String tlds; + + @Parameter( + names = {"-i", "--input"}, + description = "Filename containing a list of assignees, newline-delimited", + validateWith = PathParameter.InputFile.class) + private Path assigneesFile; + + @Inject StringGenerator stringGenerator; + + private static final int TOKEN_LENGTH = 16; + private static final int BATCH_SIZE = 20; + + @Override + public void run() throws Exception { + checkArgument( + (assignee == null) == (assigneesFile != null), + "Exactly one of either assignee or filename must be specified."); + final Set validTlds = ImmutableSet.copyOf(Splitter.on(',').split(tlds)); + for (String tld : validTlds) { + assertTldExists(tld); + } + + LineReader reader = new LineReader( + (assignee == null) + ? Files.newReader(assigneesFile.toFile(), UTF_8) + : new StringReader(assignee)); + + String line = null; + do { + ImmutableSet.Builder tokensToSave = new ImmutableSet.Builder<>(); + for (String token : generateTokens(BATCH_SIZE)) { + line = reader.readLine(); + if (!isNullOrEmpty(line)) { + tokensToSave.add(new LrpToken.Builder() + .setAssignee(line) + .setToken(token) + .setValidTlds(validTlds) + .build()); + } + } + saveTokens(tokensToSave.build()); + } while (line != null); + } + + private void saveTokens(final ImmutableSet tokens) { + Collection savedTokens = ofy().transact(new Work>() { + @Override + public Collection run() { + return ofy().save().entities(tokens).now().values(); + }}); + for (LrpToken token : savedTokens) { + System.out.printf("%s,%s%n", token.getAssignee(), token.getToken()); + } + } + + /** + * This function generates at MOST {@code count} tokens, after filtering out any token strings + * that already exist. + * + *

Note that in the incredibly rare case that all generated tokens already exist, this function + * may return an empty set. + */ + private ImmutableSet generateTokens(int count) { + final ImmutableSet candidates = + ImmutableSet.copyOf(stringGenerator.createStrings(TOKEN_LENGTH, count)); + ImmutableSet> existingTokenKeys = FluentIterable.from(candidates) + .transform(new Function>() { + @Override + public Key apply(String input) { + return Key.create(LrpToken.class, input); + }}) + .toSet(); + ImmutableSet existingTokenStrings = FluentIterable + .from(ofy().load().keys(existingTokenKeys).values()) + .transform(new Function() { + @Override + public String apply(LrpToken input) { + return input.getToken(); + }}) + .toSet(); + return ImmutableSet.copyOf(difference(candidates, existingTokenStrings)); + } +} diff --git a/java/google/registry/tools/GtechTool.java b/java/google/registry/tools/GtechTool.java index e46095c0d..58b183d54 100644 --- a/java/google/registry/tools/GtechTool.java +++ b/java/google/registry/tools/GtechTool.java @@ -38,6 +38,7 @@ public final class GtechTool { .put("create_credit_balance", CreateCreditBalanceCommand.class) .put("create_domain", CreateDomainCommand.class) .put("create_host", CreateHostCommand.class) + .put("create_lrp_tokens", CreateLrpTokensCommand.class) .put("create_registrar_groups", CreateRegistrarGroupsCommand.class) .put("create_registrar", CreateRegistrarCommand.class) .put("create_sandbox_tld", CreateSandboxTldCommand.class) diff --git a/java/google/registry/tools/StringGenerator.java b/java/google/registry/tools/StringGenerator.java index 3834ae47e..dbbb06fc7 100644 --- a/java/google/registry/tools/StringGenerator.java +++ b/java/google/registry/tools/StringGenerator.java @@ -17,6 +17,10 @@ package google.registry.tools; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Strings.isNullOrEmpty; +import com.google.common.collect.ImmutableList; + +import java.util.Collection; + /** String generator. */ abstract class StringGenerator { @@ -41,4 +45,13 @@ abstract class StringGenerator { /** Generates a string of a specified length. */ abstract String createString(int length); + + /** Batch-generates an {@link ImmutableList} of strings of a specified length. */ + public Collection createStrings(int length, int count) { + ImmutableList.Builder listBuilder = new ImmutableList.Builder<>(); + for (int i = 0; i < count; i++) { + listBuilder.add(createString(length)); + } + return listBuilder.build(); + } } diff --git a/javatests/google/registry/tools/BUILD b/javatests/google/registry/tools/BUILD index 4325c63cd..6117e6887 100644 --- a/javatests/google/registry/tools/BUILD +++ b/javatests/google/registry/tools/BUILD @@ -28,6 +28,7 @@ java_library( "//third_party/java/joda_money", "//third_party/java/joda_time", "//third_party/java/json_simple", + "//third_party/java/jsr305_annotations", "//third_party/java/jsr330_inject", "//third_party/java/junit", "//third_party/java/mockito", diff --git a/javatests/google/registry/tools/CreateLrpTokensCommandTest.java b/javatests/google/registry/tools/CreateLrpTokensCommandTest.java new file mode 100644 index 000000000..d12173878 --- /dev/null +++ b/javatests/google/registry/tools/CreateLrpTokensCommandTest.java @@ -0,0 +1,204 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.tools; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.model.ofy.ObjectifyService.ofy; +import static google.registry.testing.DatastoreHelper.createTld; +import static google.registry.testing.DatastoreHelper.persistResource; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.Files; + +import com.googlecode.objectify.Key; + +import google.registry.model.domain.LrpToken; +import google.registry.model.reporting.HistoryEntry; +import google.registry.tools.DeterministicStringGenerator.Rule; + +import java.io.File; +import java.io.IOException; +import java.util.Set; + +import javax.annotation.Nullable; + +import org.junit.Before; +import org.junit.Test; + +/** Unit tests for {@link CreateLrpTokensCommand}. */ +public class CreateLrpTokensCommandTest extends CommandTestCase { + + DeterministicStringGenerator stringGenerator = + new DeterministicStringGenerator("abcdefghijklmnopqrstuvwxyz"); + File assigneeFile; + String assigneeFilePath; + + @Before + public void init() throws IOException { + assigneeFile = tmpDir.newFile("lrp_assignees.txt"); + assigneeFilePath = assigneeFile.getPath(); + command.stringGenerator = stringGenerator; + createTld("tld"); + } + + @Test + public void testSuccess_oneAssignee() throws Exception { + runCommand("--assignee=domain.tld", "--tlds=tld"); + assertLrpTokens(createToken("abcdefghijklmnop", "domain.tld", ImmutableSet.of("tld"), null)); + assertInStdout("domain.tld,abcdefghijklmnop"); + } + + @Test + public void testSuccess_oneAssignee_tokenCollision() throws Exception { + LrpToken existingToken = persistResource(new LrpToken.Builder() + .setToken("abcdefghijklmnop") + .setAssignee("otherdomain.tld") + .setValidTlds(ImmutableSet.of("tld")) + .build()); + runCommand("--assignee=domain.tld", "--tlds=tld"); + assertLrpTokens( + existingToken, + createToken("qrstuvwxyzabcdef", "domain.tld", ImmutableSet.of("tld"), null)); + assertInStdout("domain.tld,qrstuvwxyzabcdef"); + } + + @Test + public void testSuccess_oneAssignee_byFile() throws Exception { + Files.write("domain.tld", assigneeFile, UTF_8); + runCommand("--input=" + assigneeFilePath, "--tlds=tld"); + assertLrpTokens(createToken("abcdefghijklmnop", "domain.tld", ImmutableSet.of("tld"), null)); + assertInStdout("domain.tld,abcdefghijklmnop"); + } + + @Test + public void testSuccess_emptyFile() throws Exception { + Files.write("", assigneeFile, UTF_8); + runCommand("--input=" + assigneeFilePath, "--tlds=tld"); + assertLrpTokens(); // no tokens exist + assertThat(getStdoutAsString()).isEmpty(); + } + + @Test + public void testSuccess_multipleAssignees_byFile() throws Exception { + Files.write("domain1.tld\ndomain2.tld\ndomain3.tld", assigneeFile, UTF_8); + runCommand("--input=" + assigneeFilePath, "--tlds=tld"); + + assertLrpTokens( + createToken("abcdefghijklmnop", "domain1.tld", ImmutableSet.of("tld"), null), + createToken("qrstuvwxyzabcdef", "domain2.tld", ImmutableSet.of("tld"), null), + createToken("ghijklmnopqrstuv", "domain3.tld", ImmutableSet.of("tld"), null)); + + assertInStdout("domain1.tld,abcdefghijklmnop"); + assertInStdout("domain2.tld,qrstuvwxyzabcdef"); + assertInStdout("domain3.tld,ghijklmnopqrstuv"); + } + + @Test + public void testSuccess_multipleAssignees_byFile_ignoreBlankLine() throws Exception { + Files.write("domain1.tld\n\ndomain2.tld", assigneeFile, UTF_8); + runCommand("--input=" + assigneeFilePath, "--tlds=tld"); + assertLrpTokens( + createToken("abcdefghijklmnop", "domain1.tld", ImmutableSet.of("tld"), null), + // Second deterministic token (qrstuvwxyzabcdef) still consumed but not assigned + createToken("ghijklmnopqrstuv", "domain2.tld", ImmutableSet.of("tld"), null)); + assertInStdout("domain1.tld,abcdefghijklmnop"); + assertInStdout("domain2.tld,ghijklmnopqrstuv"); + } + + @Test + public void testSuccess_largeFile() throws Exception { + int numberOfTokens = 67; + LrpToken[] expectedTokens = new LrpToken[numberOfTokens]; + // Prepend a counter to avoid collisions, 16-char alphabet will always generate the same string. + stringGenerator = + new DeterministicStringGenerator("abcdefghijklmnop", Rule.PREPEND_COUNTER); + command.stringGenerator = stringGenerator; + StringBuilder assigneeFileBuilder = new StringBuilder(); + for (int i = 0; i < numberOfTokens; i++) { + assigneeFileBuilder.append(String.format("domain%d.tld\n", i)); + expectedTokens[i] = + createToken( + String.format("%04d_abcdefghijklmnop", i), + String.format("domain%d.tld", i), + ImmutableSet.of("tld"), + null); + } + Files.write(assigneeFileBuilder, assigneeFile, UTF_8); + runCommand("--input=" + assigneeFilePath, "--tlds=tld"); + assertLrpTokens(expectedTokens); + for (int i = 0; i < numberOfTokens; i++) { + assertInStdout(String.format("domain%d.tld,%04d_abcdefghijklmnop", i, i)); + } + } + + @Test + public void testFailure_missingAssigneeOrFile() throws Exception { + thrown.expect( + IllegalArgumentException.class, + "Exactly one of either assignee or filename must be specified."); + runCommand("--tlds=tld"); + } + + @Test + public void testFailure_bothAssigneeAndFile() throws Exception { + thrown.expect( + IllegalArgumentException.class, + "Exactly one of either assignee or filename must be specified."); + runCommand("--assignee=domain.tld", "--tlds=tld", "--input=" + assigneeFilePath); + } + + @Test + public void testFailure_badTld() throws Exception { + thrown.expect(IllegalArgumentException.class, "TLD foo does not exist"); + runCommand("--assignee=domain.tld", "--tlds=foo"); + } + + private void assertLrpTokens(LrpToken... expected) throws Exception { + // Using ImmutableObject comparison here is tricky because updateTimestamp is not set on the + // expected LrpToken objects and will cause the assert to fail. + Iterable actual = ofy().load().type(LrpToken.class); + ImmutableMap.Builder actualTokenMapBuilder = new ImmutableMap.Builder<>(); + for (LrpToken token : actual) { + actualTokenMapBuilder.put(token.getToken(), token); + } + ImmutableMap actualTokenMap = actualTokenMapBuilder.build(); + assertThat(actualTokenMap).hasSize(expected.length); + for (LrpToken expectedToken : expected) { + LrpToken match = actualTokenMap.get(expectedToken.getToken()); + assertThat(match).isNotNull(); + assertThat(match.getAssignee()).isEqualTo(expectedToken.getAssignee()); + assertThat(match.getValidTlds()).containsExactlyElementsIn(expectedToken.getValidTlds()); + assertThat(match.getRedemptionHistoryEntry()) + .isEqualTo(expectedToken.getRedemptionHistoryEntry()); + } + } + + private LrpToken createToken( + String token, + String assignee, + Set validTlds, + @Nullable Key redemptionHistoryEntry) { + LrpToken.Builder tokenBuilder = new LrpToken.Builder() + .setAssignee(assignee) + .setValidTlds(validTlds) + .setToken(token); + if (redemptionHistoryEntry != null) { + tokenBuilder.setRedemptionHistoryEntry(redemptionHistoryEntry); + } + return tokenBuilder.build(); + } +} diff --git a/javatests/google/registry/tools/DeterministicStringGenerator.java b/javatests/google/registry/tools/DeterministicStringGenerator.java index f7dffbb52..708b635cb 100644 --- a/javatests/google/registry/tools/DeterministicStringGenerator.java +++ b/javatests/google/registry/tools/DeterministicStringGenerator.java @@ -33,6 +33,24 @@ import javax.inject.Named; class DeterministicStringGenerator extends StringGenerator { private Iterator iterator; + private final Rule rule; + private int counter = 0; + + /** String generation rules. */ + enum Rule { + + /** + * Simple string generation, cycling through sequential letters in the alphabet. May produce + * duplicates. + */ + DEFAULT, + + /** + * Same cyclical pattern as {@link Rule#DEFAULT}, prepending the iteration number and an + * underscore. Intended to avoid duplicates. + */ + PREPEND_COUNTER + } /** * Generates a string using sequential characters in the generator's alphabet, cycling back to the @@ -45,11 +63,22 @@ class DeterministicStringGenerator extends StringGenerator { for (int i = 0; i < length; i++) { password += iterator.next(); } - return password; + switch (rule) { + case PREPEND_COUNTER: + return String.format("%04d_%s", counter++, password); + case DEFAULT: + default: + return password; + } + } + + public DeterministicStringGenerator(@Named("alphabet") String alphabet, Rule rule) { + super(alphabet); + iterator = Iterators.cycle(charactersOf(alphabet)); + this.rule = rule; } public DeterministicStringGenerator(@Named("alphabet") String alphabet) { - super(alphabet); - iterator = Iterators.cycle(charactersOf(alphabet)); + this(alphabet, Rule.DEFAULT); } } diff --git a/javatests/google/registry/tools/GetLrpTokenCommandTest.java b/javatests/google/registry/tools/GetLrpTokenCommandTest.java index 70ffb4fd7..60a84e04c 100644 --- a/javatests/google/registry/tools/GetLrpTokenCommandTest.java +++ b/javatests/google/registry/tools/GetLrpTokenCommandTest.java @@ -26,8 +26,7 @@ import org.junit.Before; import org.junit.Test; /** Unit tests for {@link GetLrpTokenCommand}. */ -public class GetLrpTokenCommandTest - extends CommandTestCase { +public class GetLrpTokenCommandTest extends CommandTestCase { @Before public void before() { From 4f320232b1236f918345cec15800850760cd8414 Mon Sep 17 00:00:00 2001 From: cgoldfeder Date: Tue, 13 Sep 2016 19:12:08 -0700 Subject: [PATCH 26/31] Add some common functions to ResourceFlowUtils to support flat flows ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=133079286 --- .../registry/flows/ResourceFlowUtils.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/java/google/registry/flows/ResourceFlowUtils.java b/java/google/registry/flows/ResourceFlowUtils.java index fbb853c08..88065d649 100644 --- a/java/google/registry/flows/ResourceFlowUtils.java +++ b/java/google/registry/flows/ResourceFlowUtils.java @@ -15,18 +15,28 @@ package google.registry.flows; import static com.google.common.base.Preconditions.checkState; +import static google.registry.model.EppResourceUtils.queryDomainsUsingResource; import static google.registry.model.domain.DomainResource.extendRegistrationWithCap; import static google.registry.model.ofy.ObjectifyService.ofy; +import com.google.common.base.Function; +import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; import com.google.common.collect.Sets; +import com.googlecode.objectify.Key; +import com.googlecode.objectify.Work; import google.registry.flows.EppException.AuthorizationErrorException; import google.registry.flows.EppException.InvalidAuthorizationInformationErrorException; +import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException; +import google.registry.flows.exceptions.ResourceToDeleteIsReferencedException; +import google.registry.flows.exceptions.ResourceToMutateDoesNotExistException; import google.registry.model.EppResource; import google.registry.model.EppResource.Builder; import google.registry.model.EppResource.ForeignKeyedEppResource; import google.registry.model.contact.ContactResource; +import google.registry.model.domain.DomainBase; import google.registry.model.domain.DomainResource; import google.registry.model.eppcommon.AuthInfo; import google.registry.model.eppcommon.AuthInfo.BadAuthInfoException; @@ -43,6 +53,8 @@ import google.registry.model.transfer.TransferResponse; import google.registry.model.transfer.TransferResponse.ContactTransferResponse; import google.registry.model.transfer.TransferResponse.DomainTransferResponse; import google.registry.model.transfer.TransferStatus; +import java.util.List; +import java.util.Set; import org.joda.time.DateTime; /** Static utility functions for resource transfer flows. */ @@ -52,6 +64,9 @@ public class ResourceFlowUtils { private static final ImmutableSet ADD_EXDATE_STATUSES = Sets.immutableEnumSet( TransferStatus.PENDING, TransferStatus.CLIENT_APPROVED, TransferStatus.SERVER_APPROVED); + /** In {@link #failfastForAsyncDelete}, check this (arbitrary) number of query results. */ + private static final int FAILFAST_CHECK_COUNT = 5; + /** * Create a transfer response using the id and type of this resource and the specified * {@link TransferData}. @@ -166,6 +181,41 @@ public class ResourceFlowUtils { } } + /** Check whether an asynchronous delete would obviously fail, and throw an exception if so. */ + public static void failfastForAsyncDelete( + final String targetId, + final DateTime now, + final Class resourceClass, + final Function> getPotentialReferences) throws EppException { + // Enter a transactionless context briefly. + EppException failfastException = ofy().doTransactionless(new Work() { + @Override + public EppException run() { + final ForeignKeyIndex fki = ForeignKeyIndex.load(resourceClass, targetId, now); + if (fki == null) { + return new ResourceToMutateDoesNotExistException(resourceClass, targetId); + } + // Query for the first few linked domains, and if found, actually load them. The query is + // eventually consistent and so might be very stale, but the direct load will not be stale, + // just non-transactional. If we find at least one actual reference then we can reliably + // fail. If we don't find any, we can't trust the query and need to do the full mapreduce. + List> keys = queryDomainsUsingResource( + resourceClass, fki.getResourceKey(), now, FAILFAST_CHECK_COUNT); + Predicate predicate = new Predicate() { + @Override + public boolean apply(DomainBase domain) { + return getPotentialReferences.apply(domain).contains(fki.getResourceKey()); + }}; + return Iterables.any(ofy().load().keys(keys).values(), predicate) + ? new ResourceToDeleteIsReferencedException() + : null; + } + }); + if (failfastException != null) { + throw failfastException; + } + } + /** The specified resource belongs to another client. */ public static class ResourceNotOwnedException extends AuthorizationErrorException { public ResourceNotOwnedException() { @@ -183,6 +233,15 @@ public class ResourceFlowUtils { } } + /** Check that the resource does not have any disallowed status values. */ + public static void verifyNoDisallowedStatuses( + EppResource resource, ImmutableSet disallowedStatuses) throws EppException { + Set problems = Sets.intersection(resource.getStatusValues(), disallowedStatuses); + if (!problems.isEmpty()) { + throw new ResourceStatusProhibitsOperationException(problems); + } + } + /** Authorization information for accessing resource is invalid. */ public static class BadAuthInfoForResourceException extends InvalidAuthorizationInformationErrorException { From bf9a3a0fb225de64c89705c0529f3133c54e0db4 Mon Sep 17 00:00:00 2001 From: cgoldfeder Date: Tue, 13 Sep 2016 19:18:34 -0700 Subject: [PATCH 27/31] Add some more configs to ConfigModule and provide it in flow tests ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=133079669 --- java/google/registry/config/ConfigModule.java | 18 ++++++++++++++++++ .../registry/flows/EppTestComponent.java | 3 +++ 2 files changed, 21 insertions(+) diff --git a/java/google/registry/config/ConfigModule.java b/java/google/registry/config/ConfigModule.java index 74091525b..8a5d242a8 100644 --- a/java/google/registry/config/ConfigModule.java +++ b/java/google/registry/config/ConfigModule.java @@ -658,4 +658,22 @@ public final class ConfigModule { public static Duration provideMetricsWriteInterval() { return Duration.standardSeconds(60); } + + @Provides + @Config("contactAutomaticTransferLength") + public static Duration provideContactAutomaticTransferLength(RegistryConfig config) { + return config.getContactAutomaticTransferLength(); + } + + @Provides + @Config("asyncDeleteFlowMapreduceDelay") + public static Duration provideAsyncDeleteFlowMapreduceDelay(RegistryConfig config) { + return config.getAsyncDeleteFlowMapreduceDelay(); + } + + @Provides + @Config("maxChecks") + public static int provideMaxChecks(RegistryConfig config) { + return config.getMaxChecks(); + } } diff --git a/javatests/google/registry/flows/EppTestComponent.java b/javatests/google/registry/flows/EppTestComponent.java index fd8062c7a..9757677c2 100644 --- a/javatests/google/registry/flows/EppTestComponent.java +++ b/javatests/google/registry/flows/EppTestComponent.java @@ -21,6 +21,7 @@ import dagger.Component; import dagger.Module; import dagger.Provides; import dagger.Subcomponent; +import google.registry.config.ConfigModule; import google.registry.monitoring.whitebox.BigQueryMetricsEnqueuer; import google.registry.monitoring.whitebox.EppMetric; import google.registry.request.RequestScope; @@ -32,6 +33,7 @@ import javax.inject.Singleton; @Singleton @Component( modules = { + ConfigModule.class, EppTestComponent.FakesAndMocksModule.class }) interface EppTestComponent { @@ -82,3 +84,4 @@ interface EppTestComponent { FlowComponent.Builder flowComponentBuilder(); } } + From 99af33328d0fe961ae3125df73b261c0d459d154 Mon Sep 17 00:00:00 2001 From: cgoldfeder Date: Tue, 13 Sep 2016 19:28:06 -0700 Subject: [PATCH 28/31] Flatten the contact flows There was very little meat in the contact hierarchy and it flattened quiet easily. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=133080191 --- .../flows/contact/ContactCheckFlow.java | 26 +++- .../flows/contact/ContactCreateFlow.java | 60 +++++--- .../flows/contact/ContactDeleteFlow.java | 97 +++++++----- .../flows/contact/ContactInfoFlow.java | 34 ++++- .../contact/ContactTransferApproveFlow.java | 91 +++++++++++- .../contact/ContactTransferCancelFlow.java | 91 ++++++++++-- .../contact/ContactTransferQueryFlow.java | 50 ++++++- .../contact/ContactTransferRejectFlow.java | 88 ++++++++++- .../contact/ContactTransferRequestFlow.java | 140 ++++++++++++++++-- .../flows/contact/ContactUpdateFlow.java | 105 ++++++++++--- .../flows/EppLifecycleContactTest.java | 2 +- .../flows/contact/ContactCheckFlowTest.java | 2 +- .../flows/contact/ContactCreateFlowTest.java | 2 +- .../flows/contact/ContactDeleteFlowTest.java | 6 +- .../flows/contact/ContactInfoFlowTest.java | 2 +- .../ContactTransferApproveFlowTest.java | 4 +- .../ContactTransferCancelFlowTest.java | 6 +- .../contact/ContactTransferQueryFlowTest.java | 6 +- .../ContactTransferRejectFlowTest.java | 4 +- .../ContactTransferRequestFlowTest.java | 8 +- .../flows/contact/ContactUpdateFlowTest.java | 10 +- .../poll_response_contact_transfer.xml | 2 +- 22 files changed, 682 insertions(+), 154 deletions(-) diff --git a/java/google/registry/flows/contact/ContactCheckFlow.java b/java/google/registry/flows/contact/ContactCheckFlow.java index 5c40bd317..1e1346dd3 100644 --- a/java/google/registry/flows/contact/ContactCheckFlow.java +++ b/java/google/registry/flows/contact/ContactCheckFlow.java @@ -15,34 +15,46 @@ package google.registry.flows.contact; import static google.registry.model.EppResourceUtils.checkResourcesExist; +import static google.registry.model.eppoutput.Result.Code.Success; import com.google.common.collect.ImmutableList; -import google.registry.flows.ResourceCheckFlow; +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.eppoutput.CheckData; +import google.registry.model.eppinput.ResourceCommand; import google.registry.model.eppoutput.CheckData.ContactCheck; import google.registry.model.eppoutput.CheckData.ContactCheckData; +import google.registry.model.eppoutput.EppOutput; +import java.util.List; import java.util.Set; import javax.inject.Inject; /** * An EPP flow that checks whether a contact can be provisioned. * - * @error {@link google.registry.flows.ResourceCheckFlow.TooManyResourceChecksException} + * @error {@link google.registry.flows.exceptions.TooManyResourceChecksException} */ -public class ContactCheckFlow extends ResourceCheckFlow { +public class ContactCheckFlow extends LoggedInFlow { + @Inject ResourceCommand resourceCommand; + @Inject @Config("maxChecks") int maxChecks; @Inject ContactCheckFlow() {} @Override - protected CheckData getCheckData() { - Set existingIds = checkResourcesExist(resourceClass, targetIds, now); + public final EppOutput run() throws EppException { + List targetIds = ((Check) resourceCommand).getTargetIds(); + if (targetIds.size() > maxChecks) { + throw new TooManyResourceChecksException(maxChecks); + } + Set existingIds = checkResourcesExist(ContactResource.class, targetIds, now); ImmutableList.Builder checks = new ImmutableList.Builder<>(); for (String id : targetIds) { boolean unused = !existingIds.contains(id); checks.add(ContactCheck.create(unused, id, unused ? null : "In use")); } - return ContactCheckData.create(checks.build()); + return createOutput(Success, ContactCheckData.create(checks.build()), null); } } diff --git a/java/google/registry/flows/contact/ContactCreateFlow.java b/java/google/registry/flows/contact/ContactCreateFlow.java index b17ebd383..f034347ae 100644 --- a/java/google/registry/flows/contact/ContactCreateFlow.java +++ b/java/google/registry/flows/contact/ContactCreateFlow.java @@ -17,15 +17,24 @@ package google.registry.flows.contact; import static google.registry.flows.contact.ContactFlowUtils.validateAsciiPostalInfo; import static google.registry.flows.contact.ContactFlowUtils.validateContactAgainstPolicy; import static google.registry.model.EppResourceUtils.createContactHostRoid; +import static google.registry.model.EppResourceUtils.loadByUniqueId; import static google.registry.model.eppoutput.Result.Code.Success; +import static google.registry.model.ofy.ObjectifyService.ofy; +import com.googlecode.objectify.Key; import google.registry.flows.EppException; -import google.registry.flows.ResourceCreateFlow; +import google.registry.flows.LoggedInFlow; +import google.registry.flows.TransactionalFlow; +import google.registry.flows.exceptions.ResourceAlreadyExistsException; import google.registry.model.contact.ContactCommand.Create; import google.registry.model.contact.ContactResource; import google.registry.model.contact.ContactResource.Builder; +import google.registry.model.domain.metadata.MetadataExtension; +import google.registry.model.eppinput.ResourceCommand; import google.registry.model.eppoutput.CreateData.ContactCreateData; import google.registry.model.eppoutput.EppOutput; +import google.registry.model.index.EppResourceIndex; +import google.registry.model.index.ForeignKeyIndex; import google.registry.model.ofy.ObjectifyService; import google.registry.model.reporting.HistoryEntry; import javax.inject.Inject; @@ -33,37 +42,46 @@ import javax.inject.Inject; /** * An EPP flow that creates a new contact resource. * - * @error {@link google.registry.flows.ResourceCreateFlow.ResourceAlreadyExistsException} + * @error {@link google.registry.flows.exceptions.ResourceAlreadyExistsException} * @error {@link ContactFlowUtils.BadInternationalizedPostalInfoException} * @error {@link ContactFlowUtils.DeclineContactDisclosureFieldDisallowedPolicyException} */ -public class ContactCreateFlow extends ResourceCreateFlow { +public class ContactCreateFlow extends LoggedInFlow implements TransactionalFlow { + @Inject ResourceCommand resourceCommand; + @Inject HistoryEntry.Builder historyBuilder; @Inject ContactCreateFlow() {} @Override - protected EppOutput getOutput() { - return createOutput(Success, ContactCreateData.create(newResource.getContactId(), now)); + protected final void initLoggedInFlow() throws EppException { + registerExtensions(MetadataExtension.class); } @Override - protected String createFlowRepoId() { - return createContactHostRoid(ObjectifyService.allocateId()); - } - - @Override - protected void verifyNewStateIsAllowed() throws EppException { + protected final EppOutput run() throws EppException { + Create command = (Create) resourceCommand; + if (loadByUniqueId(ContactResource.class, command.getTargetId(), now) != null) { + throw new ResourceAlreadyExistsException(command.getTargetId()); + } + Builder builder = new Builder(); + command.applyTo(builder); + ContactResource newResource = builder + .setCreationClientId(getClientId()) + .setCurrentSponsorClientId(getClientId()) + .setRepoId(createContactHostRoid(ObjectifyService.allocateId())) + .build(); validateAsciiPostalInfo(newResource.getInternationalizedPostalInfo()); validateContactAgainstPolicy(newResource); - } - - @Override - protected boolean storeXmlInHistoryEntry() { - return false; - } - - @Override - protected final HistoryEntry.Type getHistoryEntryType() { - return HistoryEntry.Type.CONTACT_CREATE; + historyBuilder + .setType(HistoryEntry.Type.CONTACT_CREATE) + .setModificationTime(now) + .setXmlBytes(null) // We don't want to store contact details in the history entry. + .setParent(Key.create(newResource)); + ofy().save().entities( + newResource, + historyBuilder.build(), + ForeignKeyIndex.create(newResource, newResource.getDeletionTime()), + EppResourceIndex.create(Key.create(newResource))); + return createOutput(Success, ContactCreateData.create(newResource.getContactId(), now)); } } diff --git a/java/google/registry/flows/contact/ContactDeleteFlow.java b/java/google/registry/flows/contact/ContactDeleteFlow.java index f0424df2e..09d2affe3 100644 --- a/java/google/registry/flows/contact/ContactDeleteFlow.java +++ b/java/google/registry/flows/contact/ContactDeleteFlow.java @@ -14,61 +14,87 @@ package google.registry.flows.contact; -import static google.registry.model.EppResourceUtils.queryDomainsUsingResource; +import static google.registry.flows.ResourceFlowUtils.failfastForAsyncDelete; +import static google.registry.flows.ResourceFlowUtils.verifyAuthInfoForResource; +import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses; +import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership; +import static google.registry.model.EppResourceUtils.loadByUniqueId; +import static google.registry.model.eppoutput.Result.Code.SuccessWithActionPending; import static google.registry.model.ofy.ObjectifyService.ofy; -import com.google.common.base.Predicate; +import com.google.common.base.Function; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Iterables; +import com.google.common.collect.ImmutableSet; import com.googlecode.objectify.Key; -import google.registry.config.RegistryEnvironment; +import google.registry.config.ConfigModule.Config; import google.registry.flows.EppException; -import google.registry.flows.ResourceAsyncDeleteFlow; +import google.registry.flows.LoggedInFlow; +import google.registry.flows.TransactionalFlow; import google.registry.flows.async.AsyncFlowUtils; import google.registry.flows.async.DeleteContactResourceAction; import google.registry.flows.async.DeleteEppResourceAction; +import google.registry.flows.exceptions.ResourceToMutateDoesNotExistException; import google.registry.model.contact.ContactCommand.Delete; import google.registry.model.contact.ContactResource; -import google.registry.model.contact.ContactResource.Builder; import google.registry.model.domain.DomainBase; +import google.registry.model.domain.metadata.MetadataExtension; +import google.registry.model.eppcommon.StatusValue; +import google.registry.model.eppinput.ResourceCommand; +import google.registry.model.eppoutput.EppOutput; import google.registry.model.reporting.HistoryEntry; import javax.inject.Inject; +import org.joda.time.Duration; /** * An EPP flow that deletes a contact resource. * - * @error {@link google.registry.flows.ResourceAsyncDeleteFlow.ResourceToDeleteIsReferencedException} * @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException} - * @error {@link google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException} - * @error {@link google.registry.flows.SingleResourceFlow.ResourceStatusProhibitsOperationException} + * @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException} + * @error {@link google.registry.flows.exceptions.ResourceToDeleteIsReferencedException} + * @error {@link google.registry.flows.exceptions.ResourceToMutateDoesNotExistException} */ -public class ContactDeleteFlow extends ResourceAsyncDeleteFlow { +public class ContactDeleteFlow extends LoggedInFlow implements TransactionalFlow { - /** In {@link #isLinkedForFailfast}, check this (arbitrary) number of resources from the query. */ - private static final int FAILFAST_CHECK_COUNT = 5; + private static final ImmutableSet DISALLOWED_STATUSES = ImmutableSet.of( + StatusValue.LINKED, + StatusValue.CLIENT_DELETE_PROHIBITED, + StatusValue.PENDING_DELETE, + StatusValue.SERVER_DELETE_PROHIBITED); + @Inject ResourceCommand resourceCommand; + @Inject @Config("asyncDeleteFlowMapreduceDelay") Duration mapreduceDelay; + @Inject HistoryEntry.Builder historyBuilder; @Inject ContactDeleteFlow() {} @Override - protected boolean isLinkedForFailfast(final Key key) { - // Query for the first few linked domains, and if found, actually load them. The query is - // eventually consistent and so might be very stale, but the direct load will not be stale, - // just non-transactional. If we find at least one actual reference then we can reliably - // fail. If we don't find any, we can't trust the query and need to do the full mapreduce. - return Iterables.any( - ofy().load().keys( - queryDomainsUsingResource( - ContactResource.class, key, now, FAILFAST_CHECK_COUNT)).values(), - new Predicate() { - @Override - public boolean apply(DomainBase domain) { - return domain.getReferencedContacts().contains(key); - }}); + protected final void initLoggedInFlow() throws EppException { + registerExtensions(MetadataExtension.class); } - /** Enqueues a contact resource deletion on the mapreduce queue. */ @Override - protected final void enqueueTasks() throws EppException { + public final EppOutput run() throws EppException { + Delete command = (Delete) resourceCommand; + String targetId = command.getTargetId(); + failfastForAsyncDelete( + targetId, + now, + ContactResource.class, + new Function>() { + @Override + public ImmutableSet apply(DomainBase domain) { + return domain.getReferencedContacts(); + }}); + ContactResource existingResource = loadByUniqueId(ContactResource.class, targetId, now); + if (existingResource == null) { + throw new ResourceToMutateDoesNotExistException(ContactResource.class, targetId); + } + verifyNoDisallowedStatuses(existingResource, DISALLOWED_STATUSES); + if (command.getAuthInfo() != null) { + verifyAuthInfoForResource(command.getAuthInfo(), existingResource); + } + if (!isSuperuser) { + verifyResourceOwnership(getClientId(), existingResource); + } AsyncFlowUtils.enqueueMapreduceAction( DeleteContactResourceAction.class, ImmutableMap.of( @@ -78,11 +104,14 @@ public class ContactDeleteFlow extends ResourceAsyncDeleteFlowentities(newResource, historyBuilder.build()); + return createOutput(SuccessWithActionPending, null, null); } } diff --git a/java/google/registry/flows/contact/ContactInfoFlow.java b/java/google/registry/flows/contact/ContactInfoFlow.java index d935d39ca..350da8f24 100644 --- a/java/google/registry/flows/contact/ContactInfoFlow.java +++ b/java/google/registry/flows/contact/ContactInfoFlow.java @@ -14,17 +14,41 @@ package google.registry.flows.contact; -import google.registry.flows.ResourceInfoFlow; +import static google.registry.flows.ResourceFlowUtils.verifyAuthInfoForResource; +import static google.registry.model.EppResourceUtils.cloneResourceWithLinkedStatus; +import static google.registry.model.EppResourceUtils.loadByUniqueId; +import static google.registry.model.eppoutput.Result.Code.Success; + +import google.registry.flows.EppException; +import google.registry.flows.LoggedInFlow; +import google.registry.flows.exceptions.ResourceToQueryDoesNotExistException; import google.registry.model.contact.ContactCommand.Info; import google.registry.model.contact.ContactResource; +import google.registry.model.eppinput.ResourceCommand; +import google.registry.model.eppoutput.EppOutput; import javax.inject.Inject; /** * An EPP flow that reads a contact. * - * @error {@link google.registry.flows.ResourceQueryFlow.ResourceToQueryDoesNotExistException} + * @error {@link google.registry.flows.exceptions.ResourceToQueryDoesNotExistException} */ -public class ContactInfoFlow extends ResourceInfoFlow { - @Inject ContactInfoFlow() {} -} +public class ContactInfoFlow extends LoggedInFlow { + @Inject ResourceCommand resourceCommand; + @Inject ContactInfoFlow() {} + + @Override + public final EppOutput run() throws EppException { + Info command = (Info) resourceCommand; + String targetId = command.getTargetId(); + ContactResource existingResource = loadByUniqueId(ContactResource.class, targetId, now); + if (existingResource == null) { + throw new ResourceToQueryDoesNotExistException(ContactResource.class, targetId); + } + if (command.getAuthInfo() != null) { + verifyAuthInfoForResource(command.getAuthInfo(), existingResource); + } + return createOutput(Success, cloneResourceWithLinkedStatus(existingResource, now), null); + } +} diff --git a/java/google/registry/flows/contact/ContactTransferApproveFlow.java b/java/google/registry/flows/contact/ContactTransferApproveFlow.java index 724b11250..f68f6e27b 100644 --- a/java/google/registry/flows/contact/ContactTransferApproveFlow.java +++ b/java/google/registry/flows/contact/ContactTransferApproveFlow.java @@ -14,11 +14,31 @@ package google.registry.flows.contact; -import google.registry.flows.ResourceTransferApproveFlow; +import static google.registry.flows.ResourceFlowUtils.createPendingTransferNotificationResponse; +import static google.registry.flows.ResourceFlowUtils.createTransferResponse; +import static google.registry.flows.ResourceFlowUtils.verifyAuthInfoForResource; +import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership; +import static google.registry.model.EppResourceUtils.loadByUniqueId; +import static google.registry.model.eppoutput.Result.Code.Success; +import static google.registry.model.ofy.ObjectifyService.ofy; + +import com.google.common.collect.ImmutableList; +import com.googlecode.objectify.Key; +import google.registry.flows.EppException; +import google.registry.flows.LoggedInFlow; +import google.registry.flows.TransactionalFlow; +import google.registry.flows.exceptions.NotPendingTransferException; +import google.registry.flows.exceptions.ResourceToMutateDoesNotExistException; import google.registry.model.contact.ContactCommand.Transfer; import google.registry.model.contact.ContactResource; -import google.registry.model.contact.ContactResource.Builder; +import google.registry.model.domain.metadata.MetadataExtension; +import google.registry.model.eppcommon.StatusValue; +import google.registry.model.eppinput.ResourceCommand; +import google.registry.model.eppoutput.EppOutput; +import google.registry.model.poll.PollMessage; import google.registry.model.reporting.HistoryEntry; +import google.registry.model.transfer.TransferData; +import google.registry.model.transfer.TransferStatus; import javax.inject.Inject; /** @@ -26,16 +46,71 @@ import javax.inject.Inject; * * @error {@link google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException} * @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException} - * @error {@link google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException} - * @error {@link google.registry.flows.ResourceMutatePendingTransferFlow.NotPendingTransferException} + * @error {@link google.registry.flows.exceptions.NotPendingTransferException} + * @error {@link google.registry.flows.exceptions.ResourceToMutateDoesNotExistException} */ -public class ContactTransferApproveFlow - extends ResourceTransferApproveFlow { +public class ContactTransferApproveFlow extends LoggedInFlow implements TransactionalFlow { + @Inject ResourceCommand resourceCommand; + @Inject HistoryEntry.Builder historyBuilder; @Inject ContactTransferApproveFlow() {} @Override - protected final HistoryEntry.Type getHistoryEntryType() { - return HistoryEntry.Type.CONTACT_TRANSFER_APPROVE; + protected final void initLoggedInFlow() throws EppException { + registerExtensions(MetadataExtension.class); + } + + @Override + public final EppOutput run() throws EppException { + Transfer command = (Transfer) resourceCommand; + String targetId = command.getTargetId(); + ContactResource existingResource = loadByUniqueId(ContactResource.class, targetId, now); + if (existingResource == null) { + throw new ResourceToMutateDoesNotExistException(ContactResource.class, targetId); + } + if (command.getAuthInfo() != null) { + verifyAuthInfoForResource(command.getAuthInfo(), existingResource); + } + if (existingResource.getTransferData().getTransferStatus() != TransferStatus.PENDING) { + throw new NotPendingTransferException(targetId); + } + verifyResourceOwnership(getClientId(), existingResource); + TransferData oldTransferData = existingResource.getTransferData(); + ContactResource newResource = existingResource.asBuilder() + .removeStatusValue(StatusValue.PENDING_TRANSFER) + .setLastTransferTime(now) + .setCurrentSponsorClientId(existingResource.getTransferData().getGainingClientId()) + .setTransferData(oldTransferData.asBuilder() + .setTransferStatus(TransferStatus.CLIENT_APPROVED) + .setPendingTransferExpirationTime(now) + .setExtendedRegistrationYears(null) + .setServerApproveEntities(null) + .setServerApproveBillingEvent(null) + .setServerApproveAutorenewEvent(null) + .setServerApproveAutorenewPollMessage(null) + .build()) + .build(); + HistoryEntry historyEntry = historyBuilder + .setType(HistoryEntry.Type.CONTACT_TRANSFER_APPROVE) + .setModificationTime(now) + .setParent(Key.create(existingResource)) + .build(); + // Create a poll message for the gaining client. + PollMessage gainingPollMessage = new PollMessage.OneTime.Builder() + .setClientId(oldTransferData.getGainingClientId()) + .setEventTime(now) + .setMsg(TransferStatus.CLIENT_APPROVED.getMessage()) + .setResponseData(ImmutableList.of( + createTransferResponse(newResource, newResource.getTransferData(), now), + createPendingTransferNotificationResponse( + existingResource, oldTransferData.getTransferRequestTrid(), true, now))) + .setParent(historyEntry) + .build(); + ofy().save().entities(newResource, historyEntry, gainingPollMessage); + // Delete the billing event and poll messages that were written in case the transfer would have + // been implicitly server approved. + ofy().delete().keys(existingResource.getTransferData().getServerApproveEntities()); + return createOutput( + Success, createTransferResponse(newResource, newResource.getTransferData(), now)); } } diff --git a/java/google/registry/flows/contact/ContactTransferCancelFlow.java b/java/google/registry/flows/contact/ContactTransferCancelFlow.java index 52f786951..689ec4093 100644 --- a/java/google/registry/flows/contact/ContactTransferCancelFlow.java +++ b/java/google/registry/flows/contact/ContactTransferCancelFlow.java @@ -14,28 +14,101 @@ package google.registry.flows.contact; -import google.registry.flows.ResourceTransferCancelFlow; +import static google.registry.flows.ResourceFlowUtils.createTransferResponse; +import static google.registry.flows.ResourceFlowUtils.verifyAuthInfoForResource; +import static google.registry.model.EppResourceUtils.loadByUniqueId; +import static google.registry.model.eppoutput.Result.Code.Success; +import static google.registry.model.ofy.ObjectifyService.ofy; + +import com.google.common.collect.ImmutableList; +import com.googlecode.objectify.Key; +import google.registry.flows.EppException; +import google.registry.flows.LoggedInFlow; +import google.registry.flows.TransactionalFlow; +import google.registry.flows.exceptions.NotPendingTransferException; +import google.registry.flows.exceptions.NotTransferInitiatorException; +import google.registry.flows.exceptions.ResourceToMutateDoesNotExistException; import google.registry.model.contact.ContactCommand.Transfer; import google.registry.model.contact.ContactResource; -import google.registry.model.contact.ContactResource.Builder; +import google.registry.model.domain.metadata.MetadataExtension; +import google.registry.model.eppcommon.StatusValue; +import google.registry.model.eppinput.ResourceCommand; +import google.registry.model.eppoutput.EppOutput; +import google.registry.model.poll.PollMessage; import google.registry.model.reporting.HistoryEntry; +import google.registry.model.transfer.TransferStatus; import javax.inject.Inject; /** * An EPP flow that cancels a pending transfer on a {@link ContactResource}. * * @error {@link google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException} - * @error {@link google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException} - * @error {@link google.registry.flows.ResourceMutatePendingTransferFlow.NotPendingTransferException} - * @error {@link google.registry.flows.ResourceTransferCancelFlow.NotTransferInitiatorException} + * @error {@link google.registry.flows.exceptions.NotPendingTransferException} + * @error {@link google.registry.flows.exceptions.NotTransferInitiatorException} + * @error {@link google.registry.flows.exceptions.ResourceToMutateDoesNotExistException} */ -public class ContactTransferCancelFlow - extends ResourceTransferCancelFlow { +public class ContactTransferCancelFlow extends LoggedInFlow implements TransactionalFlow { + @Inject ResourceCommand resourceCommand; + @Inject HistoryEntry.Builder historyBuilder; @Inject ContactTransferCancelFlow() {} @Override - protected final HistoryEntry.Type getHistoryEntryType() { - return HistoryEntry.Type.CONTACT_TRANSFER_CANCEL; + protected final void initLoggedInFlow() throws EppException { + registerExtensions(MetadataExtension.class); + } + + @Override + protected final EppOutput run() throws EppException { + Transfer command = (Transfer) resourceCommand; + String targetId = command.getTargetId(); + ContactResource existingResource = loadByUniqueId(ContactResource.class, targetId, now); + // Fail if the object doesn't exist or was deleted. + if (existingResource == null) { + throw new ResourceToMutateDoesNotExistException(ContactResource.class, targetId); + } + if (command.getAuthInfo() != null) { + verifyAuthInfoForResource(command.getAuthInfo(), existingResource); + } + // Fail if object doesn't have a pending transfer, or if authinfo doesn't match. */ + if (existingResource.getTransferData().getTransferStatus() != TransferStatus.PENDING) { + throw new NotPendingTransferException(targetId); + } + // TODO(b/18997997): Determine if authInfo is necessary to cancel a transfer. + if (!getClientId().equals(existingResource.getTransferData().getGainingClientId())) { + throw new NotTransferInitiatorException(); + } + ContactResource newResource = existingResource.asBuilder() + .removeStatusValue(StatusValue.PENDING_TRANSFER) + .setTransferData(existingResource.getTransferData().asBuilder() + .setTransferStatus(TransferStatus.CLIENT_CANCELLED) + .setPendingTransferExpirationTime(now) + .setExtendedRegistrationYears(null) + .setServerApproveEntities(null) + .setServerApproveBillingEvent(null) + .setServerApproveAutorenewEvent(null) + .setServerApproveAutorenewPollMessage(null) + .build()) + .build(); + HistoryEntry historyEntry = historyBuilder + .setType(HistoryEntry.Type.CONTACT_TRANSFER_CANCEL) + .setModificationTime(now) + .setParent(Key.create(existingResource)) + .build(); + // Create a poll message for the losing client. + PollMessage losingPollMessage = new PollMessage.OneTime.Builder() + .setClientId(existingResource.getTransferData().getLosingClientId()) + .setEventTime(now) + .setMsg(TransferStatus.CLIENT_CANCELLED.getMessage()) + .setResponseData(ImmutableList.of( + createTransferResponse(newResource, newResource.getTransferData(), now))) + .setParent(historyEntry) + .build(); + ofy().save().entities(newResource, historyEntry, losingPollMessage); + // Delete the billing event and poll messages that were written in case the transfer would have + // been implicitly server approved. + ofy().delete().keys(existingResource.getTransferData().getServerApproveEntities()); + return createOutput( + Success, createTransferResponse(newResource, newResource.getTransferData(), now)); } } diff --git a/java/google/registry/flows/contact/ContactTransferQueryFlow.java b/java/google/registry/flows/contact/ContactTransferQueryFlow.java index 51d514d3a..5d7786dbb 100644 --- a/java/google/registry/flows/contact/ContactTransferQueryFlow.java +++ b/java/google/registry/flows/contact/ContactTransferQueryFlow.java @@ -14,19 +14,59 @@ package google.registry.flows.contact; -import google.registry.flows.ResourceTransferQueryFlow; +import static google.registry.flows.ResourceFlowUtils.createTransferResponse; +import static google.registry.flows.ResourceFlowUtils.verifyAuthInfoForResource; +import static google.registry.model.EppResourceUtils.loadByUniqueId; +import static google.registry.model.eppoutput.Result.Code.Success; + +import google.registry.flows.EppException; +import google.registry.flows.LoggedInFlow; +import google.registry.flows.exceptions.NoTransferHistoryToQueryException; +import google.registry.flows.exceptions.NotAuthorizedToViewTransferException; +import google.registry.flows.exceptions.ResourceToQueryDoesNotExistException; import google.registry.model.contact.ContactCommand.Transfer; import google.registry.model.contact.ContactResource; +import google.registry.model.eppinput.ResourceCommand; +import google.registry.model.eppoutput.EppOutput; import javax.inject.Inject; /** * An EPP flow that queries a pending transfer on a {@link ContactResource}. * * @error {@link google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException} - * @error {@link google.registry.flows.ResourceQueryFlow.ResourceToQueryDoesNotExistException} - * @error {@link google.registry.flows.ResourceTransferQueryFlow.NoTransferHistoryToQueryException} - * @error {@link google.registry.flows.ResourceTransferQueryFlow.NotAuthorizedToViewTransferException} + * @error {@link google.registry.flows.exceptions.NoTransferHistoryToQueryException} + * @error {@link google.registry.flows.exceptions.NotAuthorizedToViewTransferException} + * @error {@link google.registry.flows.exceptions.ResourceToQueryDoesNotExistException} */ -public class ContactTransferQueryFlow extends ResourceTransferQueryFlow { +public class ContactTransferQueryFlow extends LoggedInFlow { + + @Inject ResourceCommand resourceCommand; @Inject ContactTransferQueryFlow() {} + + @Override + public final EppOutput run() throws EppException { + Transfer command = (Transfer) resourceCommand; + String targetId = command.getTargetId(); + ContactResource existingResource = loadByUniqueId(ContactResource.class, targetId, now); + if (existingResource == null) { + throw new ResourceToQueryDoesNotExistException(ContactResource.class, targetId); + } + if (command.getAuthInfo() != null) { + verifyAuthInfoForResource(command.getAuthInfo(), existingResource); + } + // Most of the fields on the transfer response are required, so there's no way to return valid + // XML if the object has never been transferred (and hence the fields aren't populated). + if (existingResource.getTransferData().getTransferStatus() == null) { + throw new NoTransferHistoryToQueryException(); + } + // Note that the authorization info on the command (if present) has already been verified by the + // parent class. If it's present, then the other checks are unnecessary. + if (command.getAuthInfo() == null + && !getClientId().equals(existingResource.getTransferData().getGainingClientId()) + && !getClientId().equals(existingResource.getTransferData().getLosingClientId())) { + throw new NotAuthorizedToViewTransferException(); + } + return createOutput( + Success, createTransferResponse(existingResource, existingResource.getTransferData(), now)); + } } diff --git a/java/google/registry/flows/contact/ContactTransferRejectFlow.java b/java/google/registry/flows/contact/ContactTransferRejectFlow.java index 78a93c8f7..b564399e4 100644 --- a/java/google/registry/flows/contact/ContactTransferRejectFlow.java +++ b/java/google/registry/flows/contact/ContactTransferRejectFlow.java @@ -14,11 +14,31 @@ package google.registry.flows.contact; -import google.registry.flows.ResourceTransferRejectFlow; +import static google.registry.flows.ResourceFlowUtils.createPendingTransferNotificationResponse; +import static google.registry.flows.ResourceFlowUtils.createTransferResponse; +import static google.registry.flows.ResourceFlowUtils.verifyAuthInfoForResource; +import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership; +import static google.registry.model.EppResourceUtils.loadByUniqueId; +import static google.registry.model.eppoutput.Result.Code.Success; +import static google.registry.model.ofy.ObjectifyService.ofy; + +import com.google.common.collect.ImmutableList; +import com.googlecode.objectify.Key; +import google.registry.flows.EppException; +import google.registry.flows.LoggedInFlow; +import google.registry.flows.TransactionalFlow; +import google.registry.flows.exceptions.NotPendingTransferException; +import google.registry.flows.exceptions.ResourceToMutateDoesNotExistException; import google.registry.model.contact.ContactCommand.Transfer; import google.registry.model.contact.ContactResource; -import google.registry.model.contact.ContactResource.Builder; +import google.registry.model.domain.metadata.MetadataExtension; +import google.registry.model.eppcommon.StatusValue; +import google.registry.model.eppinput.ResourceCommand; +import google.registry.model.eppoutput.EppOutput; +import google.registry.model.poll.PollMessage; import google.registry.model.reporting.HistoryEntry; +import google.registry.model.transfer.TransferData; +import google.registry.model.transfer.TransferStatus; import javax.inject.Inject; /** @@ -26,16 +46,68 @@ import javax.inject.Inject; * * @error {@link google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException} * @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException} - * @error {@link google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException} - * @error {@link google.registry.flows.ResourceMutatePendingTransferFlow.NotPendingTransferException} + * @error {@link google.registry.flows.exceptions.NotPendingTransferException} + * @error {@link google.registry.flows.exceptions.ResourceToMutateDoesNotExistException} */ -public class ContactTransferRejectFlow - extends ResourceTransferRejectFlow { +public class ContactTransferRejectFlow extends LoggedInFlow implements TransactionalFlow { + @Inject ResourceCommand resourceCommand; + @Inject HistoryEntry.Builder historyBuilder; @Inject ContactTransferRejectFlow() {} @Override - protected final HistoryEntry.Type getHistoryEntryType() { - return HistoryEntry.Type.CONTACT_TRANSFER_REJECT; + protected final void initLoggedInFlow() throws EppException { + registerExtensions(MetadataExtension.class); + } + + @Override + protected final EppOutput run() throws EppException { + Transfer command = (Transfer) resourceCommand; + String targetId = command.getTargetId(); + ContactResource existingResource = loadByUniqueId(ContactResource.class, targetId, now); + if (existingResource == null) { + throw new ResourceToMutateDoesNotExistException(ContactResource.class, targetId); + } + if (command.getAuthInfo() != null) { + verifyAuthInfoForResource(command.getAuthInfo(), existingResource); + } + if (existingResource.getTransferData().getTransferStatus() != TransferStatus.PENDING) { + throw new NotPendingTransferException(targetId); + } + verifyResourceOwnership(getClientId(), existingResource); + TransferData oldTransferData = existingResource.getTransferData(); + ContactResource newResource = existingResource.asBuilder() + .removeStatusValue(StatusValue.PENDING_TRANSFER) + .setTransferData(oldTransferData.asBuilder() + .setTransferStatus(TransferStatus.CLIENT_REJECTED) + .setPendingTransferExpirationTime(now) + .setExtendedRegistrationYears(null) + .setServerApproveEntities(null) + .setServerApproveBillingEvent(null) + .setServerApproveAutorenewEvent(null) + .setServerApproveAutorenewPollMessage(null) + .build()) + .build(); + HistoryEntry historyEntry = historyBuilder + .setType(HistoryEntry.Type.CONTACT_TRANSFER_REJECT) + .setModificationTime(now) + .setParent(Key.create(existingResource)) + .build(); + PollMessage.OneTime gainingPollMessage = new PollMessage.OneTime.Builder() + .setClientId(oldTransferData.getGainingClientId()) + .setEventTime(now) + .setMsg(TransferStatus.CLIENT_REJECTED.getMessage()) + .setResponseData(ImmutableList.of( + createTransferResponse(newResource, newResource.getTransferData(), now), + createPendingTransferNotificationResponse( + existingResource, oldTransferData.getTransferRequestTrid(), false, now))) + .setParent(historyEntry) + .build(); + ofy().save().entities(newResource, historyEntry, gainingPollMessage); + // Delete the billing event and poll messages that were written in case the transfer would have + // been implicitly server approved. + ofy().delete().keys(existingResource.getTransferData().getServerApproveEntities()); + return createOutput( + Success, createTransferResponse(newResource, newResource.getTransferData(), now)); } } diff --git a/java/google/registry/flows/contact/ContactTransferRequestFlow.java b/java/google/registry/flows/contact/ContactTransferRequestFlow.java index 546619bbe..357b81ccc 100644 --- a/java/google/registry/flows/contact/ContactTransferRequestFlow.java +++ b/java/google/registry/flows/contact/ContactTransferRequestFlow.java @@ -14,35 +14,151 @@ package google.registry.flows.contact; -import google.registry.config.RegistryEnvironment; -import google.registry.flows.ResourceTransferRequestFlow; +import static google.registry.flows.ResourceFlowUtils.createPendingTransferNotificationResponse; +import static google.registry.flows.ResourceFlowUtils.createTransferResponse; +import static google.registry.flows.ResourceFlowUtils.verifyAuthInfoForResource; +import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses; +import static google.registry.model.EppResourceUtils.loadByUniqueId; +import static google.registry.model.eppoutput.Result.Code.SuccessWithActionPending; +import static google.registry.model.ofy.ObjectifyService.ofy; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.googlecode.objectify.Key; +import google.registry.config.ConfigModule.Config; +import google.registry.flows.EppException; +import google.registry.flows.LoggedInFlow; +import google.registry.flows.TransactionalFlow; +import google.registry.flows.exceptions.AlreadyPendingTransferException; +import google.registry.flows.exceptions.MissingTransferRequestAuthInfoException; +import google.registry.flows.exceptions.ObjectAlreadySponsoredException; +import google.registry.flows.exceptions.ResourceToMutateDoesNotExistException; import google.registry.model.contact.ContactCommand.Transfer; import google.registry.model.contact.ContactResource; +import google.registry.model.domain.metadata.MetadataExtension; +import google.registry.model.eppcommon.StatusValue; +import google.registry.model.eppinput.ResourceCommand; +import google.registry.model.eppoutput.EppOutput; +import google.registry.model.poll.PollMessage; import google.registry.model.reporting.HistoryEntry; +import google.registry.model.transfer.TransferData; +import google.registry.model.transfer.TransferStatus; import javax.inject.Inject; +import org.joda.time.DateTime; import org.joda.time.Duration; /** * An EPP flow that requests a transfer on a {@link ContactResource}. * * @error {@link google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException} - * @error {@link google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException} - * @error {@link google.registry.flows.ResourceTransferRequestFlow.AlreadyPendingTransferException} - * @error {@link google.registry.flows.ResourceTransferRequestFlow.MissingTransferRequestAuthInfoException} - * @error {@link google.registry.flows.ResourceTransferRequestFlow.ObjectAlreadySponsoredException} + * @error {@link google.registry.flows.exceptions.ResourceToMutateDoesNotExistException} + * @error {@link google.registry.flows.exceptions.AlreadyPendingTransferException} + * @error {@link google.registry.flows.exceptions.MissingTransferRequestAuthInfoException} + * @error {@link google.registry.flows.exceptions.ObjectAlreadySponsoredException} */ -public class ContactTransferRequestFlow - extends ResourceTransferRequestFlow { +public class ContactTransferRequestFlow extends LoggedInFlow implements TransactionalFlow { + private static final ImmutableSet DISALLOWED_STATUSES = ImmutableSet.of( + StatusValue.CLIENT_TRANSFER_PROHIBITED, + StatusValue.PENDING_DELETE, + StatusValue.SERVER_TRANSFER_PROHIBITED); + + @Inject ResourceCommand resourceCommand; + @Inject @Config("contactAutomaticTransferLength") Duration automaticTransferLength; + @Inject HistoryEntry.Builder historyBuilder; @Inject ContactTransferRequestFlow() {} @Override - protected final HistoryEntry.Type getHistoryEntryType() { - return HistoryEntry.Type.CONTACT_TRANSFER_REQUEST; + protected final void initLoggedInFlow() throws EppException { + registerExtensions(MetadataExtension.class); } @Override - protected Duration getAutomaticTransferLength() { - return RegistryEnvironment.get().config().getContactAutomaticTransferLength(); + protected final EppOutput run() throws EppException { + Transfer command = (Transfer) resourceCommand; + String targetId = command.getTargetId(); + ContactResource existingResource = loadByUniqueId(ContactResource.class, targetId, now); + if (existingResource == null) { + throw new ResourceToMutateDoesNotExistException(ContactResource.class, targetId); + } + if (command.getAuthInfo() == null) { + throw new MissingTransferRequestAuthInfoException(); + } + verifyAuthInfoForResource(command.getAuthInfo(), existingResource); + // Verify that the resource does not already have a pending transfer. + if (TransferStatus.PENDING.equals(existingResource.getTransferData().getTransferStatus())) { + throw new AlreadyPendingTransferException(targetId); + } + String gainingClientId = getClientId(); + String losingClientId = existingResource.getCurrentSponsorClientId(); + // Verify that this client doesn't already sponsor this resource. + if (gainingClientId.equals(losingClientId)) { + throw new ObjectAlreadySponsoredException(); + } + verifyAuthInfoForResource(command.getAuthInfo(), existingResource); + verifyNoDisallowedStatuses(existingResource, DISALLOWED_STATUSES); + HistoryEntry historyEntry = historyBuilder + .setType(HistoryEntry.Type.CONTACT_TRANSFER_REQUEST) + .setModificationTime(now) + .setParent(Key.create(existingResource)) + .build(); + DateTime transferExpirationTime = now.plus(automaticTransferLength); + TransferData serverApproveTransferData = new TransferData.Builder() + .setTransferRequestTime(now) + .setTransferRequestTrid(trid) + .setGainingClientId(gainingClientId) + .setLosingClientId(losingClientId) + .setPendingTransferExpirationTime(transferExpirationTime) + .setTransferStatus(TransferStatus.SERVER_APPROVED) + .build(); + // If the transfer is server approved, this message will be sent to the losing registrar. */ + PollMessage.OneTime serverApproveLosingPollMessage = new PollMessage.OneTime.Builder() + .setClientId(losingClientId) + .setMsg(TransferStatus.SERVER_APPROVED.getMessage()) + .setParent(historyEntry) + .setEventTime(transferExpirationTime) + .setResponseData(ImmutableList.of( + createTransferResponse(existingResource, serverApproveTransferData, now))) + .build(); + // If the transfer is server approved, this message will be sent to the gaining registrar. */ + PollMessage.OneTime serverApproveGainingPollMessage = new PollMessage.OneTime.Builder() + .setClientId(gainingClientId) + .setMsg(TransferStatus.SERVER_APPROVED.getMessage()) + .setParent(historyEntry) + .setEventTime(transferExpirationTime) + .setResponseData(ImmutableList.of( + createTransferResponse(existingResource, serverApproveTransferData, now), + createPendingTransferNotificationResponse(existingResource, trid, true, now))) + .build(); + TransferData pendingTransferData = serverApproveTransferData.asBuilder() + .setTransferStatus(TransferStatus.PENDING) + .setServerApproveEntities( + ImmutableSet.>of( + Key.create(serverApproveGainingPollMessage), + Key.create(serverApproveLosingPollMessage))) + .build(); + // When a transfer is requested, a poll message is created to notify the losing registrar. + PollMessage.OneTime requestPollMessage = new PollMessage.OneTime.Builder() + .setClientId(losingClientId) + .setMsg(TransferStatus.PENDING.getMessage()) + .setParent(historyEntry) + .setEventTime(now) + .setResponseData(ImmutableList.of( + createTransferResponse(existingResource, pendingTransferData, now))) + .build(); + ContactResource newResource = existingResource.asBuilder() + .setTransferData(pendingTransferData) + .addStatusValue(StatusValue.PENDING_TRANSFER) + .build(); + ofy().save().entities( + newResource, + historyEntry, + requestPollMessage, + serverApproveGainingPollMessage, + serverApproveLosingPollMessage); + return createOutput( + SuccessWithActionPending, + createTransferResponse(newResource, newResource.getTransferData(), now), + null); } } diff --git a/java/google/registry/flows/contact/ContactUpdateFlow.java b/java/google/registry/flows/contact/ContactUpdateFlow.java index de0c854e4..0f0b19fda 100644 --- a/java/google/registry/flows/contact/ContactUpdateFlow.java +++ b/java/google/registry/flows/contact/ContactUpdateFlow.java @@ -14,14 +14,33 @@ package google.registry.flows.contact; +import static google.registry.flows.ResourceFlowUtils.verifyAuthInfoForResource; +import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses; +import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership; import static google.registry.flows.contact.ContactFlowUtils.validateAsciiPostalInfo; import static google.registry.flows.contact.ContactFlowUtils.validateContactAgainstPolicy; +import static google.registry.model.EppResourceUtils.loadByUniqueId; +import static google.registry.model.eppoutput.Result.Code.Success; +import static google.registry.model.ofy.ObjectifyService.ofy; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import com.googlecode.objectify.Key; import google.registry.flows.EppException; -import google.registry.flows.ResourceUpdateFlow; +import google.registry.flows.LoggedInFlow; +import google.registry.flows.TransactionalFlow; +import google.registry.flows.exceptions.AddRemoveSameValueEppException; +import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException; +import google.registry.flows.exceptions.ResourceToMutateDoesNotExistException; +import google.registry.flows.exceptions.StatusNotClientSettableException; import google.registry.model.contact.ContactCommand.Update; import google.registry.model.contact.ContactResource; import google.registry.model.contact.ContactResource.Builder; +import google.registry.model.domain.metadata.MetadataExtension; +import google.registry.model.eppcommon.StatusValue; +import google.registry.model.eppinput.ResourceCommand; +import google.registry.model.eppinput.ResourceCommand.AddRemoveSameValueException; +import google.registry.model.eppoutput.EppOutput; import google.registry.model.reporting.HistoryEntry; import javax.inject.Inject; @@ -29,31 +48,81 @@ import javax.inject.Inject; * An EPP flow that updates a contact resource. * * @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException} - * @error {@link google.registry.flows.ResourceUpdateFlow.AddRemoveSameValueEppException} - * @error {@link google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException} - * @error {@link google.registry.flows.ResourceUpdateFlow.ResourceHasClientUpdateProhibitedException} - * @error {@link google.registry.flows.ResourceUpdateFlow.StatusNotClientSettableException} - * @error {@link google.registry.flows.SingleResourceFlow.ResourceStatusProhibitsOperationException} + * @error {@link google.registry.flows.exceptions.AddRemoveSameValueEppException} + * @error {@link google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException} + * @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException} + * @error {@link google.registry.flows.exceptions.ResourceToMutateDoesNotExistException} + * @error {@link google.registry.flows.exceptions.StatusNotClientSettableException} * @error {@link ContactFlowUtils.BadInternationalizedPostalInfoException} * @error {@link ContactFlowUtils.DeclineContactDisclosureFieldDisallowedPolicyException} */ -public class ContactUpdateFlow extends ResourceUpdateFlow { +public class ContactUpdateFlow extends LoggedInFlow implements TransactionalFlow { + /** + * Note that CLIENT_UPDATE_PROHIBITED is intentionally not in this list. This is because it + * requires special checking, since you must be able to clear the status off the object with an + * update. + */ + private static final ImmutableSet DISALLOWED_STATUSES = ImmutableSet.of( + StatusValue.PENDING_DELETE, + StatusValue.SERVER_UPDATE_PROHIBITED); + + @Inject ResourceCommand resourceCommand; + @Inject HistoryEntry.Builder historyBuilder; @Inject ContactUpdateFlow() {} @Override - protected void verifyNewUpdatedStateIsAllowed() throws EppException { + protected final void initLoggedInFlow() throws EppException { + registerExtensions(MetadataExtension.class); + } + + @Override + public final EppOutput run() throws EppException { + Update command = (Update) resourceCommand; + String targetId = command.getTargetId(); + ContactResource existingResource = loadByUniqueId(ContactResource.class, targetId, now); + if (existingResource == null) { + throw new ResourceToMutateDoesNotExistException(ContactResource.class, targetId); + } + if (command.getAuthInfo() != null) { + verifyAuthInfoForResource(command.getAuthInfo(), existingResource); + } + if (!isSuperuser) { + verifyResourceOwnership(getClientId(), existingResource); + } + for (StatusValue statusValue : Sets.union( + command.getInnerAdd().getStatusValues(), + command.getInnerRemove().getStatusValues())) { + if (!isSuperuser && !statusValue.isClientSettable()) { // The superuser can set any status. + throw new StatusNotClientSettableException(statusValue.getXmlName()); + } + } + verifyNoDisallowedStatuses(existingResource, DISALLOWED_STATUSES); + historyBuilder + .setType(HistoryEntry.Type.CONTACT_UPDATE) + .setModificationTime(now) + .setXmlBytes(null) // We don't want to store contact details in the history entry. + .setParent(Key.create(existingResource)); + Builder builder = existingResource.asBuilder(); + try { + command.applyTo(builder); + } catch (AddRemoveSameValueException e) { + throw new AddRemoveSameValueEppException(); + } + ContactResource newResource = builder + .setLastEppUpdateTime(now) + .setLastEppUpdateClientId(getClientId()) + .build(); + // If the resource is marked with clientUpdateProhibited, and this update did not clear that + // status, then the update must be disallowed (unless a superuser is requesting the change). + if (!isSuperuser + && existingResource.getStatusValues().contains(StatusValue.CLIENT_UPDATE_PROHIBITED) + && newResource.getStatusValues().contains(StatusValue.CLIENT_UPDATE_PROHIBITED)) { + throw new ResourceHasClientUpdateProhibitedException(); + } validateAsciiPostalInfo(newResource.getInternationalizedPostalInfo()); validateContactAgainstPolicy(newResource); - } - - @Override - protected boolean storeXmlInHistoryEntry() { - return false; - } - - @Override - protected final HistoryEntry.Type getHistoryEntryType() { - return HistoryEntry.Type.CONTACT_UPDATE; + ofy().save().entities(newResource, historyBuilder.build()); + return createOutput(Success); } } diff --git a/javatests/google/registry/flows/EppLifecycleContactTest.java b/javatests/google/registry/flows/EppLifecycleContactTest.java index 05f936d52..fd6e272ae 100644 --- a/javatests/google/registry/flows/EppLifecycleContactTest.java +++ b/javatests/google/registry/flows/EppLifecycleContactTest.java @@ -76,7 +76,7 @@ public class EppLifecycleContactTest extends EppTestCase { DateTime.parse("2000-06-08T22:01:00Z")); assertCommandAndResponse( "poll_ack.xml", - ImmutableMap.of("ID", "2-1-ROID-3-4"), + ImmutableMap.of("ID", "2-1-ROID-3-6"), "poll_ack_response_empty.xml", null, DateTime.parse("2000-06-08T22:02:00Z")); diff --git a/javatests/google/registry/flows/contact/ContactCheckFlowTest.java b/javatests/google/registry/flows/contact/ContactCheckFlowTest.java index f028f6478..b951a97c3 100644 --- a/javatests/google/registry/flows/contact/ContactCheckFlowTest.java +++ b/javatests/google/registry/flows/contact/ContactCheckFlowTest.java @@ -18,8 +18,8 @@ import static google.registry.model.eppoutput.CheckData.ContactCheck.create; import static google.registry.testing.DatastoreHelper.persistActiveContact; import static google.registry.testing.DatastoreHelper.persistDeletedContact; -import google.registry.flows.ResourceCheckFlow.TooManyResourceChecksException; import google.registry.flows.ResourceCheckFlowTestCase; +import google.registry.flows.exceptions.TooManyResourceChecksException; import google.registry.model.contact.ContactResource; import org.junit.Test; diff --git a/javatests/google/registry/flows/contact/ContactCreateFlowTest.java b/javatests/google/registry/flows/contact/ContactCreateFlowTest.java index 463d8e4f6..26e1f0435 100644 --- a/javatests/google/registry/flows/contact/ContactCreateFlowTest.java +++ b/javatests/google/registry/flows/contact/ContactCreateFlowTest.java @@ -19,10 +19,10 @@ import static google.registry.testing.DatastoreHelper.assertNoBillingEvents; import static google.registry.testing.DatastoreHelper.persistActiveContact; import static google.registry.testing.DatastoreHelper.persistDeletedContact; -import google.registry.flows.ResourceCreateFlow.ResourceAlreadyExistsException; import google.registry.flows.ResourceFlowTestCase; import google.registry.flows.contact.ContactFlowUtils.BadInternationalizedPostalInfoException; import google.registry.flows.contact.ContactFlowUtils.DeclineContactDisclosureFieldDisallowedPolicyException; +import google.registry.flows.exceptions.ResourceAlreadyExistsException; import google.registry.model.contact.ContactResource; import org.joda.time.DateTime; import org.junit.Test; diff --git a/javatests/google/registry/flows/contact/ContactDeleteFlowTest.java b/javatests/google/registry/flows/contact/ContactDeleteFlowTest.java index 2a03454c0..ec2ea08a8 100644 --- a/javatests/google/registry/flows/contact/ContactDeleteFlowTest.java +++ b/javatests/google/registry/flows/contact/ContactDeleteFlowTest.java @@ -28,13 +28,13 @@ import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued; import com.google.common.collect.ImmutableSet; import com.googlecode.objectify.Key; -import google.registry.flows.ResourceAsyncDeleteFlow.ResourceToDeleteIsReferencedException; import google.registry.flows.ResourceFlowTestCase; import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException; -import google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException; -import google.registry.flows.SingleResourceFlow.ResourceStatusProhibitsOperationException; import google.registry.flows.async.DeleteContactResourceAction; import google.registry.flows.async.DeleteEppResourceAction; +import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException; +import google.registry.flows.exceptions.ResourceToDeleteIsReferencedException; +import google.registry.flows.exceptions.ResourceToMutateDoesNotExistException; import google.registry.model.contact.ContactResource; import google.registry.model.eppcommon.StatusValue; import google.registry.model.reporting.HistoryEntry; diff --git a/javatests/google/registry/flows/contact/ContactInfoFlowTest.java b/javatests/google/registry/flows/contact/ContactInfoFlowTest.java index 9c130fa42..d0c87dd0f 100644 --- a/javatests/google/registry/flows/contact/ContactInfoFlowTest.java +++ b/javatests/google/registry/flows/contact/ContactInfoFlowTest.java @@ -24,7 +24,7 @@ import static google.registry.testing.DatastoreHelper.persistResource; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import google.registry.flows.ResourceFlowTestCase; -import google.registry.flows.ResourceQueryFlow.ResourceToQueryDoesNotExistException; +import google.registry.flows.exceptions.ResourceToQueryDoesNotExistException; import google.registry.model.contact.ContactAddress; import google.registry.model.contact.ContactAuthInfo; import google.registry.model.contact.ContactPhoneNumber; diff --git a/javatests/google/registry/flows/contact/ContactTransferApproveFlowTest.java b/javatests/google/registry/flows/contact/ContactTransferApproveFlowTest.java index 964a574f0..3582079c0 100644 --- a/javatests/google/registry/flows/contact/ContactTransferApproveFlowTest.java +++ b/javatests/google/registry/flows/contact/ContactTransferApproveFlowTest.java @@ -27,8 +27,8 @@ import com.google.common.collect.FluentIterable; import com.google.common.collect.Iterables; import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException; import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException; -import google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException; -import google.registry.flows.ResourceMutatePendingTransferFlow.NotPendingTransferException; +import google.registry.flows.exceptions.NotPendingTransferException; +import google.registry.flows.exceptions.ResourceToMutateDoesNotExistException; import google.registry.model.contact.ContactAuthInfo; import google.registry.model.contact.ContactResource; import google.registry.model.eppcommon.AuthInfo.PasswordAuth; diff --git a/javatests/google/registry/flows/contact/ContactTransferCancelFlowTest.java b/javatests/google/registry/flows/contact/ContactTransferCancelFlowTest.java index aad737471..7e0e306b3 100644 --- a/javatests/google/registry/flows/contact/ContactTransferCancelFlowTest.java +++ b/javatests/google/registry/flows/contact/ContactTransferCancelFlowTest.java @@ -25,9 +25,9 @@ import static google.registry.testing.DatastoreHelper.persistResource; import com.google.common.collect.FluentIterable; import com.google.common.collect.Iterables; import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException; -import google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException; -import google.registry.flows.ResourceMutatePendingTransferFlow.NotPendingTransferException; -import google.registry.flows.ResourceTransferCancelFlow.NotTransferInitiatorException; +import google.registry.flows.exceptions.NotPendingTransferException; +import google.registry.flows.exceptions.NotTransferInitiatorException; +import google.registry.flows.exceptions.ResourceToMutateDoesNotExistException; import google.registry.model.contact.ContactAuthInfo; import google.registry.model.contact.ContactResource; import google.registry.model.eppcommon.AuthInfo.PasswordAuth; diff --git a/javatests/google/registry/flows/contact/ContactTransferQueryFlowTest.java b/javatests/google/registry/flows/contact/ContactTransferQueryFlowTest.java index d3f28b77d..5302aaf46 100644 --- a/javatests/google/registry/flows/contact/ContactTransferQueryFlowTest.java +++ b/javatests/google/registry/flows/contact/ContactTransferQueryFlowTest.java @@ -20,9 +20,9 @@ import static google.registry.testing.DatastoreHelper.deleteResource; import static google.registry.testing.DatastoreHelper.persistResource; import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException; -import google.registry.flows.ResourceQueryFlow.ResourceToQueryDoesNotExistException; -import google.registry.flows.ResourceTransferQueryFlow.NoTransferHistoryToQueryException; -import google.registry.flows.ResourceTransferQueryFlow.NotAuthorizedToViewTransferException; +import google.registry.flows.exceptions.NoTransferHistoryToQueryException; +import google.registry.flows.exceptions.NotAuthorizedToViewTransferException; +import google.registry.flows.exceptions.ResourceToQueryDoesNotExistException; import google.registry.model.contact.ContactAuthInfo; import google.registry.model.contact.ContactResource; import google.registry.model.eppcommon.AuthInfo.PasswordAuth; diff --git a/javatests/google/registry/flows/contact/ContactTransferRejectFlowTest.java b/javatests/google/registry/flows/contact/ContactTransferRejectFlowTest.java index d34f79354..f483a2569 100644 --- a/javatests/google/registry/flows/contact/ContactTransferRejectFlowTest.java +++ b/javatests/google/registry/flows/contact/ContactTransferRejectFlowTest.java @@ -26,8 +26,8 @@ import com.google.common.collect.FluentIterable; import com.google.common.collect.Iterables; import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException; import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException; -import google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException; -import google.registry.flows.ResourceMutatePendingTransferFlow.NotPendingTransferException; +import google.registry.flows.exceptions.NotPendingTransferException; +import google.registry.flows.exceptions.ResourceToMutateDoesNotExistException; import google.registry.model.contact.ContactAuthInfo; import google.registry.model.contact.ContactResource; import google.registry.model.eppcommon.AuthInfo.PasswordAuth; diff --git a/javatests/google/registry/flows/contact/ContactTransferRequestFlowTest.java b/javatests/google/registry/flows/contact/ContactTransferRequestFlowTest.java index 3b5e80777..ebcfd12b8 100644 --- a/javatests/google/registry/flows/contact/ContactTransferRequestFlowTest.java +++ b/javatests/google/registry/flows/contact/ContactTransferRequestFlowTest.java @@ -24,10 +24,10 @@ import static google.registry.testing.DatastoreHelper.persistResource; import google.registry.config.RegistryEnvironment; import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException; -import google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException; -import google.registry.flows.ResourceTransferRequestFlow.AlreadyPendingTransferException; -import google.registry.flows.ResourceTransferRequestFlow.MissingTransferRequestAuthInfoException; -import google.registry.flows.ResourceTransferRequestFlow.ObjectAlreadySponsoredException; +import google.registry.flows.exceptions.AlreadyPendingTransferException; +import google.registry.flows.exceptions.MissingTransferRequestAuthInfoException; +import google.registry.flows.exceptions.ObjectAlreadySponsoredException; +import google.registry.flows.exceptions.ResourceToMutateDoesNotExistException; import google.registry.model.contact.ContactAuthInfo; import google.registry.model.contact.ContactResource; import google.registry.model.eppcommon.AuthInfo.PasswordAuth; diff --git a/javatests/google/registry/flows/contact/ContactUpdateFlowTest.java b/javatests/google/registry/flows/contact/ContactUpdateFlowTest.java index b8f5752b0..9cb937c92 100644 --- a/javatests/google/registry/flows/contact/ContactUpdateFlowTest.java +++ b/javatests/google/registry/flows/contact/ContactUpdateFlowTest.java @@ -25,13 +25,13 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import google.registry.flows.ResourceFlowTestCase; import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException; -import google.registry.flows.ResourceMutateFlow.ResourceToMutateDoesNotExistException; -import google.registry.flows.ResourceUpdateFlow.AddRemoveSameValueEppException; -import google.registry.flows.ResourceUpdateFlow.ResourceHasClientUpdateProhibitedException; -import google.registry.flows.ResourceUpdateFlow.StatusNotClientSettableException; -import google.registry.flows.SingleResourceFlow.ResourceStatusProhibitsOperationException; import google.registry.flows.contact.ContactFlowUtils.BadInternationalizedPostalInfoException; import google.registry.flows.contact.ContactFlowUtils.DeclineContactDisclosureFieldDisallowedPolicyException; +import google.registry.flows.exceptions.AddRemoveSameValueEppException; +import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException; +import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException; +import google.registry.flows.exceptions.ResourceToMutateDoesNotExistException; +import google.registry.flows.exceptions.StatusNotClientSettableException; import google.registry.model.contact.ContactAddress; import google.registry.model.contact.ContactResource; import google.registry.model.contact.PostalInfo; diff --git a/javatests/google/registry/flows/testdata/poll_response_contact_transfer.xml b/javatests/google/registry/flows/testdata/poll_response_contact_transfer.xml index 5c38f5b85..641961da3 100644 --- a/javatests/google/registry/flows/testdata/poll_response_contact_transfer.xml +++ b/javatests/google/registry/flows/testdata/poll_response_contact_transfer.xml @@ -3,7 +3,7 @@ Command completed successfully; ack to dequeue - + 2000-06-08T22:00:00Z Transfer requested. From 3978b4f169e5d2fe768aa1bca70d483827447ebf Mon Sep 17 00:00:00 2001 From: cgoldfeder Date: Wed, 14 Sep 2016 08:35:36 -0700 Subject: [PATCH 29/31] Get rid of @Nullable on the injected client id This allows us to inject an optional once, in FlowRunner, and inject a non-null value in the flows (not done yet, after this goes in). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=133130485 --- java/google/registry/flows/FlowModule.java | 9 +++++---- java/google/registry/flows/FlowRunner.java | 6 ++---- javatests/google/registry/flows/FlowRunnerTest.java | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/java/google/registry/flows/FlowModule.java b/java/google/registry/flows/FlowModule.java index 0d1dfe6ff..aab0f04ac 100644 --- a/java/google/registry/flows/FlowModule.java +++ b/java/google/registry/flows/FlowModule.java @@ -16,6 +16,7 @@ package google.registry.flows; import static com.google.common.base.Preconditions.checkState; +import com.google.common.base.Strings; import dagger.Module; import dagger.Provides; import google.registry.flows.exceptions.OnlyToolCanPassMetadataException; @@ -27,7 +28,6 @@ import google.registry.model.eppinput.EppInput.ResourceCommandWrapper; import google.registry.model.eppinput.ResourceCommand; import google.registry.model.reporting.HistoryEntry; import java.lang.annotation.Documented; -import javax.annotation.Nullable; import javax.inject.Qualifier; /** Module to choose and instantiate an EPP flow. */ @@ -147,10 +147,11 @@ public class FlowModule { @Provides @FlowScope - @Nullable @ClientId static String provideClientId(SessionMetadata sessionMetadata) { - return sessionMetadata.getClientId(); + // Treat a missing clientId as null so we can always inject a non-null value. All we do with the + // clientId is log it (as "") or detect its absence, both of which work fine with empty. + return Strings.nullToEmpty(sessionMetadata.getClientId()); } @Provides @@ -187,7 +188,7 @@ public class FlowModule { Trid trid, @InputXml byte[] inputXmlBytes, @Superuser boolean isSuperuser, - @ClientId @Nullable String clientId, + @ClientId String clientId, EppRequestSource eppRequestSource, EppInput eppInput) { HistoryEntry.Builder historyBuilder = new HistoryEntry.Builder() diff --git a/java/google/registry/flows/FlowRunner.java b/java/google/registry/flows/FlowRunner.java index bf94c110d..0e1a95377 100644 --- a/java/google/registry/flows/FlowRunner.java +++ b/java/google/registry/flows/FlowRunner.java @@ -14,7 +14,6 @@ package google.registry.flows; -import static com.google.common.base.Strings.nullToEmpty; import static com.google.common.base.Throwables.getStackTraceAsString; import static com.google.common.io.BaseEncoding.base64; import static google.registry.model.ofy.ObjectifyService.ofy; @@ -34,7 +33,6 @@ import google.registry.model.eppoutput.EppOutput; import google.registry.monitoring.whitebox.EppMetric; import google.registry.util.Clock; import google.registry.util.FormattingLogger; -import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Provider; import org.joda.time.DateTime; @@ -57,7 +55,7 @@ public class FlowRunner { private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass(); - @Inject @Nullable @ClientId String clientId; + @Inject @ClientId String clientId; @Inject Clock clock; @Inject TransportCredentials credentials; @Inject EppInput eppInput; @@ -96,7 +94,7 @@ public class FlowRunner { REPORTING_LOG_SIGNATURE, JSONValue.toJSONString(ImmutableMap.of( "trid", trid.getServerTransactionId(), - "clientId", nullToEmpty(clientId), + "clientId", clientId, "xml", prettyXml, "xmlBytes", xmlBase64))); if (!isTransactional) { diff --git a/javatests/google/registry/flows/FlowRunnerTest.java b/javatests/google/registry/flows/FlowRunnerTest.java index 36aa64a17..2548f35fd 100644 --- a/javatests/google/registry/flows/FlowRunnerTest.java +++ b/javatests/google/registry/flows/FlowRunnerTest.java @@ -123,7 +123,7 @@ public class FlowRunnerTest extends ShardableTestCase { @Test public void testRun_reportingLogStatement_noClientId() throws Exception { - flowRunner.clientId = null; + flowRunner.clientId = ""; flowRunner.run(); assertThat(parseJsonMap(findLogMessageByPrefix(handler, "EPP-REPORTING-LOG-SIGNATURE: "))) .containsExactly( From 1ee02108ae025f84e968b7f28228c286cdfad60a Mon Sep 17 00:00:00 2001 From: cgoldfeder Date: Wed, 14 Sep 2016 11:16:12 -0700 Subject: [PATCH 30/31] Crush out shared code in contact flows, especially transfer Although the delta implies that this is actually adding code, it's better than it looks, because some of the stuff in ContactFlowUtils is duplicating more generic methods in ResourceFlowUtils, which can be deleted when the domain and host flows are cut over. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=133149104 --- java/google/registry/flows/FlowModule.java | 9 ++++ .../registry/flows/ResourceFlowUtils.java | 9 ++++ .../flows/contact/ContactCheckFlow.java | 2 +- .../flows/contact/ContactCreateFlow.java | 6 ++- .../flows/contact/ContactDeleteFlow.java | 17 +++--- .../flows/contact/ContactFlowUtils.java | 53 ++++++++++++++++++- .../flows/contact/ContactInfoFlow.java | 11 ++-- .../contact/ContactTransferApproveFlow.java | 47 +++++----------- .../contact/ContactTransferCancelFlow.java | 42 +++++---------- .../contact/ContactTransferQueryFlow.java | 23 ++++---- .../contact/ContactTransferRejectFlow.java | 38 ++++--------- .../contact/ContactTransferRequestFlow.java | 53 +++++++------------ .../flows/contact/ContactUpdateFlow.java | 15 +++--- java/google/registry/model/EppResource.java | 22 ++++++++ .../model/transfer/TransferStatus.java | 4 ++ 15 files changed, 194 insertions(+), 157 deletions(-) diff --git a/java/google/registry/flows/FlowModule.java b/java/google/registry/flows/FlowModule.java index aab0f04ac..669ca333f 100644 --- a/java/google/registry/flows/FlowModule.java +++ b/java/google/registry/flows/FlowModule.java @@ -16,16 +16,19 @@ package google.registry.flows; import static com.google.common.base.Preconditions.checkState; +import com.google.common.base.Optional; import com.google.common.base.Strings; import dagger.Module; import dagger.Provides; import google.registry.flows.exceptions.OnlyToolCanPassMetadataException; import google.registry.flows.picker.FlowPicker; import google.registry.model.domain.metadata.MetadataExtension; +import google.registry.model.eppcommon.AuthInfo; import google.registry.model.eppcommon.Trid; import google.registry.model.eppinput.EppInput; import google.registry.model.eppinput.EppInput.ResourceCommandWrapper; import google.registry.model.eppinput.ResourceCommand; +import google.registry.model.eppinput.ResourceCommand.SingleResourceCommand; import google.registry.model.reporting.HistoryEntry; import java.lang.annotation.Documented; import javax.inject.Qualifier; @@ -177,6 +180,12 @@ public class FlowModule { .getResourceCommand(); } + @Provides + @FlowScope + static Optional provideAuthInfo(ResourceCommand resourceCommand) { + return Optional.fromNullable(((SingleResourceCommand) resourceCommand).getAuthInfo()); + } + /** * Provides a partially filled in {@link HistoryEntry} builder. * diff --git a/java/google/registry/flows/ResourceFlowUtils.java b/java/google/registry/flows/ResourceFlowUtils.java index 88065d649..6450779d5 100644 --- a/java/google/registry/flows/ResourceFlowUtils.java +++ b/java/google/registry/flows/ResourceFlowUtils.java @@ -20,6 +20,7 @@ import static google.registry.model.domain.DomainResource.extendRegistrationWith import static google.registry.model.ofy.ObjectifyService.ofy; import com.google.common.base.Function; +import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -223,6 +224,14 @@ public class ResourceFlowUtils { } } + /** 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 { + if (authInfo.isPresent()) { + verifyAuthInfoForResource(authInfo.get(), resource); + } + } + /** Check that the given AuthInfo is valid for the given resource. */ public static void verifyAuthInfoForResource(AuthInfo authInfo, EppResource resource) throws EppException { diff --git a/java/google/registry/flows/contact/ContactCheckFlow.java b/java/google/registry/flows/contact/ContactCheckFlow.java index 1e1346dd3..e8b2b0237 100644 --- a/java/google/registry/flows/contact/ContactCheckFlow.java +++ b/java/google/registry/flows/contact/ContactCheckFlow.java @@ -55,6 +55,6 @@ public class ContactCheckFlow extends LoggedInFlow { boolean unused = !existingIds.contains(id); checks.add(ContactCheck.create(unused, id, unused ? null : "In use")); } - return createOutput(Success, ContactCheckData.create(checks.build()), null); + return createOutput(Success, ContactCheckData.create(checks.build())); } } diff --git a/java/google/registry/flows/contact/ContactCreateFlow.java b/java/google/registry/flows/contact/ContactCreateFlow.java index f034347ae..53d26fcf0 100644 --- a/java/google/registry/flows/contact/ContactCreateFlow.java +++ b/java/google/registry/flows/contact/ContactCreateFlow.java @@ -23,6 +23,7 @@ import static google.registry.model.ofy.ObjectifyService.ofy; import com.googlecode.objectify.Key; import google.registry.flows.EppException; +import google.registry.flows.FlowModule.ClientId; import google.registry.flows.LoggedInFlow; import google.registry.flows.TransactionalFlow; import google.registry.flows.exceptions.ResourceAlreadyExistsException; @@ -49,6 +50,7 @@ import javax.inject.Inject; public class ContactCreateFlow extends LoggedInFlow implements TransactionalFlow { @Inject ResourceCommand resourceCommand; + @Inject @ClientId String clientId; @Inject HistoryEntry.Builder historyBuilder; @Inject ContactCreateFlow() {} @@ -66,8 +68,8 @@ public class ContactCreateFlow extends LoggedInFlow implements TransactionalFlow Builder builder = new Builder(); command.applyTo(builder); ContactResource newResource = builder - .setCreationClientId(getClientId()) - .setCurrentSponsorClientId(getClientId()) + .setCreationClientId(clientId) + .setCurrentSponsorClientId(clientId) .setRepoId(createContactHostRoid(ObjectifyService.allocateId())) .build(); validateAsciiPostalInfo(newResource.getInternationalizedPostalInfo()); diff --git a/java/google/registry/flows/contact/ContactDeleteFlow.java b/java/google/registry/flows/contact/ContactDeleteFlow.java index 09d2affe3..fa300f68b 100644 --- a/java/google/registry/flows/contact/ContactDeleteFlow.java +++ b/java/google/registry/flows/contact/ContactDeleteFlow.java @@ -15,19 +15,21 @@ package google.registry.flows.contact; import static google.registry.flows.ResourceFlowUtils.failfastForAsyncDelete; -import static google.registry.flows.ResourceFlowUtils.verifyAuthInfoForResource; import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses; +import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfoForResource; import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership; import static google.registry.model.EppResourceUtils.loadByUniqueId; import static google.registry.model.eppoutput.Result.Code.SuccessWithActionPending; import static google.registry.model.ofy.ObjectifyService.ofy; import com.google.common.base.Function; +import com.google.common.base.Optional; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.googlecode.objectify.Key; import google.registry.config.ConfigModule.Config; import google.registry.flows.EppException; +import google.registry.flows.FlowModule.ClientId; import google.registry.flows.LoggedInFlow; import google.registry.flows.TransactionalFlow; import google.registry.flows.async.AsyncFlowUtils; @@ -38,6 +40,7 @@ import google.registry.model.contact.ContactCommand.Delete; import google.registry.model.contact.ContactResource; import google.registry.model.domain.DomainBase; import google.registry.model.domain.metadata.MetadataExtension; +import google.registry.model.eppcommon.AuthInfo; import google.registry.model.eppcommon.StatusValue; import google.registry.model.eppinput.ResourceCommand; import google.registry.model.eppoutput.EppOutput; @@ -62,6 +65,8 @@ public class ContactDeleteFlow extends LoggedInFlow implements TransactionalFlow StatusValue.SERVER_DELETE_PROHIBITED); @Inject ResourceCommand resourceCommand; + @Inject @ClientId String clientId; + @Inject Optional authInfo; @Inject @Config("asyncDeleteFlowMapreduceDelay") Duration mapreduceDelay; @Inject HistoryEntry.Builder historyBuilder; @Inject ContactDeleteFlow() {} @@ -89,11 +94,9 @@ public class ContactDeleteFlow extends LoggedInFlow implements TransactionalFlow throw new ResourceToMutateDoesNotExistException(ContactResource.class, targetId); } verifyNoDisallowedStatuses(existingResource, DISALLOWED_STATUSES); - if (command.getAuthInfo() != null) { - verifyAuthInfoForResource(command.getAuthInfo(), existingResource); - } + verifyOptionalAuthInfoForResource(authInfo, existingResource); if (!isSuperuser) { - verifyResourceOwnership(getClientId(), existingResource); + verifyResourceOwnership(clientId, existingResource); } AsyncFlowUtils.enqueueMapreduceAction( DeleteContactResourceAction.class, @@ -101,7 +104,7 @@ public class ContactDeleteFlow extends LoggedInFlow implements TransactionalFlow DeleteEppResourceAction.PARAM_RESOURCE_KEY, Key.create(existingResource).getString(), DeleteEppResourceAction.PARAM_REQUESTING_CLIENT_ID, - getClientId(), + clientId, DeleteEppResourceAction.PARAM_IS_SUPERUSER, Boolean.toString(isSuperuser)), mapreduceDelay); @@ -112,6 +115,6 @@ public class ContactDeleteFlow extends LoggedInFlow implements TransactionalFlow .setModificationTime(now) .setParent(Key.create(existingResource)); ofy().save().entities(newResource, historyBuilder.build()); - return createOutput(SuccessWithActionPending, null, null); + return createOutput(SuccessWithActionPending); } } diff --git a/java/google/registry/flows/contact/ContactFlowUtils.java b/java/google/registry/flows/contact/ContactFlowUtils.java index a812fe46e..661f4d4ba 100644 --- a/java/google/registry/flows/contact/ContactFlowUtils.java +++ b/java/google/registry/flows/contact/ContactFlowUtils.java @@ -18,6 +18,7 @@ import static google.registry.model.contact.PostalInfo.Type.INTERNATIONALIZED; import com.google.common.base.CharMatcher; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Sets; import google.registry.flows.EppException; import google.registry.flows.EppException.ParameterValuePolicyErrorException; @@ -25,6 +26,11 @@ import google.registry.flows.EppException.ParameterValueSyntaxErrorException; import google.registry.model.contact.ContactAddress; import google.registry.model.contact.ContactResource; import google.registry.model.contact.PostalInfo; +import google.registry.model.poll.PendingActionNotificationResponse.ContactPendingActionNotificationResponse; +import google.registry.model.poll.PollMessage; +import google.registry.model.reporting.HistoryEntry; +import google.registry.model.transfer.TransferData; +import google.registry.model.transfer.TransferResponse.ContactTransferResponse; import java.util.Set; import javax.annotation.Nullable; @@ -50,7 +56,7 @@ public class ContactFlowUtils { } } } - + /** Check contact's state against server policy. */ static void validateContactAgainstPolicy(ContactResource contact) throws EppException { if (contact.getDisclose() != null && !contact.getDisclose().getFlag()) { @@ -58,6 +64,49 @@ public class ContactFlowUtils { } } + /** Create a poll message for the gaining client in a transfer. */ + static PollMessage createGainingTransferPollMessage( + String targetId, TransferData transferData, HistoryEntry historyEntry) { + return new PollMessage.OneTime.Builder() + .setClientId(transferData.getGainingClientId()) + .setEventTime(transferData.getPendingTransferExpirationTime()) + .setMsg(transferData.getTransferStatus().getMessage()) + .setResponseData(ImmutableList.of( + createTransferResponse(targetId, transferData), + ContactPendingActionNotificationResponse.create( + targetId, + transferData.getTransferStatus().isApproved(), + transferData.getTransferRequestTrid(), + historyEntry.getModificationTime()))) + .setParent(historyEntry) + .build(); + } + + /** Create a poll message for the losing client in a transfer. */ + static PollMessage createLosingTransferPollMessage( + String targetId, TransferData transferData, HistoryEntry historyEntry) { + return new PollMessage.OneTime.Builder() + .setClientId(transferData.getLosingClientId()) + .setEventTime(transferData.getPendingTransferExpirationTime()) + .setMsg(transferData.getTransferStatus().getMessage()) + .setResponseData(ImmutableList.of(createTransferResponse(targetId, transferData))) + .setParent(historyEntry) + .build(); + } + + /** Create a {@link ContactTransferResponse} off of the info in a {@link TransferData}. */ + static ContactTransferResponse createTransferResponse( + String targetId, TransferData transferData) { + return new ContactTransferResponse.Builder() + .setContactId(targetId) + .setGainingClientId(transferData.getGainingClientId()) + .setLosingClientId(transferData.getLosingClientId()) + .setPendingTransferExpirationTime(transferData.getPendingTransferExpirationTime()) + .setTransferRequestTime(transferData.getTransferRequestTime()) + .setTransferStatus(transferData.getTransferStatus()) + .build(); + } + /** Declining contact disclosure is disallowed by server policy. */ static class DeclineContactDisclosureFieldDisallowedPolicyException extends ParameterValuePolicyErrorException { @@ -65,7 +114,7 @@ public class ContactFlowUtils { super("Declining contact disclosure is disallowed by server policy."); } } - + /** Internationalized postal infos can only contain ASCII characters. */ static class BadInternationalizedPostalInfoException extends ParameterValueSyntaxErrorException { public BadInternationalizedPostalInfoException() { diff --git a/java/google/registry/flows/contact/ContactInfoFlow.java b/java/google/registry/flows/contact/ContactInfoFlow.java index 350da8f24..9adcad333 100644 --- a/java/google/registry/flows/contact/ContactInfoFlow.java +++ b/java/google/registry/flows/contact/ContactInfoFlow.java @@ -14,16 +14,18 @@ package google.registry.flows.contact; -import static google.registry.flows.ResourceFlowUtils.verifyAuthInfoForResource; +import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfoForResource; import static google.registry.model.EppResourceUtils.cloneResourceWithLinkedStatus; import static google.registry.model.EppResourceUtils.loadByUniqueId; import static google.registry.model.eppoutput.Result.Code.Success; +import com.google.common.base.Optional; import google.registry.flows.EppException; import google.registry.flows.LoggedInFlow; import google.registry.flows.exceptions.ResourceToQueryDoesNotExistException; import google.registry.model.contact.ContactCommand.Info; import google.registry.model.contact.ContactResource; +import google.registry.model.eppcommon.AuthInfo; import google.registry.model.eppinput.ResourceCommand; import google.registry.model.eppoutput.EppOutput; import javax.inject.Inject; @@ -36,6 +38,7 @@ import javax.inject.Inject; public class ContactInfoFlow extends LoggedInFlow { @Inject ResourceCommand resourceCommand; + @Inject Optional authInfo; @Inject ContactInfoFlow() {} @Override @@ -46,9 +49,7 @@ public class ContactInfoFlow extends LoggedInFlow { if (existingResource == null) { throw new ResourceToQueryDoesNotExistException(ContactResource.class, targetId); } - if (command.getAuthInfo() != null) { - verifyAuthInfoForResource(command.getAuthInfo(), existingResource); - } - return createOutput(Success, cloneResourceWithLinkedStatus(existingResource, now), null); + verifyOptionalAuthInfoForResource(authInfo, existingResource); + return createOutput(Success, cloneResourceWithLinkedStatus(existingResource, now)); } } diff --git a/java/google/registry/flows/contact/ContactTransferApproveFlow.java b/java/google/registry/flows/contact/ContactTransferApproveFlow.java index f68f6e27b..b3e4dbc90 100644 --- a/java/google/registry/flows/contact/ContactTransferApproveFlow.java +++ b/java/google/registry/flows/contact/ContactTransferApproveFlow.java @@ -14,17 +14,18 @@ package google.registry.flows.contact; -import static google.registry.flows.ResourceFlowUtils.createPendingTransferNotificationResponse; -import static google.registry.flows.ResourceFlowUtils.createTransferResponse; -import static google.registry.flows.ResourceFlowUtils.verifyAuthInfoForResource; +import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfoForResource; import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership; +import static google.registry.flows.contact.ContactFlowUtils.createGainingTransferPollMessage; +import static google.registry.flows.contact.ContactFlowUtils.createTransferResponse; import static google.registry.model.EppResourceUtils.loadByUniqueId; import static google.registry.model.eppoutput.Result.Code.Success; import static google.registry.model.ofy.ObjectifyService.ofy; -import com.google.common.collect.ImmutableList; +import com.google.common.base.Optional; import com.googlecode.objectify.Key; import google.registry.flows.EppException; +import google.registry.flows.FlowModule.ClientId; import google.registry.flows.LoggedInFlow; import google.registry.flows.TransactionalFlow; import google.registry.flows.exceptions.NotPendingTransferException; @@ -32,12 +33,11 @@ import google.registry.flows.exceptions.ResourceToMutateDoesNotExistException; import google.registry.model.contact.ContactCommand.Transfer; import google.registry.model.contact.ContactResource; import google.registry.model.domain.metadata.MetadataExtension; -import google.registry.model.eppcommon.StatusValue; +import google.registry.model.eppcommon.AuthInfo; import google.registry.model.eppinput.ResourceCommand; import google.registry.model.eppoutput.EppOutput; import google.registry.model.poll.PollMessage; import google.registry.model.reporting.HistoryEntry; -import google.registry.model.transfer.TransferData; import google.registry.model.transfer.TransferStatus; import javax.inject.Inject; @@ -52,6 +52,8 @@ import javax.inject.Inject; public class ContactTransferApproveFlow extends LoggedInFlow implements TransactionalFlow { @Inject ResourceCommand resourceCommand; + @Inject @ClientId String clientId; + @Inject Optional authInfo; @Inject HistoryEntry.Builder historyBuilder; @Inject ContactTransferApproveFlow() {} @@ -68,27 +70,15 @@ public class ContactTransferApproveFlow extends LoggedInFlow implements Transact if (existingResource == null) { throw new ResourceToMutateDoesNotExistException(ContactResource.class, targetId); } - if (command.getAuthInfo() != null) { - verifyAuthInfoForResource(command.getAuthInfo(), existingResource); - } + verifyOptionalAuthInfoForResource(authInfo, existingResource); if (existingResource.getTransferData().getTransferStatus() != TransferStatus.PENDING) { throw new NotPendingTransferException(targetId); } - verifyResourceOwnership(getClientId(), existingResource); - TransferData oldTransferData = existingResource.getTransferData(); + verifyResourceOwnership(clientId, existingResource); ContactResource newResource = existingResource.asBuilder() - .removeStatusValue(StatusValue.PENDING_TRANSFER) + .clearPendingTransfer(TransferStatus.CLIENT_APPROVED, now) .setLastTransferTime(now) .setCurrentSponsorClientId(existingResource.getTransferData().getGainingClientId()) - .setTransferData(oldTransferData.asBuilder() - .setTransferStatus(TransferStatus.CLIENT_APPROVED) - .setPendingTransferExpirationTime(now) - .setExtendedRegistrationYears(null) - .setServerApproveEntities(null) - .setServerApproveBillingEvent(null) - .setServerApproveAutorenewEvent(null) - .setServerApproveAutorenewPollMessage(null) - .build()) .build(); HistoryEntry historyEntry = historyBuilder .setType(HistoryEntry.Type.CONTACT_TRANSFER_APPROVE) @@ -96,21 +86,12 @@ public class ContactTransferApproveFlow extends LoggedInFlow implements Transact .setParent(Key.create(existingResource)) .build(); // Create a poll message for the gaining client. - PollMessage gainingPollMessage = new PollMessage.OneTime.Builder() - .setClientId(oldTransferData.getGainingClientId()) - .setEventTime(now) - .setMsg(TransferStatus.CLIENT_APPROVED.getMessage()) - .setResponseData(ImmutableList.of( - createTransferResponse(newResource, newResource.getTransferData(), now), - createPendingTransferNotificationResponse( - existingResource, oldTransferData.getTransferRequestTrid(), true, now))) - .setParent(historyEntry) - .build(); + PollMessage gainingPollMessage = + createGainingTransferPollMessage(targetId, newResource.getTransferData(), historyEntry); ofy().save().entities(newResource, historyEntry, gainingPollMessage); // Delete the billing event and poll messages that were written in case the transfer would have // been implicitly server approved. ofy().delete().keys(existingResource.getTransferData().getServerApproveEntities()); - return createOutput( - Success, createTransferResponse(newResource, newResource.getTransferData(), now)); + return createOutput(Success, createTransferResponse(targetId, newResource.getTransferData())); } } diff --git a/java/google/registry/flows/contact/ContactTransferCancelFlow.java b/java/google/registry/flows/contact/ContactTransferCancelFlow.java index 689ec4093..665064e27 100644 --- a/java/google/registry/flows/contact/ContactTransferCancelFlow.java +++ b/java/google/registry/flows/contact/ContactTransferCancelFlow.java @@ -14,15 +14,17 @@ package google.registry.flows.contact; -import static google.registry.flows.ResourceFlowUtils.createTransferResponse; -import static google.registry.flows.ResourceFlowUtils.verifyAuthInfoForResource; +import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfoForResource; +import static google.registry.flows.contact.ContactFlowUtils.createLosingTransferPollMessage; +import static google.registry.flows.contact.ContactFlowUtils.createTransferResponse; import static google.registry.model.EppResourceUtils.loadByUniqueId; import static google.registry.model.eppoutput.Result.Code.Success; import static google.registry.model.ofy.ObjectifyService.ofy; -import com.google.common.collect.ImmutableList; +import com.google.common.base.Optional; import com.googlecode.objectify.Key; import google.registry.flows.EppException; +import google.registry.flows.FlowModule.ClientId; import google.registry.flows.LoggedInFlow; import google.registry.flows.TransactionalFlow; import google.registry.flows.exceptions.NotPendingTransferException; @@ -31,7 +33,7 @@ import google.registry.flows.exceptions.ResourceToMutateDoesNotExistException; import google.registry.model.contact.ContactCommand.Transfer; import google.registry.model.contact.ContactResource; import google.registry.model.domain.metadata.MetadataExtension; -import google.registry.model.eppcommon.StatusValue; +import google.registry.model.eppcommon.AuthInfo; import google.registry.model.eppinput.ResourceCommand; import google.registry.model.eppoutput.EppOutput; import google.registry.model.poll.PollMessage; @@ -50,6 +52,8 @@ import javax.inject.Inject; public class ContactTransferCancelFlow extends LoggedInFlow implements TransactionalFlow { @Inject ResourceCommand resourceCommand; + @Inject Optional authInfo; + @Inject @ClientId String clientId; @Inject HistoryEntry.Builder historyBuilder; @Inject ContactTransferCancelFlow() {} @@ -67,28 +71,17 @@ public class ContactTransferCancelFlow extends LoggedInFlow implements Transacti if (existingResource == null) { throw new ResourceToMutateDoesNotExistException(ContactResource.class, targetId); } - if (command.getAuthInfo() != null) { - verifyAuthInfoForResource(command.getAuthInfo(), existingResource); - } + verifyOptionalAuthInfoForResource(authInfo, existingResource); // Fail if object doesn't have a pending transfer, or if authinfo doesn't match. */ if (existingResource.getTransferData().getTransferStatus() != TransferStatus.PENDING) { throw new NotPendingTransferException(targetId); } // TODO(b/18997997): Determine if authInfo is necessary to cancel a transfer. - if (!getClientId().equals(existingResource.getTransferData().getGainingClientId())) { + if (!clientId.equals(existingResource.getTransferData().getGainingClientId())) { throw new NotTransferInitiatorException(); } ContactResource newResource = existingResource.asBuilder() - .removeStatusValue(StatusValue.PENDING_TRANSFER) - .setTransferData(existingResource.getTransferData().asBuilder() - .setTransferStatus(TransferStatus.CLIENT_CANCELLED) - .setPendingTransferExpirationTime(now) - .setExtendedRegistrationYears(null) - .setServerApproveEntities(null) - .setServerApproveBillingEvent(null) - .setServerApproveAutorenewEvent(null) - .setServerApproveAutorenewPollMessage(null) - .build()) + .clearPendingTransfer(TransferStatus.CLIENT_CANCELLED, now) .build(); HistoryEntry historyEntry = historyBuilder .setType(HistoryEntry.Type.CONTACT_TRANSFER_CANCEL) @@ -96,19 +89,12 @@ public class ContactTransferCancelFlow extends LoggedInFlow implements Transacti .setParent(Key.create(existingResource)) .build(); // Create a poll message for the losing client. - PollMessage losingPollMessage = new PollMessage.OneTime.Builder() - .setClientId(existingResource.getTransferData().getLosingClientId()) - .setEventTime(now) - .setMsg(TransferStatus.CLIENT_CANCELLED.getMessage()) - .setResponseData(ImmutableList.of( - createTransferResponse(newResource, newResource.getTransferData(), now))) - .setParent(historyEntry) - .build(); + PollMessage losingPollMessage = + createLosingTransferPollMessage(targetId, newResource.getTransferData(), historyEntry); ofy().save().entities(newResource, historyEntry, losingPollMessage); // Delete the billing event and poll messages that were written in case the transfer would have // been implicitly server approved. ofy().delete().keys(existingResource.getTransferData().getServerApproveEntities()); - return createOutput( - Success, createTransferResponse(newResource, newResource.getTransferData(), now)); + return createOutput(Success, createTransferResponse(targetId, newResource.getTransferData())); } } diff --git a/java/google/registry/flows/contact/ContactTransferQueryFlow.java b/java/google/registry/flows/contact/ContactTransferQueryFlow.java index 5d7786dbb..56c4052d2 100644 --- a/java/google/registry/flows/contact/ContactTransferQueryFlow.java +++ b/java/google/registry/flows/contact/ContactTransferQueryFlow.java @@ -14,18 +14,21 @@ package google.registry.flows.contact; -import static google.registry.flows.ResourceFlowUtils.createTransferResponse; -import static google.registry.flows.ResourceFlowUtils.verifyAuthInfoForResource; +import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfoForResource; +import static google.registry.flows.contact.ContactFlowUtils.createTransferResponse; import static google.registry.model.EppResourceUtils.loadByUniqueId; import static google.registry.model.eppoutput.Result.Code.Success; +import com.google.common.base.Optional; import google.registry.flows.EppException; +import google.registry.flows.FlowModule.ClientId; import google.registry.flows.LoggedInFlow; import google.registry.flows.exceptions.NoTransferHistoryToQueryException; import google.registry.flows.exceptions.NotAuthorizedToViewTransferException; import google.registry.flows.exceptions.ResourceToQueryDoesNotExistException; import google.registry.model.contact.ContactCommand.Transfer; import google.registry.model.contact.ContactResource; +import google.registry.model.eppcommon.AuthInfo; import google.registry.model.eppinput.ResourceCommand; import google.registry.model.eppoutput.EppOutput; import javax.inject.Inject; @@ -41,6 +44,8 @@ import javax.inject.Inject; public class ContactTransferQueryFlow extends LoggedInFlow { @Inject ResourceCommand resourceCommand; + @Inject Optional authInfo; + @Inject @ClientId String clientId; @Inject ContactTransferQueryFlow() {} @Override @@ -51,22 +56,20 @@ public class ContactTransferQueryFlow extends LoggedInFlow { if (existingResource == null) { throw new ResourceToQueryDoesNotExistException(ContactResource.class, targetId); } - if (command.getAuthInfo() != null) { - verifyAuthInfoForResource(command.getAuthInfo(), existingResource); - } + verifyOptionalAuthInfoForResource(authInfo, existingResource); // Most of the fields on the transfer response are required, so there's no way to return valid // XML if the object has never been transferred (and hence the fields aren't populated). if (existingResource.getTransferData().getTransferStatus() == null) { throw new NoTransferHistoryToQueryException(); } - // Note that the authorization info on the command (if present) has already been verified by the - // parent class. If it's present, then the other checks are unnecessary. + // Note that the authorization info on the command (if present) has already been verified. If + // it's present, then the other checks are unnecessary. if (command.getAuthInfo() == null - && !getClientId().equals(existingResource.getTransferData().getGainingClientId()) - && !getClientId().equals(existingResource.getTransferData().getLosingClientId())) { + && !clientId.equals(existingResource.getTransferData().getGainingClientId()) + && !clientId.equals(existingResource.getTransferData().getLosingClientId())) { throw new NotAuthorizedToViewTransferException(); } return createOutput( - Success, createTransferResponse(existingResource, existingResource.getTransferData(), now)); + Success, createTransferResponse(targetId, existingResource.getTransferData())); } } diff --git a/java/google/registry/flows/contact/ContactTransferRejectFlow.java b/java/google/registry/flows/contact/ContactTransferRejectFlow.java index b564399e4..3fd7308b9 100644 --- a/java/google/registry/flows/contact/ContactTransferRejectFlow.java +++ b/java/google/registry/flows/contact/ContactTransferRejectFlow.java @@ -14,17 +14,17 @@ package google.registry.flows.contact; -import static google.registry.flows.ResourceFlowUtils.createPendingTransferNotificationResponse; -import static google.registry.flows.ResourceFlowUtils.createTransferResponse; import static google.registry.flows.ResourceFlowUtils.verifyAuthInfoForResource; import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership; +import static google.registry.flows.contact.ContactFlowUtils.createGainingTransferPollMessage; +import static google.registry.flows.contact.ContactFlowUtils.createTransferResponse; import static google.registry.model.EppResourceUtils.loadByUniqueId; import static google.registry.model.eppoutput.Result.Code.Success; import static google.registry.model.ofy.ObjectifyService.ofy; -import com.google.common.collect.ImmutableList; import com.googlecode.objectify.Key; import google.registry.flows.EppException; +import google.registry.flows.FlowModule.ClientId; import google.registry.flows.LoggedInFlow; import google.registry.flows.TransactionalFlow; import google.registry.flows.exceptions.NotPendingTransferException; @@ -32,12 +32,10 @@ import google.registry.flows.exceptions.ResourceToMutateDoesNotExistException; import google.registry.model.contact.ContactCommand.Transfer; import google.registry.model.contact.ContactResource; import google.registry.model.domain.metadata.MetadataExtension; -import google.registry.model.eppcommon.StatusValue; import google.registry.model.eppinput.ResourceCommand; import google.registry.model.eppoutput.EppOutput; import google.registry.model.poll.PollMessage; import google.registry.model.reporting.HistoryEntry; -import google.registry.model.transfer.TransferData; import google.registry.model.transfer.TransferStatus; import javax.inject.Inject; @@ -52,6 +50,7 @@ import javax.inject.Inject; public class ContactTransferRejectFlow extends LoggedInFlow implements TransactionalFlow { @Inject ResourceCommand resourceCommand; + @Inject @ClientId String clientId; @Inject HistoryEntry.Builder historyBuilder; @Inject ContactTransferRejectFlow() {} @@ -74,40 +73,21 @@ public class ContactTransferRejectFlow extends LoggedInFlow implements Transacti if (existingResource.getTransferData().getTransferStatus() != TransferStatus.PENDING) { throw new NotPendingTransferException(targetId); } - verifyResourceOwnership(getClientId(), existingResource); - TransferData oldTransferData = existingResource.getTransferData(); + verifyResourceOwnership(clientId, existingResource); ContactResource newResource = existingResource.asBuilder() - .removeStatusValue(StatusValue.PENDING_TRANSFER) - .setTransferData(oldTransferData.asBuilder() - .setTransferStatus(TransferStatus.CLIENT_REJECTED) - .setPendingTransferExpirationTime(now) - .setExtendedRegistrationYears(null) - .setServerApproveEntities(null) - .setServerApproveBillingEvent(null) - .setServerApproveAutorenewEvent(null) - .setServerApproveAutorenewPollMessage(null) - .build()) + .clearPendingTransfer(TransferStatus.CLIENT_REJECTED, now) .build(); HistoryEntry historyEntry = historyBuilder .setType(HistoryEntry.Type.CONTACT_TRANSFER_REJECT) .setModificationTime(now) .setParent(Key.create(existingResource)) .build(); - PollMessage.OneTime gainingPollMessage = new PollMessage.OneTime.Builder() - .setClientId(oldTransferData.getGainingClientId()) - .setEventTime(now) - .setMsg(TransferStatus.CLIENT_REJECTED.getMessage()) - .setResponseData(ImmutableList.of( - createTransferResponse(newResource, newResource.getTransferData(), now), - createPendingTransferNotificationResponse( - existingResource, oldTransferData.getTransferRequestTrid(), false, now))) - .setParent(historyEntry) - .build(); + PollMessage gainingPollMessage = + createGainingTransferPollMessage(targetId, newResource.getTransferData(), historyEntry); ofy().save().entities(newResource, historyEntry, gainingPollMessage); // Delete the billing event and poll messages that were written in case the transfer would have // been implicitly server approved. ofy().delete().keys(existingResource.getTransferData().getServerApproveEntities()); - return createOutput( - Success, createTransferResponse(newResource, newResource.getTransferData(), now)); + return createOutput(Success, createTransferResponse(targetId, newResource.getTransferData())); } } diff --git a/java/google/registry/flows/contact/ContactTransferRequestFlow.java b/java/google/registry/flows/contact/ContactTransferRequestFlow.java index 357b81ccc..c40201d3b 100644 --- a/java/google/registry/flows/contact/ContactTransferRequestFlow.java +++ b/java/google/registry/flows/contact/ContactTransferRequestFlow.java @@ -14,19 +14,21 @@ package google.registry.flows.contact; -import static google.registry.flows.ResourceFlowUtils.createPendingTransferNotificationResponse; -import static google.registry.flows.ResourceFlowUtils.createTransferResponse; import static google.registry.flows.ResourceFlowUtils.verifyAuthInfoForResource; import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses; +import static google.registry.flows.contact.ContactFlowUtils.createGainingTransferPollMessage; +import static google.registry.flows.contact.ContactFlowUtils.createLosingTransferPollMessage; +import static google.registry.flows.contact.ContactFlowUtils.createTransferResponse; import static google.registry.model.EppResourceUtils.loadByUniqueId; import static google.registry.model.eppoutput.Result.Code.SuccessWithActionPending; import static google.registry.model.ofy.ObjectifyService.ofy; -import com.google.common.collect.ImmutableList; +import com.google.common.base.Optional; import com.google.common.collect.ImmutableSet; import com.googlecode.objectify.Key; import google.registry.config.ConfigModule.Config; import google.registry.flows.EppException; +import google.registry.flows.FlowModule.ClientId; import google.registry.flows.LoggedInFlow; import google.registry.flows.TransactionalFlow; import google.registry.flows.exceptions.AlreadyPendingTransferException; @@ -36,6 +38,7 @@ import google.registry.flows.exceptions.ResourceToMutateDoesNotExistException; import google.registry.model.contact.ContactCommand.Transfer; import google.registry.model.contact.ContactResource; import google.registry.model.domain.metadata.MetadataExtension; +import google.registry.model.eppcommon.AuthInfo; import google.registry.model.eppcommon.StatusValue; import google.registry.model.eppinput.ResourceCommand; import google.registry.model.eppoutput.EppOutput; @@ -64,6 +67,8 @@ public class ContactTransferRequestFlow extends LoggedInFlow implements Transact StatusValue.SERVER_TRANSFER_PROHIBITED); @Inject ResourceCommand resourceCommand; + @Inject Optional authInfo; + @Inject @ClientId String gainingClientId; @Inject @Config("contactAutomaticTransferLength") Duration automaticTransferLength; @Inject HistoryEntry.Builder historyBuilder; @Inject ContactTransferRequestFlow() {} @@ -81,21 +86,19 @@ public class ContactTransferRequestFlow extends LoggedInFlow implements Transact if (existingResource == null) { throw new ResourceToMutateDoesNotExistException(ContactResource.class, targetId); } - if (command.getAuthInfo() == null) { + if (!authInfo.isPresent()) { throw new MissingTransferRequestAuthInfoException(); } - verifyAuthInfoForResource(command.getAuthInfo(), existingResource); + verifyAuthInfoForResource(authInfo.get(), existingResource); // Verify that the resource does not already have a pending transfer. if (TransferStatus.PENDING.equals(existingResource.getTransferData().getTransferStatus())) { throw new AlreadyPendingTransferException(targetId); } - String gainingClientId = getClientId(); String losingClientId = existingResource.getCurrentSponsorClientId(); // Verify that this client doesn't already sponsor this resource. if (gainingClientId.equals(losingClientId)) { throw new ObjectAlreadySponsoredException(); } - verifyAuthInfoForResource(command.getAuthInfo(), existingResource); verifyNoDisallowedStatuses(existingResource, DISALLOWED_STATUSES); HistoryEntry historyEntry = historyBuilder .setType(HistoryEntry.Type.CONTACT_TRANSFER_REQUEST) @@ -112,24 +115,11 @@ public class ContactTransferRequestFlow extends LoggedInFlow implements Transact .setTransferStatus(TransferStatus.SERVER_APPROVED) .build(); // If the transfer is server approved, this message will be sent to the losing registrar. */ - PollMessage.OneTime serverApproveLosingPollMessage = new PollMessage.OneTime.Builder() - .setClientId(losingClientId) - .setMsg(TransferStatus.SERVER_APPROVED.getMessage()) - .setParent(historyEntry) - .setEventTime(transferExpirationTime) - .setResponseData(ImmutableList.of( - createTransferResponse(existingResource, serverApproveTransferData, now))) - .build(); + PollMessage serverApproveLosingPollMessage = + createLosingTransferPollMessage(targetId, serverApproveTransferData, historyEntry); // If the transfer is server approved, this message will be sent to the gaining registrar. */ - PollMessage.OneTime serverApproveGainingPollMessage = new PollMessage.OneTime.Builder() - .setClientId(gainingClientId) - .setMsg(TransferStatus.SERVER_APPROVED.getMessage()) - .setParent(historyEntry) - .setEventTime(transferExpirationTime) - .setResponseData(ImmutableList.of( - createTransferResponse(existingResource, serverApproveTransferData, now), - createPendingTransferNotificationResponse(existingResource, trid, true, now))) - .build(); + PollMessage serverApproveGainingPollMessage = + createGainingTransferPollMessage(targetId, serverApproveTransferData, historyEntry); TransferData pendingTransferData = serverApproveTransferData.asBuilder() .setTransferStatus(TransferStatus.PENDING) .setServerApproveEntities( @@ -138,13 +128,9 @@ public class ContactTransferRequestFlow extends LoggedInFlow implements Transact Key.create(serverApproveLosingPollMessage))) .build(); // When a transfer is requested, a poll message is created to notify the losing registrar. - PollMessage.OneTime requestPollMessage = new PollMessage.OneTime.Builder() - .setClientId(losingClientId) - .setMsg(TransferStatus.PENDING.getMessage()) - .setParent(historyEntry) - .setEventTime(now) - .setResponseData(ImmutableList.of( - createTransferResponse(existingResource, pendingTransferData, now))) + PollMessage requestPollMessage = + createLosingTransferPollMessage(targetId, pendingTransferData, historyEntry).asBuilder() + .setEventTime(now) // Unlike the serverApprove messages, this applies immediately. .build(); ContactResource newResource = existingResource.asBuilder() .setTransferData(pendingTransferData) @@ -157,8 +143,7 @@ public class ContactTransferRequestFlow extends LoggedInFlow implements Transact serverApproveGainingPollMessage, serverApproveLosingPollMessage); return createOutput( - SuccessWithActionPending, - createTransferResponse(newResource, newResource.getTransferData(), now), - null); + SuccessWithActionPending, createTransferResponse(targetId, newResource.getTransferData())); } } + diff --git a/java/google/registry/flows/contact/ContactUpdateFlow.java b/java/google/registry/flows/contact/ContactUpdateFlow.java index 0f0b19fda..9052a00c6 100644 --- a/java/google/registry/flows/contact/ContactUpdateFlow.java +++ b/java/google/registry/flows/contact/ContactUpdateFlow.java @@ -14,8 +14,8 @@ package google.registry.flows.contact; -import static google.registry.flows.ResourceFlowUtils.verifyAuthInfoForResource; import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses; +import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfoForResource; import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership; import static google.registry.flows.contact.ContactFlowUtils.validateAsciiPostalInfo; import static google.registry.flows.contact.ContactFlowUtils.validateContactAgainstPolicy; @@ -23,10 +23,12 @@ import static google.registry.model.EppResourceUtils.loadByUniqueId; import static google.registry.model.eppoutput.Result.Code.Success; import static google.registry.model.ofy.ObjectifyService.ofy; +import com.google.common.base.Optional; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.googlecode.objectify.Key; import google.registry.flows.EppException; +import google.registry.flows.FlowModule.ClientId; import google.registry.flows.LoggedInFlow; import google.registry.flows.TransactionalFlow; import google.registry.flows.exceptions.AddRemoveSameValueEppException; @@ -37,6 +39,7 @@ import google.registry.model.contact.ContactCommand.Update; import google.registry.model.contact.ContactResource; import google.registry.model.contact.ContactResource.Builder; import google.registry.model.domain.metadata.MetadataExtension; +import google.registry.model.eppcommon.AuthInfo; import google.registry.model.eppcommon.StatusValue; import google.registry.model.eppinput.ResourceCommand; import google.registry.model.eppinput.ResourceCommand.AddRemoveSameValueException; @@ -68,6 +71,8 @@ public class ContactUpdateFlow extends LoggedInFlow implements TransactionalFlow StatusValue.SERVER_UPDATE_PROHIBITED); @Inject ResourceCommand resourceCommand; + @Inject Optional authInfo; + @Inject @ClientId String clientId; @Inject HistoryEntry.Builder historyBuilder; @Inject ContactUpdateFlow() {} @@ -84,11 +89,9 @@ public class ContactUpdateFlow extends LoggedInFlow implements TransactionalFlow if (existingResource == null) { throw new ResourceToMutateDoesNotExistException(ContactResource.class, targetId); } - if (command.getAuthInfo() != null) { - verifyAuthInfoForResource(command.getAuthInfo(), existingResource); - } + verifyOptionalAuthInfoForResource(authInfo, existingResource); if (!isSuperuser) { - verifyResourceOwnership(getClientId(), existingResource); + verifyResourceOwnership(clientId, existingResource); } for (StatusValue statusValue : Sets.union( command.getInnerAdd().getStatusValues(), @@ -111,7 +114,7 @@ public class ContactUpdateFlow extends LoggedInFlow implements TransactionalFlow } ContactResource newResource = builder .setLastEppUpdateTime(now) - .setLastEppUpdateClientId(getClientId()) + .setLastEppUpdateClientId(clientId) .build(); // If the resource is marked with clientUpdateProhibited, and this update did not clear that // status, then the update must be disallowed (unless a superuser is requesting the change). diff --git a/java/google/registry/model/EppResource.java b/java/google/registry/model/EppResource.java index 741653ae6..2d09f2cb9 100644 --- a/java/google/registry/model/EppResource.java +++ b/java/google/registry/model/EppResource.java @@ -32,6 +32,7 @@ import google.registry.model.eppcommon.StatusValue; import google.registry.model.eppoutput.EppResponse.ResponseData; import google.registry.model.ofy.CommitLogManifest; import google.registry.model.transfer.TransferData; +import google.registry.model.transfer.TransferStatus; import java.util.Set; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlTransient; @@ -303,6 +304,27 @@ public abstract class EppResource extends BackupGroupRoot implements Buildable, return thisCastToDerived(); } + /** + * Remove a pending transfer. + * + *

This removes the {@link StatusValue#PENDING_TRANSFER} status, clears all the + * server-approve fields on the {@link TransferData} including the extended registration years + * field, and sets the expiration time of the last pending transfer (i.e. the one being cleared) + * to now. + */ + public B clearPendingTransfer(TransferStatus transferStatus, DateTime now) { + removeStatusValue(StatusValue.PENDING_TRANSFER); + return setTransferData(getInstance().getTransferData().asBuilder() + .setExtendedRegistrationYears(null) + .setServerApproveEntities(null) + .setServerApproveBillingEvent(null) + .setServerApproveAutorenewEvent(null) + .setServerApproveAutorenewPollMessage(null) + .setTransferStatus(transferStatus) + .setPendingTransferExpirationTime(now) + .build()); + } + /** Wipe out any personal information in the resource. */ public B wipeOut() { return thisCastToDerived(); diff --git a/java/google/registry/model/transfer/TransferStatus.java b/java/google/registry/model/transfer/TransferStatus.java index 0b9d985f0..4768c216f 100644 --- a/java/google/registry/model/transfer/TransferStatus.java +++ b/java/google/registry/model/transfer/TransferStatus.java @@ -50,4 +50,8 @@ public enum TransferStatus { public String getXmlName() { return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, toString()); } + + public boolean isApproved() { + return this.equals(CLIENT_APPROVED) || this.equals(SERVER_APPROVED); + } } From 01e2e0141dde2318e8505056b46df04899883903 Mon Sep 17 00:00:00 2001 From: cgoldfeder Date: Wed, 14 Sep 2016 11:16:35 -0700 Subject: [PATCH 31/31] Simplify the use of the fee extension a little ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=133149148 --- .../flows/domain/DomainCheckFlow.java | 12 +++------ .../flows/domain/DomainFlowUtils.java | 21 +++++++-------- .../registry/flows/domain/DomainInfoFlow.java | 4 +-- .../domain/fee/FeeCheckCommandExtension.java | 15 ++++++----- .../fee/FeeQueryCommandExtensionItem.java | 15 ++++++----- .../FeeCheckCommandExtensionItemV06.java | 7 +---- .../fee06/FeeCheckCommandExtensionV06.java | 9 ++----- .../fee06/FeeInfoCommandExtensionV06.java | 7 +---- .../fee11/FeeCheckCommandExtensionV11.java | 27 +++++++------------ .../FeeCheckCommandExtensionItemV12.java | 25 +++++++---------- .../fee12/FeeCheckCommandExtensionV12.java | 7 +---- 11 files changed, 57 insertions(+), 92 deletions(-) diff --git a/java/google/registry/flows/domain/DomainCheckFlow.java b/java/google/registry/flows/domain/DomainCheckFlow.java index 7047226d9..46fab25af 100644 --- a/java/google/registry/flows/domain/DomainCheckFlow.java +++ b/java/google/registry/flows/domain/DomainCheckFlow.java @@ -23,7 +23,6 @@ import static google.registry.model.index.DomainApplicationIndex.loadActiveAppli import static google.registry.model.registry.label.ReservationType.UNRESERVED; import static google.registry.pricing.PricingEngineProxy.getPricesForDomainName; import static google.registry.util.CollectionUtils.nullToEmpty; -import static google.registry.util.DomainNameUtils.getTldFromDomainName; import com.google.common.base.Predicate; import com.google.common.collect.FluentIterable; @@ -49,7 +48,6 @@ import google.registry.model.registry.label.ReservationType; import java.util.Collections; import java.util.Set; import javax.inject.Inject; -import org.joda.money.CurrencyUnit; /** * An EPP flow that checks whether a domain can be provisioned. @@ -148,7 +146,7 @@ public class DomainCheckFlow extends BaseDomainCheckFlow { // If this version of the fee extension is nameless, use the full list of domains. return domainNames.keySet(); } - } + } /** Handle the fee check extension. */ @Override @@ -160,7 +158,6 @@ public class DomainCheckFlow extends BaseDomainCheckFlow { if (feeCheck == null) { return null; // No fee checks were requested. } - CurrencyUnit topLevelCurrency = feeCheck.isCurrencySupported() ? feeCheck.getCurrency() : null; ImmutableList.Builder feeCheckResponseItemsBuilder = new ImmutableList.Builder<>(); for (FeeCheckCommandExtensionItem feeCheckItem : feeCheck.getItems()) { @@ -169,10 +166,9 @@ public class DomainCheckFlow extends BaseDomainCheckFlow { handleFeeRequest( feeCheckItem, builder, - domainName, - getTldFromDomainName(domainName), + domainNames.get(domainName), getClientId(), - topLevelCurrency, + feeCheck.getCurrency(), now, eppInput); feeCheckResponseItemsBuilder @@ -182,7 +178,7 @@ public class DomainCheckFlow extends BaseDomainCheckFlow { return ImmutableList.of( feeCheck.createResponse(feeCheckResponseItemsBuilder.build())); } - + /** By server policy, fee check names must be listed in the availability check. */ static class OnlyCheckedNamesCanBeFeeCheckedException extends ParameterValuePolicyErrorException { OnlyCheckedNamesCanBeFeeCheckedException() { diff --git a/java/google/registry/flows/domain/DomainFlowUtils.java b/java/google/registry/flows/domain/DomainFlowUtils.java index 495404818..ef0f4b8dd 100644 --- a/java/google/registry/flows/domain/DomainFlowUtils.java +++ b/java/google/registry/flows/domain/DomainFlowUtils.java @@ -564,14 +564,13 @@ public class DomainFlowUtils { static void handleFeeRequest( FeeQueryCommandExtensionItem feeRequest, FeeQueryResponseExtensionItem.Builder builder, - String domainName, - String tld, + InternetDomainName domain, String clientIdentifier, @Nullable CurrencyUnit topLevelCurrency, DateTime now, EppInput eppInput) throws EppException { - InternetDomainName domain = InternetDomainName.from(domainName); - Registry registry = Registry.get(tld); + String domainNameString = domain.toString(); + Registry registry = Registry.get(domain.parent().toString()); int years = verifyUnitIsYears(feeRequest.getPeriod()).getValue(); boolean isSunrise = registry.getTldState(now).equals(TldState.SUNRISE); @@ -580,7 +579,7 @@ public class DomainFlowUtils { } CurrencyUnit currency = - feeRequest.isCurrencySupported() ? feeRequest.getCurrency() : topLevelCurrency; + feeRequest.getCurrency() != null ? feeRequest.getCurrency() : topLevelCurrency; if ((currency != null) && !currency.equals(registry.getCurrency())) { throw new CurrencyUnitMismatchException(); } @@ -589,7 +588,7 @@ public class DomainFlowUtils { .setCommand(feeRequest.getCommandName(), feeRequest.getPhase(), feeRequest.getSubphase()) .setCurrencyIfSupported(registry.getCurrency()) .setPeriod(feeRequest.getPeriod()) - .setClass(TldSpecificLogicProxy.getFeeClass(domainName, now).orNull()); + .setClass(TldSpecificLogicProxy.getFeeClass(domainNameString, now).orNull()); switch (feeRequest.getCommandName()) { case CREATE: @@ -600,13 +599,13 @@ public class DomainFlowUtils { } else { builder.setAvailIfSupported(true); builder.setFees(TldSpecificLogicProxy.getCreatePrice( - registry, domainName, clientIdentifier, now, years, eppInput).getFees()); + registry, domainNameString, clientIdentifier, now, years, eppInput).getFees()); } break; case RENEW: builder.setAvailIfSupported(true); builder.setFees(TldSpecificLogicProxy.getRenewPrice( - registry, domainName, clientIdentifier, now, years, eppInput).getFees()); + registry, domainNameString, clientIdentifier, now, years, eppInput).getFees()); break; case RESTORE: if (years != 1) { @@ -614,17 +613,17 @@ public class DomainFlowUtils { } builder.setAvailIfSupported(true); builder.setFees(TldSpecificLogicProxy.getRestorePrice( - registry, domainName, clientIdentifier, now, eppInput).getFees()); + registry, domainNameString, clientIdentifier, now, eppInput).getFees()); break; case TRANSFER: builder.setAvailIfSupported(true); builder.setFees(TldSpecificLogicProxy.getTransferPrice( - registry, domainName, clientIdentifier, now, years, eppInput).getFees()); + registry, domainNameString, clientIdentifier, now, years, eppInput).getFees()); break; case UPDATE: builder.setAvailIfSupported(true); builder.setFees(TldSpecificLogicProxy.getUpdatePrice( - registry, domainName, clientIdentifier, now, eppInput).getFees()); + registry, domainNameString, clientIdentifier, now, eppInput).getFees()); break; default: throw new UnknownFeeCommandException(feeRequest.getUnparsedCommandName()); diff --git a/java/google/registry/flows/domain/DomainInfoFlow.java b/java/google/registry/flows/domain/DomainInfoFlow.java index 06c38296a..c9043e0d1 100644 --- a/java/google/registry/flows/domain/DomainInfoFlow.java +++ b/java/google/registry/flows/domain/DomainInfoFlow.java @@ -19,6 +19,7 @@ import static google.registry.flows.domain.DomainFlowUtils.handleFeeRequest; 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.model.domain.DomainResource; import google.registry.model.domain.DomainResource.Builder; @@ -97,8 +98,7 @@ public class DomainInfoFlow extends BaseDomainInfoFlow handleFeeRequest( feeInfo, builder, - getTargetId(), - existingResource.getTld(), + InternetDomainName.from(getTargetId()), getClientId(), null, now, diff --git a/java/google/registry/model/domain/fee/FeeCheckCommandExtension.java b/java/google/registry/model/domain/fee/FeeCheckCommandExtension.java index e8b662720..160ec6718 100644 --- a/java/google/registry/model/domain/fee/FeeCheckCommandExtension.java +++ b/java/google/registry/model/domain/fee/FeeCheckCommandExtension.java @@ -24,7 +24,7 @@ import org.joda.money.CurrencyUnit; * of items requesting the fees for particular commands and domains. For some versions of the fee * extension, the currency is also specified here; for other versions it is contained in the * individual items. - * + * * @type C the type of extension item used by this command (e.g. v6 items for a v6 extension) * @type R the type of response returned for for this command (e.g. v6 responses for a v6 extension) */ @@ -33,13 +33,14 @@ public interface FeeCheckCommandExtension< R extends FeeCheckResponseExtension> extends CommandExtension { - /** True if this version of the fee extension specifies the currency at the top level. */ - public boolean isCurrencySupported(); + /** + * Three-character ISO4217 currency code. + * + *

Returns null if this version of the fee extension doesn't specify currency at the top level. + */ + public CurrencyUnit getCurrency(); - /** Three-character currency code; throws an exception if currency is not supported. */ - public CurrencyUnit getCurrency() throws UnsupportedOperationException; - public ImmutableSet getItems(); - + public R createResponse(ImmutableList items); } diff --git a/java/google/registry/model/domain/fee/FeeQueryCommandExtensionItem.java b/java/google/registry/model/domain/fee/FeeQueryCommandExtensionItem.java index eb91a7604..fc0e2cc6b 100644 --- a/java/google/registry/model/domain/fee/FeeQueryCommandExtensionItem.java +++ b/java/google/registry/model/domain/fee/FeeQueryCommandExtensionItem.java @@ -35,18 +35,19 @@ public interface FeeQueryCommandExtensionItem { UPDATE } - /** True if this version of fee extension includes a currency in this type of query item. */ - public boolean isCurrencySupported(); - - /** A three-character ISO4217 currency code; throws an exception if currency is not supported. */ - public CurrencyUnit getCurrency() throws UnsupportedOperationException; + /** + * Three-character ISO4217 currency code. + * + *

Returns null if this version of the fee extension doesn't specify currency at the top level. + */ + public CurrencyUnit getCurrency(); /** The name of the command being checked. */ public CommandName getCommandName(); - + /** The unparse name of the command being checked, for use in error strings. */ public String getUnparsedCommandName(); - + /** The phase of the command being checked. */ public String getPhase(); diff --git a/java/google/registry/model/domain/fee06/FeeCheckCommandExtensionItemV06.java b/java/google/registry/model/domain/fee06/FeeCheckCommandExtensionItemV06.java index 238017e6f..7b1c977d1 100644 --- a/java/google/registry/model/domain/fee06/FeeCheckCommandExtensionItemV06.java +++ b/java/google/registry/model/domain/fee06/FeeCheckCommandExtensionItemV06.java @@ -29,7 +29,7 @@ public class FeeCheckCommandExtensionItemV06 String name; CurrencyUnit currency; - + @Override public boolean isDomainNameSupported() { return true; @@ -40,11 +40,6 @@ public class FeeCheckCommandExtensionItemV06 return name; } - @Override - public boolean isCurrencySupported() { - return true; - } - @Override public CurrencyUnit getCurrency() { return currency; diff --git a/java/google/registry/model/domain/fee06/FeeCheckCommandExtensionV06.java b/java/google/registry/model/domain/fee06/FeeCheckCommandExtensionV06.java index 45590b087..afe88d242 100644 --- a/java/google/registry/model/domain/fee06/FeeCheckCommandExtensionV06.java +++ b/java/google/registry/model/domain/fee06/FeeCheckCommandExtensionV06.java @@ -32,18 +32,13 @@ public class FeeCheckCommandExtensionV06 extends ImmutableObject implements FeeCheckCommandExtension< FeeCheckCommandExtensionItemV06, FeeCheckResponseExtensionV06> { - + @XmlElement(name = "domain") Set items; - @Override - public boolean isCurrencySupported() { - return false; - } - @Override public CurrencyUnit getCurrency() { - throw new UnsupportedOperationException("Currency not supported"); + return null; // This version of the fee extension doesn't specify a top-level currency. } @Override diff --git a/java/google/registry/model/domain/fee06/FeeInfoCommandExtensionV06.java b/java/google/registry/model/domain/fee06/FeeInfoCommandExtensionV06.java index 56c905c88..6ce0a4dda 100644 --- a/java/google/registry/model/domain/fee06/FeeInfoCommandExtensionV06.java +++ b/java/google/registry/model/domain/fee06/FeeInfoCommandExtensionV06.java @@ -25,15 +25,10 @@ import org.joda.money.CurrencyUnit; @XmlType(propOrder = {"currency", "command", "period"}) public class FeeInfoCommandExtensionV06 extends FeeQueryCommandExtensionItemImpl implements CommandExtension { - + /** A three-character ISO4217 currency code. */ CurrencyUnit currency; - @Override - public boolean isCurrencySupported() { - return true; - } - @Override public CurrencyUnit getCurrency() { return currency; diff --git a/java/google/registry/model/domain/fee11/FeeCheckCommandExtensionV11.java b/java/google/registry/model/domain/fee11/FeeCheckCommandExtensionV11.java index 57c6b4718..ceb8791f8 100644 --- a/java/google/registry/model/domain/fee11/FeeCheckCommandExtensionV11.java +++ b/java/google/registry/model/domain/fee11/FeeCheckCommandExtensionV11.java @@ -52,22 +52,20 @@ public class FeeCheckCommandExtensionV11 extends ImmutableObject /** Three-letter currency code in which results should be returned. */ CurrencyUnit currency; - + /** The period to check. */ Period period; /** The class to check. */ @XmlElement(name = "class") String feeClass; - - @Override - public boolean isCurrencySupported() { - return false; - } - + @Override public CurrencyUnit getCurrency() { - throw new UnsupportedOperationException("Currency not supported"); + // This version of the fee extension does not have any items, and although the currency is + // specified at the top level we've modeled it as a single fake item with the currency inside, + // so there's no top level currency to return here. + return null; } @Override @@ -96,13 +94,13 @@ public class FeeCheckCommandExtensionV11 extends ImmutableObject public CommandName getCommandName() { return command.getCommand(); } - + /** The command name before being parsed into an enum, for use in error strings. */ @Override public String getUnparsedCommandName() { return command.getUnparsedCommandName(); } - + /** The phase of the command being checked. */ @Override public String getPhase() { @@ -119,22 +117,17 @@ public class FeeCheckCommandExtensionV11 extends ImmutableObject public Period getPeriod() { return Optional.fromNullable(period).or(DEFAULT_PERIOD); } - + @Override public boolean isDomainNameSupported() { return false; } - + @Override public String getDomainName() { throw new UnsupportedOperationException("Domain not supported"); } - @Override - public boolean isCurrencySupported() { - return true; - } - @Override public CurrencyUnit getCurrency() { return currency; diff --git a/java/google/registry/model/domain/fee12/FeeCheckCommandExtensionItemV12.java b/java/google/registry/model/domain/fee12/FeeCheckCommandExtensionItemV12.java index 78782efbe..329667d17 100644 --- a/java/google/registry/model/domain/fee12/FeeCheckCommandExtensionItemV12.java +++ b/java/google/registry/model/domain/fee12/FeeCheckCommandExtensionItemV12.java @@ -30,13 +30,13 @@ import org.joda.time.DateTime; /** * An individual price check item in version 0.12 of the fee extension on domain check commands. * Items look like: - * + * * * 1 * premium * 2017-05-17T13:22:21.0Z * - * + * * In a change from previous versions of the extension, items do not contain domain names; instead, * the names from the non-extension check element are used. */ @@ -49,7 +49,7 @@ public class FeeCheckCommandExtensionItemV12 @XmlAttribute(name = "name") String commandName; - + @XmlAttribute String phase; @@ -58,10 +58,10 @@ public class FeeCheckCommandExtensionItemV12 @XmlElement Period period; - + @XmlElement(name = "class") String feeClass; - + @XmlElement(name = "date") DateTime feeDate; @@ -75,22 +75,17 @@ public class FeeCheckCommandExtensionItemV12 public String getDomainName() { throw new UnsupportedOperationException("Domain not supported"); } - - @Override - public boolean isCurrencySupported() { - return false; - } - + @Override public CurrencyUnit getCurrency() { - throw new UnsupportedOperationException("Currency not supported"); + return null; // This version of the fee extension doesn't specify currency per-item. } - + @Override public String getUnparsedCommandName() { return commandName; } - + @Override public CommandName getCommandName() { // Require the xml string to be lowercase. @@ -108,7 +103,7 @@ public class FeeCheckCommandExtensionItemV12 public String getPhase() { return phase; } - + @Override public String getSubphase() { return subphase; diff --git a/java/google/registry/model/domain/fee12/FeeCheckCommandExtensionV12.java b/java/google/registry/model/domain/fee12/FeeCheckCommandExtensionV12.java index 309d86239..0bd94185a 100644 --- a/java/google/registry/model/domain/fee12/FeeCheckCommandExtensionV12.java +++ b/java/google/registry/model/domain/fee12/FeeCheckCommandExtensionV12.java @@ -36,17 +36,12 @@ public class FeeCheckCommandExtensionV12 extends ImmutableObject FeeCheckResponseExtensionV12> { CurrencyUnit currency; - - @Override - public boolean isCurrencySupported() { - return true; - } @Override public CurrencyUnit getCurrency() { return currency; } - + @XmlElement(name = "command") Set items;