diff --git a/java/google/registry/tools/RegistryTool.java b/java/google/registry/tools/RegistryTool.java index 36ca98165..e9755ffc6 100644 --- a/java/google/registry/tools/RegistryTool.java +++ b/java/google/registry/tools/RegistryTool.java @@ -112,6 +112,7 @@ public final class RegistryTool { .put("send_escrow_report_to_icann", SendEscrowReportToIcannCommand.class) .put("setup_ote", SetupOteCommand.class) .put("uniform_rapid_suspension", UniformRapidSuspensionCommand.class) + .put("unlock_domain", UnlockDomainCommand.class) .put("update_application_status", UpdateApplicationStatusCommand.class) .put("update_claims_notice", UpdateClaimsNoticeCommand.class) .put("update_cursors", UpdateCursorsCommand.class) diff --git a/java/google/registry/tools/UnlockDomainCommand.java b/java/google/registry/tools/UnlockDomainCommand.java new file mode 100644 index 000000000..598814629 --- /dev/null +++ b/java/google/registry/tools/UnlockDomainCommand.java @@ -0,0 +1,78 @@ +// Copyright 2017 The Nomulus 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 google.registry.model.EppResourceUtils.loadByForeignKey; +import static google.registry.util.FormattingLogger.getLoggerForCallerClass; +import static java.util.stream.Collectors.toList; +import static org.joda.time.DateTimeZone.UTC; + +import com.beust.jcommander.Parameters; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import com.google.template.soy.data.SoyMapData; +import google.registry.model.domain.DomainResource; +import google.registry.model.eppcommon.StatusValue; +import google.registry.tools.soy.DomainUpdateSoyInfo; +import google.registry.util.FormattingLogger; +import org.joda.time.DateTime; + +/** + * A command to registry unlock domain names via EPP. + * + *

A registry lock consists of server-side statuses preventing deletes, updates, and transfers. + */ +@Parameters(separators = " =", commandDescription = "Registry unlock a domain via EPP.") +public class UnlockDomainCommand extends LockOrUnlockDomainCommand { + + private static final FormattingLogger logger = getLoggerForCallerClass(); + + @Override + protected void initMutatingEppToolCommand() throws Exception { + // Project all domains as of the same time so that argument order doesn't affect behavior. + DateTime now = DateTime.now(UTC); + for (String domain : getDomains()) { + DomainResource domainResource = loadByForeignKey(DomainResource.class, domain, now); + checkArgument(domainResource != null, "Domain '%s' does not exist", domain); + ImmutableSet statusesToRemove = + Sets.intersection(domainResource.getStatusValues(), REGISTRY_LOCK_STATUSES) + .immutableCopy(); + if (statusesToRemove.isEmpty()) { + logger.infofmt("Domain '%s' is already unlocked and needs no updates.", domain); + continue; + } + + setSoyTemplate(DomainUpdateSoyInfo.getInstance(), DomainUpdateSoyInfo.DOMAINUPDATE); + addSoyRecord( + clientId, + new SoyMapData( + "domain", domain, + "add", false, + "addNameservers", ImmutableList.of(), + "addAdmins", ImmutableList.of(), + "addTechs", ImmutableList.of(), + "addStatuses", ImmutableList.of(), + "remove", true, + "removeNameservers", ImmutableList.of(), + "removeAdmins", ImmutableList.of(), + "removeTechs", ImmutableList.of(), + "removeStatuses", + statusesToRemove.stream().map(StatusValue::getXmlName).collect(toList()), + "change", false)); + } + } +} diff --git a/javatests/google/registry/tools/UnlockDomainCommandTest.java b/javatests/google/registry/tools/UnlockDomainCommandTest.java new file mode 100644 index 000000000..81949dbd7 --- /dev/null +++ b/javatests/google/registry/tools/UnlockDomainCommandTest.java @@ -0,0 +1,120 @@ +// Copyright 2017 The Nomulus 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.eppcommon.StatusValue.SERVER_DELETE_PROHIBITED; +import static google.registry.model.eppcommon.StatusValue.SERVER_TRANSFER_PROHIBITED; +import static google.registry.model.eppcommon.StatusValue.SERVER_UPDATE_PROHIBITED; +import static google.registry.testing.DatastoreHelper.newDomainResource; +import static google.registry.testing.DatastoreHelper.persistActiveDomain; +import static google.registry.testing.DatastoreHelper.persistResource; +import static google.registry.testing.JUnitBackports.expectThrows; +import static google.registry.testing.TestDataHelper.applySubstitutions; +import static google.registry.tools.server.ToolsTestData.loadUtf8; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; + +/** Unit tests for {@link UnlockDomainCommand}. */ +public class UnlockDomainCommandTest extends EppToolCommandTestCase { + + /** Gets an overridden eppVerifier that has superuser set to true on it. */ + @Override + EppToolVerifier eppVerifier() { + return new EppToolVerifier() + .withConnection(connection) + .withClientId("NewRegistrar") + .asSuperuser(); + } + + private static void persistLockedDomain(String domainName) { + persistResource( + newDomainResource(domainName) + .asBuilder() + .addStatusValues( + ImmutableSet.of( + SERVER_DELETE_PROHIBITED, SERVER_TRANSFER_PROHIBITED, SERVER_UPDATE_PROHIBITED)) + .build()); + } + + @Test + public void testSuccess_sendsCorrectEppXml() throws Exception { + persistLockedDomain("example.tld"); + runCommandForced("--client=NewRegistrar", "example.tld"); + eppVerifier() + .verifySentContents( + ImmutableList.of( + loadUtf8("domain_unlock.xml", ImmutableMap.of("DOMAIN", "example.tld")))); + } + + @Test + public void testSuccess_partiallyUpdatesStatuses() throws Exception { + persistResource( + newDomainResource("example.tld") + .asBuilder() + .addStatusValues(ImmutableSet.of(SERVER_DELETE_PROHIBITED, SERVER_UPDATE_PROHIBITED)) + .build()); + runCommandForced("--client=NewRegistrar", "example.tld"); + eppVerifier().verifySent("domain_unlock_partial_statuses.xml"); + } + + @Test + public void testSuccess_manyDomains() throws Exception { + List params = new ArrayList<>(); + List expectedXmls = new ArrayList<>(); + params.add("--client=NewRegistrar"); + String updateDomainXml = loadUtf8("domain_unlock.xml"); + // Create 26 domains -- one more than the number of entity groups allowed in a transaction (in + // case that was going to be the failure point). + for (int n = 0; n < 26; n++) { + String domain = String.format("domain%d.tld", n); + persistLockedDomain(domain); + params.add(domain); + expectedXmls.add(applySubstitutions(updateDomainXml, ImmutableMap.of("DOMAIN", domain))); + } + runCommandForced(params); + eppVerifier().verifySentContents(expectedXmls); + } + + @Test + public void testFailure_domainDoesntExist() throws Exception { + IllegalArgumentException e = + expectThrows( + IllegalArgumentException.class, + () -> runCommandForced("--client=NewRegistrar", "missing.tld")); + assertThat(e).hasMessageThat().isEqualTo("Domain 'missing.tld' does not exist"); + } + + @Test + public void testSuccess_alreadyUnlockedDomain_performsNoAction() throws Exception { + persistActiveDomain("example.tld"); + runCommandForced("--client=NewRegistrar", "example.tld"); + eppVerifier().verifyNothingSent(); + } + + @Test + public void testFailure_duplicateDomainsAreSpecified() throws Exception { + IllegalArgumentException e = + expectThrows( + IllegalArgumentException.class, + () -> runCommandForced("--client=NewRegistrar", "dupe.tld", "dupe.tld")); + assertThat(e).hasMessageThat().isEqualTo("Duplicate domain arguments found: 'dupe.tld'"); + } +} diff --git a/javatests/google/registry/tools/server/testdata/domain_unlock.xml b/javatests/google/registry/tools/server/testdata/domain_unlock.xml new file mode 100644 index 000000000..f8be65aaf --- /dev/null +++ b/javatests/google/registry/tools/server/testdata/domain_unlock.xml @@ -0,0 +1,16 @@ + + + + + + %DOMAIN% + + + + + + + + RegistryTool + + diff --git a/javatests/google/registry/tools/server/testdata/domain_unlock_partial_statuses.xml b/javatests/google/registry/tools/server/testdata/domain_unlock_partial_statuses.xml new file mode 100644 index 000000000..d8454b7b3 --- /dev/null +++ b/javatests/google/registry/tools/server/testdata/domain_unlock_partial_statuses.xml @@ -0,0 +1,15 @@ + + + + + + example.tld + + + + + + + RegistryTool + +