From ecf1721755de276340c4b3130014dc6dc32d72d1 Mon Sep 17 00:00:00 2001 From: gbrodman Date: Thu, 20 Feb 2020 15:07:39 -0500 Subject: [PATCH] Add a scrap command to backfill registry locks (#478) * Add a scrap command to backfill registry locks * fix tests * Change comments and messages * Use URS time (best effort) if one exists * Don't bother with root cause --- .../registry/tools/CommandWithRemoteApi.java | 2 +- .../google/registry/tools/RegistryTool.java | 2 + .../BackfillRegistryLocksCommand.java | 152 +++++++++++++++ .../integration/SqlIntegrationTestSuite.java | 2 + .../BackfillRegistryLocksCommandTest.java | 176 ++++++++++++++++++ 5 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 core/src/main/java/google/registry/tools/javascrap/BackfillRegistryLocksCommand.java create mode 100644 core/src/test/java/google/registry/tools/javascrap/BackfillRegistryLocksCommandTest.java diff --git a/core/src/main/java/google/registry/tools/CommandWithRemoteApi.java b/core/src/main/java/google/registry/tools/CommandWithRemoteApi.java index 0d7dd855a..eab2c48ee 100644 --- a/core/src/main/java/google/registry/tools/CommandWithRemoteApi.java +++ b/core/src/main/java/google/registry/tools/CommandWithRemoteApi.java @@ -20,4 +20,4 @@ package google.registry.tools; *

Just implementing this is sufficient to use the remote api; {@link RegistryTool} will install * it as needed. */ -interface CommandWithRemoteApi extends Command {} +public interface CommandWithRemoteApi extends Command {} diff --git a/core/src/main/java/google/registry/tools/RegistryTool.java b/core/src/main/java/google/registry/tools/RegistryTool.java index 6e15298d9..4ffde7201 100644 --- a/core/src/main/java/google/registry/tools/RegistryTool.java +++ b/core/src/main/java/google/registry/tools/RegistryTool.java @@ -15,6 +15,7 @@ package google.registry.tools; import com.google.common.collect.ImmutableMap; +import google.registry.tools.javascrap.BackfillRegistryLocksCommand; import google.registry.tools.javascrap.PopulateNullRegistrarFieldsCommand; import google.registry.tools.javascrap.RemoveIpAddressCommand; @@ -30,6 +31,7 @@ public final class RegistryTool { public static final ImmutableMap> COMMAND_MAP = new ImmutableMap.Builder>() .put("ack_poll_messages", AckPollMessagesCommand.class) + .put("backfill_registry_locks", BackfillRegistryLocksCommand.class) .put("canonicalize_labels", CanonicalizeLabelsCommand.class) .put("check_domain", CheckDomainCommand.class) .put("check_domain_claims", CheckDomainClaimsCommand.class) diff --git a/core/src/main/java/google/registry/tools/javascrap/BackfillRegistryLocksCommand.java b/core/src/main/java/google/registry/tools/javascrap/BackfillRegistryLocksCommand.java new file mode 100644 index 000000000..7457af7ad --- /dev/null +++ b/core/src/main/java/google/registry/tools/javascrap/BackfillRegistryLocksCommand.java @@ -0,0 +1,152 @@ +// Copyright 2020 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.javascrap; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static google.registry.model.ofy.ObjectifyService.ofy; +import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STATUSES; + +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.flogger.FluentLogger; +import com.googlecode.objectify.Key; +import google.registry.config.RegistryConfig.Config; +import google.registry.model.domain.DomainBase; +import google.registry.model.registry.RegistryLockDao; +import google.registry.model.reporting.HistoryEntry; +import google.registry.schema.domain.RegistryLock; +import google.registry.tools.CommandWithRemoteApi; +import google.registry.tools.ConfirmingCommand; +import google.registry.util.Clock; +import google.registry.util.StringGenerator; +import java.util.Comparator; +import java.util.List; +import javax.inject.Inject; +import javax.inject.Named; +import org.joda.time.DateTime; + +/** + * Scrap tool to backfill {@link RegistryLock}s for domains previously locked. + * + *

This will save new objects for all existing domains that are locked but don't have any + * corresponding lock objects already in the database. + */ +@Parameters( + separators = " =", + commandDescription = + "Backfills RegistryLock objects for specified domain resource IDs that are locked but don't" + + " already have a corresponding RegistryLock object.") +public class BackfillRegistryLocksCommand extends ConfirmingCommand + implements CommandWithRemoteApi { + + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private static final int VERIFICATION_CODE_LENGTH = 32; + + @Parameter( + names = {"--domain_roids"}, + description = "Comma-separated list of domain roids to check") + protected List roids; + + // Inject here so that we can create the command automatically for tests + @Inject Clock clock; + + @Inject + @Config("registryAdminClientId") + String registryAdminClientId; + + @Inject + @Named("base58StringGenerator") + StringGenerator stringGenerator; + + private DateTime now; + private ImmutableList lockedDomains; + + @Override + protected String prompt() { + checkArgument( + roids != null && !roids.isEmpty(), "Must provide non-empty domain_roids argument"); + now = clock.nowUtc(); + lockedDomains = getLockedDomainsWithoutLocks(); + ImmutableList lockedDomainNames = + lockedDomains.stream() + .map(DomainBase::getFullyQualifiedDomainName) + .collect(toImmutableList()); + return String.format( + "Locked domains for which there does not exist a RegistryLock object: %s", + lockedDomainNames); + } + + @Override + protected String execute() { + ImmutableSet.Builder failedDomainsBuilder = new ImmutableSet.Builder<>(); + for (DomainBase domainBase : lockedDomains) { + try { + RegistryLockDao.save( + new RegistryLock.Builder() + .isSuperuser(true) + .setRegistrarId(registryAdminClientId) + .setRepoId(domainBase.getRepoId()) + .setDomainName(domainBase.getFullyQualifiedDomainName()) + .setLockCompletionTimestamp(getLockCompletionTimestamp(domainBase, now)) + .setVerificationCode(stringGenerator.createString(VERIFICATION_CODE_LENGTH)) + .build()); + } catch (Throwable t) { + logger.atSevere().withCause(t).log( + "Error when creating lock object for domain %s.", + domainBase.getFullyQualifiedDomainName()); + failedDomainsBuilder.add(domainBase); + } + } + ImmutableSet failedDomains = failedDomainsBuilder.build(); + if (failedDomains.isEmpty()) { + return String.format( + "Successfully created lock objects for %d domains.", lockedDomains.size()); + } else { + return String.format( + "Successfully created lock objects for %d domains. We failed to create locks " + + "for the following domains: %s", + lockedDomains.size() - failedDomains.size(), lockedDomains); + } + } + + private DateTime getLockCompletionTimestamp(DomainBase domainBase, DateTime now) { + // Best-effort, if a domain was URS-locked we should use that time + // If we can't find that, return now. + return ofy().load().type(HistoryEntry.class).ancestor(domainBase).list().stream() + // sort by modification time descending so we get the most recent one if it was locked twice + .sorted(Comparator.comparing(HistoryEntry::getModificationTime).reversed()) + .filter(entry -> entry.getReason().equals("Uniform Rapid Suspension")) + .findFirst() + .map(HistoryEntry::getModificationTime) + .orElse(now); + } + + private ImmutableList getLockedDomainsWithoutLocks() { + return ImmutableList.copyOf( + ofy().load() + .keys( + roids.stream() + .map(roid -> Key.create(DomainBase.class, roid)) + .collect(toImmutableList())) + .values().stream() + .filter(d -> d.getDeletionTime().isAfter(now)) + .filter(d -> d.getStatusValues().containsAll(REGISTRY_LOCK_STATUSES)) + .filter(d -> !RegistryLockDao.getMostRecentByRepoId(d.getRepoId()).isPresent()) + .collect(toImmutableList())); + } +} diff --git a/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java b/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java index 6bd2db5c9..0c3a8a26a 100644 --- a/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java +++ b/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java @@ -32,6 +32,7 @@ import google.registry.tools.LockDomainCommandTest; import google.registry.tools.UnlockDomainCommandTest; import google.registry.tools.UpdateRegistrarCommandTest; import google.registry.tools.UpdateReservedListCommandTest; +import google.registry.tools.javascrap.BackfillRegistryLocksCommandTest; import google.registry.tools.server.CreatePremiumListActionTest; import google.registry.tools.server.UpdatePremiumListActionTest; import google.registry.ui.server.registrar.RegistryLockGetActionTest; @@ -56,6 +57,7 @@ import org.junit.runners.Suite.SuiteClasses; */ @RunWith(Suite.class) @SuiteClasses({ + BackfillRegistryLocksCommandTest.class, ClaimsListDaoTest.class, CreatePremiumListActionTest.class, CreateRegistrarCommandTest.class, diff --git a/core/src/test/java/google/registry/tools/javascrap/BackfillRegistryLocksCommandTest.java b/core/src/test/java/google/registry/tools/javascrap/BackfillRegistryLocksCommandTest.java new file mode 100644 index 000000000..49aebade2 --- /dev/null +++ b/core/src/test/java/google/registry/tools/javascrap/BackfillRegistryLocksCommandTest.java @@ -0,0 +1,176 @@ +// Copyright 2020 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.javascrap; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.testing.DatastoreHelper.createTld; +import static google.registry.testing.DatastoreHelper.persistActiveDomain; +import static google.registry.testing.DatastoreHelper.persistDeletedDomain; +import static google.registry.testing.DatastoreHelper.persistNewRegistrar; +import static google.registry.testing.DatastoreHelper.persistResource; +import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STATUSES; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.truth.Truth8; +import google.registry.model.domain.DomainBase; +import google.registry.model.registrar.Registrar; +import google.registry.model.registry.RegistryLockDao; +import google.registry.model.reporting.HistoryEntry; +import google.registry.persistence.transaction.JpaTestRules; +import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationWithCoverageRule; +import google.registry.schema.domain.RegistryLock; +import google.registry.testing.DeterministicStringGenerator; +import google.registry.testing.FakeClock; +import google.registry.tools.CommandTestCase; +import google.registry.util.StringGenerator.Alphabets; +import java.util.Optional; +import org.joda.time.DateTime; +import org.joda.time.Duration; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link BackfillRegistryLocksCommand}. */ +@RunWith(JUnit4.class) +public class BackfillRegistryLocksCommandTest + extends CommandTestCase { + + private final FakeClock fakeClock = new FakeClock(); + + @Rule + public final JpaIntegrationWithCoverageRule jpaRule = + new JpaTestRules.Builder().buildIntegrationWithCoverageRule(); + + @Before + public void before() { + persistNewRegistrar("adminreg", "Admin Registrar", Registrar.Type.REAL, 693L); + createTld("tld"); + command.registryAdminClientId = "adminreg"; + command.clock = fakeClock; + command.stringGenerator = new DeterministicStringGenerator(Alphabets.BASE_58); + } + + @Test + public void testSimpleBackfill() throws Exception { + DomainBase domain = persistLockedDomain("example.tld"); + Truth8.assertThat(RegistryLockDao.getMostRecentByRepoId(domain.getRepoId())).isEmpty(); + + runCommandForced("--domain_roids", domain.getRepoId()); + + Optional lockOptional = RegistryLockDao.getMostRecentByRepoId(domain.getRepoId()); + Truth8.assertThat(lockOptional).isPresent(); + Truth8.assertThat(lockOptional.get().getLockCompletionTimestamp()).isPresent(); + } + + @Test + public void testBackfill_onlyLockedDomains() throws Exception { + DomainBase neverLockedDomain = persistActiveDomain("neverlocked.tld"); + DomainBase previouslyLockedDomain = persistLockedDomain("unlocked.tld"); + persistResource(previouslyLockedDomain.asBuilder().setStatusValues(ImmutableSet.of()).build()); + DomainBase lockedDomain = persistLockedDomain("locked.tld"); + + runCommandForced( + "--domain_roids", + String.format( + "%s,%s,%s", + neverLockedDomain.getRepoId(), + previouslyLockedDomain.getRepoId(), + lockedDomain.getRepoId())); + + ImmutableList locks = RegistryLockDao.getLockedDomainsByRegistrarId("adminreg"); + assertThat(locks).hasSize(1); + assertThat(Iterables.getOnlyElement(locks).getDomainName()).isEqualTo("locked.tld"); + } + + @Test + public void testBackfill_skipsDeletedDomains() throws Exception { + DomainBase domain = persistDeletedDomain("example.tld", fakeClock.nowUtc()); + persistResource(domain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build()); + fakeClock.advanceBy(Duration.standardSeconds(1)); + runCommandForced("--domain_roids", domain.getRepoId()); + Truth8.assertThat(RegistryLockDao.getMostRecentByRepoId(domain.getRepoId())).isEmpty(); + } + + @Test + public void testBackfill_skipsDomains_ifLockAlreadyExists() throws Exception { + DomainBase domain = persistLockedDomain("example.tld"); + + RegistryLock previousLock = + RegistryLockDao.save( + new RegistryLock.Builder() + .isSuperuser(true) + .setRegistrarId("adminreg") + .setRepoId(domain.getRepoId()) + .setDomainName(domain.getFullyQualifiedDomainName()) + .setLockCompletionTimestamp(fakeClock.nowUtc()) + .setVerificationCode(command.stringGenerator.createString(32)) + .build()); + + fakeClock.advanceBy(Duration.standardDays(1)); + runCommandForced("--domain_roids", domain.getRepoId()); + + assertThat( + RegistryLockDao.getMostRecentByRepoId(domain.getRepoId()) + .get() + .getLockCompletionTimestamp()) + .isEqualTo(previousLock.getLockCompletionTimestamp()); + } + + @Test + public void testBackfill_usesUrsTime_ifExists() throws Exception { + DateTime ursTime = fakeClock.nowUtc(); + DomainBase ursDomain = persistLockedDomain("urs.tld"); + HistoryEntry historyEntry = + new HistoryEntry.Builder() + .setBySuperuser(true) + .setClientId("adminreg") + .setModificationTime(ursTime) + .setParent(ursDomain) + .setReason("Uniform Rapid Suspension") + .setType(HistoryEntry.Type.DOMAIN_UPDATE) + .setRequestedByRegistrar(false) + .build(); + persistResource(historyEntry); + DomainBase nonUrsDomain = persistLockedDomain("nonurs.tld"); + + fakeClock.advanceBy(Duration.standardDays(10)); + runCommandForced( + "--domain_roids", String.format("%s,%s", ursDomain.getRepoId(), nonUrsDomain.getRepoId())); + + RegistryLock ursLock = + RegistryLockDao.getMostRecentVerifiedLockByRepoId(ursDomain.getRepoId()).get(); + assertThat(ursLock.getLockCompletionTimestamp().get()).isEqualTo(ursTime); + RegistryLock nonUrsLock = + RegistryLockDao.getMostRecentVerifiedLockByRepoId(nonUrsDomain.getRepoId()).get(); + assertThat(nonUrsLock.getLockCompletionTimestamp().get()).isEqualTo(fakeClock.nowUtc()); + } + + @Test + public void testFailure_mustProvideDomainRoids() { + assertThat(assertThrows(IllegalArgumentException.class, () -> runCommandForced())) + .hasMessageThat() + .isEqualTo("Must provide non-empty domain_roids argument"); + } + + private static DomainBase persistLockedDomain(String domainName) { + DomainBase domain = persistActiveDomain(domainName); + return persistResource(domain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build()); + } +}