diff --git a/java/google/registry/tools/GtechTool.java b/java/google/registry/tools/GtechTool.java index 4201500f6..a91ad68d7 100644 --- a/java/google/registry/tools/GtechTool.java +++ b/java/google/registry/tools/GtechTool.java @@ -67,6 +67,7 @@ public final class GtechTool { .put("registrar_activity_report", RegistrarActivityReportCommand.class) .put("registrar_contact", RegistrarContactCommand.class) .put("setup_ote", SetupOteCommand.class) + .put("uniform_rapid_suspension", UniformRapidSuspensionCommand.class) .put("update_registrar", UpdateRegistrarCommand.class) .put("update_sandbox_tld", UpdateSandboxTldCommand.class) .put("update_server_locks", UpdateServerLocksCommand.class) diff --git a/java/google/registry/tools/UniformRapidSuspensionCommand.java b/java/google/registry/tools/UniformRapidSuspensionCommand.java new file mode 100644 index 000000000..aafe094cc --- /dev/null +++ b/java/google/registry/tools/UniformRapidSuspensionCommand.java @@ -0,0 +1,149 @@ +// 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.collect.Sets.difference; +import static google.registry.model.EppResourceUtils.checkResourcesExist; +import static google.registry.model.EppResourceUtils.loadByUniqueId; +import static org.joda.time.DateTimeZone.UTC; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSortedSet; +import com.google.template.soy.data.SoyMapData; + +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; + +import google.registry.model.domain.DomainResource; +import google.registry.model.domain.ReferenceUnion; +import google.registry.model.eppcommon.StatusValue; +import google.registry.model.host.HostResource; +import google.registry.tools.Command.GtechCommand; +import google.registry.tools.soy.UniformRapidSuspensionSoyInfo; + +import org.joda.time.DateTime; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** A command to suspend a domain for the Uniform Rapid Suspension process. */ +@Parameters(separators = " =", + commandDescription = "Suspend a domain for Uniform Rapid Suspension.") +final class UniformRapidSuspensionCommand extends MutatingEppToolCommand implements GtechCommand { + + private static final ImmutableSet URS_LOCKS = ImmutableSet.of( + StatusValue.SERVER_DELETE_PROHIBITED.getXmlName(), + StatusValue.SERVER_TRANSFER_PROHIBITED.getXmlName(), + StatusValue.SERVER_UPDATE_PROHIBITED.getXmlName()); + + /** Client id that made this change. Only recorded in the history entry. **/ + private static final String CLIENT_ID = "CharlestonRoad"; + + @Parameter( + names = {"-n", "--domain_name"}, + description = "Domain to suspend.", + required = true) + private String domainName; + + @Parameter( + names = {"-h", "--hosts"}, + description = "Comma-delimited set of fully qualified host names to replace the current hosts" + + " on the domain.") + private List newHosts = new ArrayList<>(); + + @Parameter( + names = {"-p", "--preserve"}, + description = "Comma-delimited set of locks to preserve (only valid with --undo). " + + "Valid locks: serverDeleteProhibited, serverTransferProhibited, serverUpdateProhibited") + private List locksToPreserve = new ArrayList<>(); + + @Parameter( + names = {"--undo"}, + description = "Flag indicating that is is an undo command, which removes locks.") + private boolean undo; + + /** Set of existing locks that need to be preserved during undo, sorted for nicer output. */ + ImmutableSortedSet existingLocks; + + /** Set of existing nameservers that need to be restored during undo, sorted for nicer output. */ + ImmutableSortedSet existingNameservers; + + @Override + protected void initMutatingEppToolCommand() { + superuser = true; + DateTime now = DateTime.now(UTC); + ImmutableSet newHostsSet = ImmutableSet.copyOf(newHosts); + DomainResource domain = loadByUniqueId(DomainResource.class, domainName, now); + checkArgument(domain != null, "Domain '%s' does not exist", domainName); + Set missingHosts = + difference(newHostsSet, checkResourcesExist(HostResource.class, newHosts, now)); + checkArgument(missingHosts.isEmpty(), "Hosts do not exist: %s", missingHosts); + checkArgument( + locksToPreserve.isEmpty() || undo, + "Locks can only be preserved when running with --undo"); + existingNameservers = getExistingNameservers(domain); + existingLocks = getExistingLocks(domain); + setSoyTemplate( + UniformRapidSuspensionSoyInfo.getInstance(), + UniformRapidSuspensionSoyInfo.UNIFORMRAPIDSUSPENSION); + addSoyRecord(CLIENT_ID, new SoyMapData( + "domainName", domainName, + "hostsToAdd", difference(newHostsSet, existingNameservers), + "hostsToRemove", difference(existingNameservers, newHostsSet), + "locksToApply", undo ? ImmutableSet.of() : URS_LOCKS, + "locksToRemove", + undo ? difference(URS_LOCKS, ImmutableSet.copyOf(locksToPreserve)) : ImmutableSet.of(), + "reason", (undo ? "Undo " : "") + "Uniform Rapid Suspension")); + } + + private ImmutableSortedSet getExistingNameservers(DomainResource domain) { + ImmutableSortedSet.Builder nameservers = ImmutableSortedSet.naturalOrder(); + for (ReferenceUnion nameserverRef : domain.getNameservers()) { + nameservers.add(nameserverRef.getLinked().get().getForeignKey()); + } + return nameservers.build(); + } + + private ImmutableSortedSet getExistingLocks(DomainResource domain) { + ImmutableSortedSet.Builder locks = ImmutableSortedSet.naturalOrder(); + for (StatusValue lock : domain.getStatusValues()) { + if (URS_LOCKS.contains(lock.getXmlName())) { + locks.add(lock.getXmlName()); + } + } + return locks.build(); + } + + @Override + protected String postExecute() throws Exception { + if (undo) { + return ""; + } + StringBuilder undoBuilder = new StringBuilder("UNDO COMMAND:\n\ngtech_tool -e ") + .append(RegistryToolEnvironment.get()) + .append(" uniform_rapid_suspension --undo --domain_name ") + .append(domainName); + if (!existingNameservers.isEmpty()) { + undoBuilder.append(" --hosts ").append(Joiner.on(',').join(existingNameservers)); + } + if (!existingLocks.isEmpty()) { + undoBuilder.append(" --preserve ").append(Joiner.on(',').join(existingLocks)); + } + return undoBuilder.toString(); + } +} diff --git a/java/google/registry/tools/soy/UniformRapidSuspension.soy b/java/google/registry/tools/soy/UniformRapidSuspension.soy new file mode 100644 index 000000000..5a83602ad --- /dev/null +++ b/java/google/registry/tools/soy/UniformRapidSuspension.soy @@ -0,0 +1,54 @@ +{namespace domain.registry.tools autoescape="strict"} + +/** + * Uniform Rapid Suspension + */ +{template .uniformrapidsuspension} +{@param domainName: string} +{@param hostsToAdd: list} +{@param hostsToRemove: list} +{@param locksToApply: list} +{@param locksToRemove: list} +{@param reason: string} + + + + + + {$domainName} + + {if length($hostsToAdd) > 0} + + {foreach $ha in $hostsToAdd} + {$ha} + {/foreach} + + {/if} + {foreach $la in $locksToApply} + + {/foreach} + + + {if length($hostsToRemove) > 0} + + {foreach $hr in $hostsToRemove} + {$hr} + {/foreach} + + {/if} + {foreach $lr in $locksToRemove} + + {/foreach} + + + + + + {$reason} + false + + + GTechTool + + +{/template} diff --git a/javatests/google/registry/tools/UniformRapidSuspensionCommandTest.java b/javatests/google/registry/tools/UniformRapidSuspensionCommandTest.java new file mode 100644 index 000000000..914fd5c5e --- /dev/null +++ b/javatests/google/registry/tools/UniformRapidSuspensionCommandTest.java @@ -0,0 +1,162 @@ +// 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 google.registry.testing.DatastoreHelper.newDomainResource; +import static google.registry.testing.DatastoreHelper.persistActiveDomain; +import static google.registry.testing.DatastoreHelper.persistActiveHost; +import static google.registry.testing.DatastoreHelper.persistResource; + +import com.google.common.collect.ImmutableSet; + +import com.beust.jcommander.ParameterException; +import com.googlecode.objectify.Ref; + +import google.registry.model.domain.ReferenceUnion; +import google.registry.model.eppcommon.StatusValue; +import google.registry.model.host.HostResource; +import google.registry.model.registrar.Registrar; + +import org.junit.Before; +import org.junit.Test; + +/** Unit tests for {@link UniformRapidSuspensionCommand}. */ +public class UniformRapidSuspensionCommandTest + extends EppToolCommandTestCase { + + @Before + public void initRegistrar() { + // Since the command's history client ID must be CharlestonRoad, resave TheRegistrar that way. + persistResource(Registrar.loadByClientId("TheRegistrar").asBuilder() + .setClientIdentifier("CharlestonRoad") + .build()); + } + + private void persistDomainWithHosts(HostResource... hosts) { + ImmutableSet.Builder> hostRefs = new ImmutableSet.Builder<>(); + for (HostResource host : hosts) { + hostRefs.add(ReferenceUnion.create(Ref.create(host))); + } + persistResource(newDomainResource("evil.tld").asBuilder() + .setNameservers(hostRefs.build()) + .build()); + } + + @Test + public void testCommand_addsLocksReplacesHostsPrintsUndo() throws Exception { + persistActiveHost("urs1.example.com"); + persistActiveHost("urs2.example.com"); + persistDomainWithHosts( + persistActiveHost("ns1.example.com"), + persistActiveHost("ns2.example.com")); + runCommandForced("--domain_name=evil.tld", "--hosts=urs1.example.com,urs2.example.com"); + eppVerifier() + .setClientIdentifier("CharlestonRoad") + .asSuperuser() + .verifySent("testdata/uniform_rapid_suspension.xml"); + assertInStdout("uniform_rapid_suspension " + + "--undo " + + "--domain_name evil.tld " + + "--hosts ns1.example.com,ns2.example.com"); + } + + @Test + public void testCommand_respectsExistingHost() throws Exception { + persistActiveHost("urs1.example.com"); + persistDomainWithHosts( + persistActiveHost("urs2.example.com"), + persistActiveHost("ns1.example.com")); + runCommandForced("--domain_name=evil.tld", "--hosts=urs1.example.com,urs2.example.com"); + eppVerifier() + .setClientIdentifier("CharlestonRoad") + .asSuperuser() + .verifySent("testdata/uniform_rapid_suspension_existing_host.xml"); + assertInStdout("uniform_rapid_suspension " + + "--undo " + + "--domain_name evil.tld " + + "--hosts ns1.example.com,urs2.example.com"); + } + + @Test + public void testCommand_generatesUndoForUndelegatedDomain() throws Exception { + persistActiveHost("urs1.example.com"); + persistActiveHost("urs2.example.com"); + persistActiveDomain("evil.tld"); + runCommandForced("--domain_name=evil.tld", "--hosts=urs1.example.com,urs2.example.com"); + assertInStdout("uniform_rapid_suspension --undo --domain_name evil.tld"); + } + + @Test + public void testCommand_generatesUndoWithPreserve() throws Exception { + persistResource( + newDomainResource("evil.tld").asBuilder() + .addStatusValue(StatusValue.SERVER_DELETE_PROHIBITED) + .build()); + runCommandForced("--domain_name=evil.tld"); + assertInStdout( + "uniform_rapid_suspension --undo --domain_name evil.tld --preserve serverDeleteProhibited"); + } + + @Test + public void testUndo_removesLocksReplacesHosts() throws Exception { + persistActiveHost("ns1.example.com"); + persistActiveHost("ns2.example.com"); + persistDomainWithHosts( + persistActiveHost("urs1.example.com"), + persistActiveHost("urs2.example.com")); + runCommandForced( + "--domain_name=evil.tld", "--undo", "--hosts=ns1.example.com,ns2.example.com"); + eppVerifier() + .setClientIdentifier("CharlestonRoad") + .asSuperuser() + .verifySent("testdata/uniform_rapid_suspension_undo.xml"); + assertNotInStdout("--undo"); // Undo shouldn't print a new undo command. + } + + @Test + public void testUndo_respectsPreserveFlag() throws Exception { + persistActiveHost("ns1.example.com"); + persistActiveHost("ns2.example.com"); + persistDomainWithHosts( + persistActiveHost("urs1.example.com"), + persistActiveHost("urs2.example.com")); + runCommandForced( + "--domain_name=evil.tld", + "--undo", + "--preserve=serverDeleteProhibited", + "--hosts=ns1.example.com,ns2.example.com"); + eppVerifier() + .setClientIdentifier("CharlestonRoad") + .asSuperuser() + .verifySent("testdata/uniform_rapid_suspension_undo_preserve.xml"); + assertNotInStdout("--undo"); // Undo shouldn't print a new undo command. + } + + @Test + public void testFailure_preserveWithoutUndo() throws Exception { + persistActiveDomain("evil.tld"); + thrown.expect(IllegalArgumentException.class, "--undo"); + runCommandForced("--domain_name=evil.tld", "--preserve=serverDeleteProhibited"); + } + + @Test + public void testFailure_domainNameRequired() throws Exception { + persistActiveHost("urs1.example.com"); + persistActiveHost("urs2.example.com"); + persistActiveDomain("evil.tld"); + thrown.expect(ParameterException.class, "--domain_name"); + runCommandForced("--hosts=urs1.example.com,urs2.example.com"); + } +} diff --git a/javatests/google/registry/tools/testdata/uniform_rapid_suspension.xml b/javatests/google/registry/tools/testdata/uniform_rapid_suspension.xml new file mode 100644 index 000000000..8f5b2d06b --- /dev/null +++ b/javatests/google/registry/tools/testdata/uniform_rapid_suspension.xml @@ -0,0 +1,32 @@ + + + + + + evil.tld + + + urs1.example.com + urs2.example.com + + + + + + + + ns2.example.com + ns1.example.com + + + + + + + Uniform Rapid Suspension + false + + + GTechTool + + diff --git a/javatests/google/registry/tools/testdata/uniform_rapid_suspension_existing_host.xml b/javatests/google/registry/tools/testdata/uniform_rapid_suspension_existing_host.xml new file mode 100644 index 000000000..4f098fb84 --- /dev/null +++ b/javatests/google/registry/tools/testdata/uniform_rapid_suspension_existing_host.xml @@ -0,0 +1,30 @@ + + + + + + evil.tld + + + urs1.example.com + + + + + + + + ns1.example.com + + + + + + + Uniform Rapid Suspension + false + + + GTechTool + + diff --git a/javatests/google/registry/tools/testdata/uniform_rapid_suspension_undo.xml b/javatests/google/registry/tools/testdata/uniform_rapid_suspension_undo.xml new file mode 100644 index 000000000..734ac602a --- /dev/null +++ b/javatests/google/registry/tools/testdata/uniform_rapid_suspension_undo.xml @@ -0,0 +1,32 @@ + + + + + + evil.tld + + + ns1.example.com + ns2.example.com + + + + + urs1.example.com + urs2.example.com + + + + + + + + + + Undo Uniform Rapid Suspension + false + + + GTechTool + + diff --git a/javatests/google/registry/tools/testdata/uniform_rapid_suspension_undo_preserve.xml b/javatests/google/registry/tools/testdata/uniform_rapid_suspension_undo_preserve.xml new file mode 100644 index 000000000..403dabcf1 --- /dev/null +++ b/javatests/google/registry/tools/testdata/uniform_rapid_suspension_undo_preserve.xml @@ -0,0 +1,31 @@ + + + + + + evil.tld + + + ns1.example.com + ns2.example.com + + + + + urs1.example.com + urs2.example.com + + + + + + + + + Undo Uniform Rapid Suspension + false + + + GTechTool + +