Refactor lock/unlock commands to use Registry Locks (#390)

* Refactor lock/unlock commands to use Registry Locks

* CR responses

* Remove unnecessary XML test files

* Add tests

* Respond to CR

* Refactor further the creation and verification of locks

* remove isUnlocked

* Responses to CR

* Fix tests

* Add admin-override back to unlocking

* Add TODO

* Fix imports
This commit is contained in:
gbrodman
2020-01-23 10:22:56 -05:00
committed by GitHub
parent 02e7106262
commit 7df3d85243
16 changed files with 907 additions and 224 deletions
@@ -30,7 +30,7 @@ public final class RegistryLockDao {
* code (there may be two instances of the same code in the database--one after lock object
* creation and one after verification.
*/
public static RegistryLock getByVerificationCode(String verificationCode) {
public static Optional<RegistryLock> getByVerificationCode(String verificationCode) {
return jpaTm()
.transact(
() -> {
@@ -42,9 +42,8 @@ public final class RegistryLockDao {
Long.class)
.setParameter("verificationCode", verificationCode)
.getSingleResult();
// TODO(gbrodman): Don't throw NPE here. Maybe NoResultException fits better?
checkNotNull(revisionId, "No registry lock with this code");
return em.find(RegistryLock.class, revisionId);
return Optional.ofNullable(revisionId)
.map(revision -> em.find(RegistryLock.class, revision));
});
}
@@ -67,8 +66,8 @@ public final class RegistryLockDao {
}
/**
* Returns the most recent lock object for a given repo ID (i.e. a domain) or empty if this domain
* hasn't been locked before.
* Returns the most recent lock object for a given domain specified by repo ID, or empty if this
* domain hasn't been locked before.
*/
public static Optional<RegistryLock> getMostRecentByRepoId(String repoId) {
return jpaTm()
@@ -86,6 +85,28 @@ public final class RegistryLockDao {
.findFirst());
}
/**
* Returns the most recent verified lock object for a given domain specified by repo ID, or empty
* if no lock has ever been finalized for this domain. This is different from {@link
* #getMostRecentByRepoId(String)} in that it only returns verified locks.
*/
public static Optional<RegistryLock> getMostRecentVerifiedLockByRepoId(String repoId) {
return jpaTm()
.transact(
() ->
jpaTm()
.getEntityManager()
.createQuery(
"SELECT lock FROM RegistryLock lock WHERE lock.repoId = :repoId AND"
+ " lock.lockCompletionTimestamp IS NOT NULL ORDER BY lock.revisionId"
+ " DESC",
RegistryLock.class)
.setParameter("repoId", repoId)
.setMaxResults(1)
.getResultStream()
.findFirst());
}
public static RegistryLock save(RegistryLock registryLock) {
checkNotNull(registryLock, "Null registry lock cannot be saved");
return jpaTm().transact(() -> jpaTm().getEntityManager().merge(registryLock));
@@ -15,6 +15,7 @@
package google.registry.schema.domain;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.util.DateTimeUtils.isBeforeOrAt;
import static google.registry.util.DateTimeUtils.toZonedDateTime;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
@@ -22,6 +23,7 @@ import google.registry.model.Buildable;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.ImmutableObject;
import google.registry.model.UpdateAutoTimestamp;
import google.registry.util.Clock;
import google.registry.util.DateTimeUtils;
import java.time.ZonedDateTime;
import java.util.Optional;
@@ -182,6 +184,20 @@ public final class RegistryLock extends ImmutableObject implements Buildable {
return lockCompletionTimestamp != null && unlockCompletionTimestamp == null;
}
/** Returns true iff the lock was requested >= 1 hour ago and has not been verified. */
public boolean isLockRequestExpired(Clock clock) {
return !getLockCompletionTimestamp().isPresent()
&& isBeforeOrAt(getLockRequestTimestamp(), clock.nowUtc().minusHours(1));
}
/** Returns true iff the unlock was requested >= 1 hour ago and has not been verified. */
public boolean isUnlockRequestExpired(Clock clock) {
Optional<DateTime> unlockRequestTimestamp = getUnlockRequestTimestamp();
return unlockRequestTimestamp.isPresent()
&& !getUnlockCompletionTimestamp().isPresent()
&& isBeforeOrAt(unlockRequestTimestamp.get(), clock.nowUtc().minusHours(1));
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
@@ -0,0 +1,269 @@
// Copyright 2019 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.loadByForeignKeyCached;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STATUSES;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.googlecode.objectify.Key;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.domain.DomainBase;
import google.registry.model.registry.Registry;
import google.registry.model.registry.RegistryLockDao;
import google.registry.model.reporting.HistoryEntry;
import google.registry.schema.domain.RegistryLock;
import google.registry.util.Clock;
import java.util.Optional;
import java.util.UUID;
import javax.annotation.Nullable;
/**
* Utility class for validating and applying {@link RegistryLock}s.
*
* <p>For both locks and unlocks, a lock must be requested via the createRegistry*Requst methods
* then verified through the verifyAndApply* methods. These methods will verify that the domain in
* question is in a lock/unlockable state and will return the lock object.
*/
public final class DomainLockUtils {
private DomainLockUtils() {}
public static RegistryLock createRegistryLockRequest(
String domainName,
String registrarId,
@Nullable String registrarPocId,
boolean isAdmin,
Clock clock) {
DomainBase domainBase = getDomain(domainName, clock);
verifyDomainNotLocked(domainBase);
// Multiple pending actions are not allowed
RegistryLockDao.getMostRecentByRepoId(domainBase.getRepoId())
.ifPresent(
previousLock ->
checkArgument(
previousLock.isLockRequestExpired(clock)
|| previousLock.getUnlockCompletionTimestamp().isPresent(),
"A pending or completed lock action already exists for %s",
previousLock.getDomainName()));
RegistryLock lock =
new RegistryLock.Builder()
.setVerificationCode(UUID.randomUUID().toString())
.setDomainName(domainName)
.setRepoId(domainBase.getRepoId())
.setRegistrarId(registrarId)
.setRegistrarPocId(registrarPocId)
.isSuperuser(isAdmin)
.build();
return RegistryLockDao.save(lock);
}
public static RegistryLock createRegistryUnlockRequest(
String domainName, String registrarId, boolean isAdmin, Clock clock) {
DomainBase domainBase = getDomain(domainName, clock);
Optional<RegistryLock> lockOptional =
RegistryLockDao.getMostRecentVerifiedLockByRepoId(domainBase.getRepoId());
RegistryLock.Builder newLockBuilder;
if (isAdmin) {
// Admins should always be able to unlock domains in case we get in a bad state
// TODO(b/147411297): Remove the admin checks / failsafes once we have migrated existing
// locked domains to have lock objects
newLockBuilder =
lockOptional
.map(RegistryLock::asBuilder)
.orElse(
new RegistryLock.Builder()
.setRepoId(domainBase.getRepoId())
.setDomainName(domainName)
.setLockCompletionTimestamp(clock.nowUtc())
.setRegistrarId(registrarId));
} else {
verifyDomainLocked(domainBase);
RegistryLock lock =
lockOptional.orElseThrow(
() ->
new IllegalArgumentException(
String.format("No lock object for domain %s", domainName)));
checkArgument(
lock.isLocked(), "Lock object for domain %s is not currently locked", domainName);
checkArgument(
!lock.getUnlockRequestTimestamp().isPresent() || lock.isUnlockRequestExpired(clock),
"A pending unlock action already exists for %s",
domainName);
checkArgument(
lock.getRegistrarId().equals(registrarId),
"Lock object does not have registrar ID %s",
registrarId);
checkArgument(
!lock.isSuperuser(), "Non-admin user cannot unlock admin-locked domain %s", domainName);
newLockBuilder = lock.asBuilder();
}
RegistryLock newLock =
newLockBuilder
.setVerificationCode(UUID.randomUUID().toString())
.isSuperuser(isAdmin)
.setUnlockRequestTimestamp(clock.nowUtc())
.setRegistrarId(registrarId)
.build();
return RegistryLockDao.save(newLock);
}
public static RegistryLock verifyAndApplyLock(
String verificationCode, boolean isAdmin, Clock clock) {
return jpaTm()
.transact(
() -> {
RegistryLock lock = getByVerificationCode(verificationCode);
checkArgument(
!lock.getLockCompletionTimestamp().isPresent(),
"Domain %s is already locked",
lock.getDomainName());
checkArgument(
!lock.isLockRequestExpired(clock),
"The pending lock has expired; please try again");
checkArgument(
!lock.isSuperuser() || isAdmin, "Non-admin user cannot complete admin lock");
RegistryLock newLock =
RegistryLockDao.save(
lock.asBuilder().setLockCompletionTimestamp(clock.nowUtc()).build());
tm().transact(() -> applyLockStatuses(newLock, clock));
return newLock;
});
}
public static RegistryLock verifyAndApplyUnlock(
String verificationCode, boolean isAdmin, Clock clock) {
return jpaTm()
.transact(
() -> {
RegistryLock lock = getByVerificationCode(verificationCode);
checkArgument(
!lock.getUnlockCompletionTimestamp().isPresent(),
"Domain %s is already unlocked",
lock.getDomainName());
checkArgument(
!lock.isUnlockRequestExpired(clock),
"The pending unlock has expired; please try again");
checkArgument(
isAdmin || !lock.isSuperuser(), "Non-admin user cannot complete admin unlock");
RegistryLock newLock =
RegistryLockDao.save(
lock.asBuilder().setUnlockCompletionTimestamp(clock.nowUtc()).build());
tm().transact(() -> removeLockStatuses(newLock, isAdmin, clock));
return newLock;
});
}
private static void verifyDomainNotLocked(DomainBase domainBase) {
checkArgument(
!domainBase.getStatusValues().containsAll(REGISTRY_LOCK_STATUSES),
"Domain %s is already locked",
domainBase.getFullyQualifiedDomainName());
}
private static void verifyDomainLocked(DomainBase domainBase) {
checkArgument(
!Sets.intersection(domainBase.getStatusValues(), REGISTRY_LOCK_STATUSES).isEmpty(),
"Domain %s is already unlocked",
domainBase.getFullyQualifiedDomainName());
}
private static DomainBase getDomain(String domainName, Clock clock) {
return loadByForeignKeyCached(DomainBase.class, domainName, clock.nowUtc())
.orElseThrow(
() -> new IllegalArgumentException(String.format("Unknown domain %s", domainName)));
}
private static RegistryLock getByVerificationCode(String verificationCode) {
return RegistryLockDao.getByVerificationCode(verificationCode)
.orElseThrow(
() ->
new IllegalArgumentException(
String.format("Invalid verification code %s", verificationCode)));
}
private static void applyLockStatuses(RegistryLock lock, Clock clock) {
DomainBase domain = getDomain(lock.getDomainName(), clock);
verifyDomainNotLocked(domain);
DomainBase newDomain =
domain
.asBuilder()
.setStatusValues(
ImmutableSet.copyOf(Sets.union(domain.getStatusValues(), REGISTRY_LOCK_STATUSES)))
.build();
saveEntities(newDomain, lock, clock);
}
private static void removeLockStatuses(RegistryLock lock, boolean isAdmin, Clock clock) {
DomainBase domain = getDomain(lock.getDomainName(), clock);
if (!isAdmin) {
verifyDomainLocked(domain);
}
DomainBase newDomain =
domain
.asBuilder()
.setStatusValues(
ImmutableSet.copyOf(
Sets.difference(domain.getStatusValues(), REGISTRY_LOCK_STATUSES)))
.build();
saveEntities(newDomain, lock, clock);
}
private static void saveEntities(DomainBase domain, RegistryLock lock, Clock clock) {
String reason = "Lock or unlock of a domain through a RegistryLock operation";
HistoryEntry historyEntry =
new HistoryEntry.Builder()
.setClientId(domain.getCurrentSponsorClientId())
.setBySuperuser(lock.isSuperuser())
.setRequestedByRegistrar(!lock.isSuperuser())
.setType(HistoryEntry.Type.DOMAIN_UPDATE)
.setModificationTime(clock.nowUtc())
.setParent(Key.create(domain))
.setReason(reason)
.build();
ofy().save().entities(domain, historyEntry);
if (!lock.isSuperuser()) { // admin actions shouldn't affect billing
BillingEvent.OneTime oneTime =
new BillingEvent.OneTime.Builder()
.setReason(Reason.SERVER_STATUS)
.setTargetId(domain.getForeignKey())
.setClientId(domain.getCurrentSponsorClientId())
.setCost(Registry.get(domain.getTld()).getServerStatusChangeCost())
.setEventTime(clock.nowUtc())
.setBillingTime(clock.nowUtc())
.setParent(historyEntry)
.build();
ofy().save().entity(oneTime);
}
}
}
@@ -14,25 +14,19 @@
package google.registry.tools;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
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.common.flogger.FluentLogger;
import com.google.template.soy.data.SoyMapData;
import google.registry.model.domain.DomainBase;
import google.registry.model.eppcommon.StatusValue;
import google.registry.tools.soy.DomainUpdateSoyInfo;
import java.util.Optional;
import google.registry.schema.domain.RegistryLock;
import org.joda.time.DateTime;
/**
* A command to registry lock domain names via EPP.
* A command to registry lock domain names.
*
* <p>A registry lock consists of server-side statuses preventing deletes, updates, and transfers.
*/
@@ -42,41 +36,35 @@ public class LockDomainCommand extends LockOrUnlockDomainCommand {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@Override
protected void initMutatingEppToolCommand() {
protected ImmutableSet<String> getRelevantDomains() {
// Project all domains as of the same time so that argument order doesn't affect behavior.
DateTime now = DateTime.now(UTC);
DateTime now = clock.nowUtc();
ImmutableSet.Builder<String> relevantDomains = new ImmutableSet.Builder<>();
for (String domain : getDomains()) {
Optional<DomainBase> domainBase = loadByForeignKey(DomainBase.class, domain, now);
checkArgumentPresent(domainBase, "Domain '%s' does not exist or is deleted", domain);
DomainBase domainBase =
loadByForeignKey(DomainBase.class, domain, now)
.orElseThrow(
() ->
new IllegalArgumentException(
String.format("Domain '%s' does not exist or is deleted", domain)));
ImmutableSet<StatusValue> statusesToAdd =
Sets.difference(REGISTRY_LOCK_STATUSES, domainBase.get().getStatusValues())
.immutableCopy();
Sets.difference(REGISTRY_LOCK_STATUSES, domainBase.getStatusValues()).immutableCopy();
if (statusesToAdd.isEmpty()) {
logger.atInfo().log("Domain '%s' is already locked and needs no updates.", domain);
continue;
}
setSoyTemplate(DomainUpdateSoyInfo.getInstance(), DomainUpdateSoyInfo.DOMAINUPDATE);
addSoyRecord(
clientId,
new SoyMapData(
"domain", domain,
"add", true,
"addNameservers", ImmutableList.of(),
"addAdmins", ImmutableList.of(),
"addTechs", ImmutableList.of(),
"addStatuses",
statusesToAdd.stream().map(StatusValue::getXmlName).collect(toImmutableList()),
"remove", false,
"removeNameservers", ImmutableList.of(),
"removeAdmins", ImmutableList.of(),
"removeTechs", ImmutableList.of(),
"removeStatuses", ImmutableList.of(),
"change", false,
"secdns", false,
"addDsRecords", ImmutableList.of(),
"removeDsRecords", ImmutableList.of(),
"removeAllDsRecords", false));
relevantDomains.add(domain);
}
return relevantDomains.build();
}
@Override
protected RegistryLock createLock(String domain) {
return DomainLockUtils.createRegistryLockRequest(domain, clientId, null, true, clock);
}
@Override
protected void finalizeLockOrUnlockRequest(RegistryLock lock) {
DomainLockUtils.verifyAndApplyLock(lock.getVerificationCode(), true, clock);
}
}
@@ -22,16 +22,23 @@ import static google.registry.util.CollectionUtils.findDuplicates;
import com.beust.jcommander.Parameter;
import com.google.common.base.Joiner;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.eppcommon.StatusValue;
import google.registry.schema.domain.RegistryLock;
import google.registry.util.Clock;
import java.util.List;
import javax.inject.Inject;
/** Shared base class for commands to registry lock or unlock a domain via EPP. */
public abstract class LockOrUnlockDomainCommand extends MutatingEppToolCommand {
public abstract class LockOrUnlockDomainCommand extends ConfirmingCommand
implements CommandWithCloudSql {
protected static final ImmutableSet<StatusValue> REGISTRY_LOCK_STATUSES =
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
static final ImmutableSet<StatusValue> REGISTRY_LOCK_STATUSES =
ImmutableSet.of(
SERVER_DELETE_PROHIBITED, SERVER_TRANSFER_PROHIBITED, SERVER_UPDATE_PROHIBITED);
@@ -48,23 +55,52 @@ public abstract class LockOrUnlockDomainCommand extends MutatingEppToolCommand {
@Config("registryAdminClientId")
String registryAdminClientId;
@Inject Clock clock;
protected ImmutableSet<String> relevantDomains = ImmutableSet.of();
protected ImmutableSet<String> getDomains() {
return ImmutableSet.copyOf(mainParameters);
}
@Override
protected void initEppToolCommand() throws Exception {
// Superuser status is required to update registry lock statuses.
superuser = true;
protected void init() {
// Default clientId to the registry registrar account if otherwise unspecified.
if (clientId == null) {
clientId = registryAdminClientId;
}
String duplicates = Joiner.on(", ").join(findDuplicates(mainParameters));
checkArgument(duplicates.isEmpty(), "Duplicate domain arguments found: '%s'", duplicates);
initMutatingEppToolCommand();
System.out.println(
"== ENSURE THAT YOU HAVE AUTHENTICATED THE REGISTRAR BEFORE RUNNING THIS COMMAND ==");
relevantDomains = getRelevantDomains();
}
@Override
protected String execute() {
int failures = 0;
for (String domain : relevantDomains) {
try {
RegistryLock lock = createLock(domain);
finalizeLockOrUnlockRequest(lock);
} catch (Throwable t) {
Throwable rootCause = Throwables.getRootCause(t);
logger.atSevere().withCause(rootCause).log("Error when (un)locking domain %s.", domain);
failures++;
}
}
if (failures == 0) {
return String.format("Successfully locked/unlocked %d domains.", relevantDomains.size());
} else {
return String.format(
"Successfully locked/unlocked %d domains with %d failures.",
relevantDomains.size() - failures, failures);
}
}
protected abstract ImmutableSet<String> getRelevantDomains();
protected abstract RegistryLock createLock(String domain);
protected abstract void finalizeLockOrUnlockRequest(RegistryLock lock);
}
@@ -14,25 +14,19 @@
package google.registry.tools;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
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.common.flogger.FluentLogger;
import com.google.template.soy.data.SoyMapData;
import google.registry.model.domain.DomainBase;
import google.registry.model.eppcommon.StatusValue;
import google.registry.tools.soy.DomainUpdateSoyInfo;
import java.util.Optional;
import google.registry.schema.domain.RegistryLock;
import org.joda.time.DateTime;
/**
* A command to registry unlock domain names via EPP.
* A command to registry unlock domain names.
*
* <p>A registry lock consists of server-side statuses preventing deletes, updates, and transfers.
*/
@@ -42,41 +36,35 @@ public class UnlockDomainCommand extends LockOrUnlockDomainCommand {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@Override
protected void initMutatingEppToolCommand() {
protected ImmutableSet<String> getRelevantDomains() {
// Project all domains as of the same time so that argument order doesn't affect behavior.
DateTime now = DateTime.now(UTC);
DateTime now = clock.nowUtc();
ImmutableSet.Builder<String> relevantDomains = new ImmutableSet.Builder<>();
for (String domain : getDomains()) {
Optional<DomainBase> domainBase = loadByForeignKey(DomainBase.class, domain, now);
checkArgumentPresent(domainBase, "Domain '%s' does not exist or is deleted", domain);
DomainBase domainBase =
loadByForeignKey(DomainBase.class, domain, now)
.orElseThrow(
() ->
new IllegalArgumentException(
String.format("Domain '%s' does not exist or is deleted", domain)));
ImmutableSet<StatusValue> statusesToRemove =
Sets.intersection(domainBase.get().getStatusValues(), REGISTRY_LOCK_STATUSES)
.immutableCopy();
Sets.intersection(domainBase.getStatusValues(), REGISTRY_LOCK_STATUSES).immutableCopy();
if (statusesToRemove.isEmpty()) {
logger.atInfo().log("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(toImmutableList()),
"change", false,
"secdns", false,
"addDsRecords", ImmutableList.of(),
"removeDsRecords", ImmutableList.of(),
"removeAllDsRecords", false));
relevantDomains.add(domain);
}
return relevantDomains.build();
}
@Override
protected RegistryLock createLock(String domain) {
return DomainLockUtils.createRegistryUnlockRequest(domain, clientId, true, clock);
}
@Override
protected void finalizeLockOrUnlockRequest(RegistryLock lock) {
DomainLockUtils.verifyAndApplyUnlock(lock.getVerificationCode(), true, clock);
}
}