Add RegistryLockVerifyAction (#461)

* Add RegistryLockVerifyAction

The action takes two parameters
- isLock is a boolean, determining whether we're trying to lock or
unlock a domain
- lockVerificationCode is the UUID by which we'll look up the lock
object in question.

The lock in question must not be expired and must be in a valid lockable
/ unlockable state

* Some responses to CR

* Add slash and move test method

* Add more data and tests

* Fix screenshot
This commit is contained in:
gbrodman 2020-01-29 16:36:39 -05:00 committed by GitHub
parent 955c3d9aeb
commit daaf231d39
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 578 additions and 4 deletions

View file

@ -30,6 +30,7 @@ import google.registry.tools.UpdateReservedListCommandTest;
import google.registry.tools.server.CreatePremiumListActionTest;
import google.registry.tools.server.UpdatePremiumListActionTest;
import google.registry.ui.server.registrar.RegistryLockGetActionTest;
import google.registry.ui.server.registrar.RegistryLockVerifyActionTest;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.ClassRule;
@ -60,6 +61,7 @@ import org.junit.runners.Suite.SuiteClasses;
PremiumListDaoTest.class,
RegistryLockDaoTest.class,
RegistryLockGetActionTest.class,
RegistryLockVerifyActionTest.class,
ReservedListDaoTest.class,
UnlockDomainCommandTest.class,
UpdatePremiumListActionTest.class,

View file

@ -83,7 +83,8 @@ public final class RegistryTestServer {
route("/registrar-ote-setup", FrontendServlet.class),
route("/registrar-ote-status", FrontendServlet.class),
route("/registrar-settings", FrontendServlet.class),
route("/registry-lock-get", FrontendServlet.class));
route("/registry-lock-get", FrontendServlet.class),
route("/registry-lock-verify", FrontendServlet.class));
private static final ImmutableList<Class<? extends Filter>> FILTERS = ImmutableList.of(
ObjectifyFilter.class,

View file

@ -0,0 +1,345 @@
// 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.ui.server.registrar;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.testing.DatastoreHelper.createTlds;
import static google.registry.testing.DatastoreHelper.getOnlyHistoryEntryOfType;
import static google.registry.testing.DatastoreHelper.newDomainBase;
import static google.registry.testing.DatastoreHelper.persistActiveHost;
import static google.registry.testing.DatastoreHelper.persistResource;
import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STATUSES;
import static javax.servlet.http.HttpServletResponse.SC_MOVED_TEMPORARILY;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.common.collect.ImmutableMap;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.domain.DomainBase;
import google.registry.model.host.HostResource;
import google.registry.model.registry.Registry;
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.request.auth.AuthLevel;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.UserAuthInfo;
import google.registry.schema.domain.RegistryLock;
import google.registry.security.XsrfTokenManager;
import google.registry.testing.AppEngineRule;
import google.registry.testing.DatastoreHelper;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeResponse;
import google.registry.testing.InjectRule;
import google.registry.testing.UserInfo;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
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;
@RunWith(JUnit4.class)
public final class RegistryLockVerifyActionTest {
private final FakeClock fakeClock = new FakeClock();
@Rule
public final AppEngineRule appEngineRule =
AppEngineRule.builder()
.withDatastore()
.withUserService(UserInfo.create("marla.singer@example.com", "12345"))
.build();
@Rule
public final JpaIntegrationWithCoverageRule jpaRule =
new JpaTestRules.Builder().withClock(fakeClock).buildIntegrationWithCoverageRule();
@Rule public final InjectRule inject = new InjectRule();
private final HttpServletRequest request = mock(HttpServletRequest.class);
private final UserService userService = UserServiceFactory.getUserService();
private final User user = new User("marla.singer@example.com", "gmail.com", "12345");
private final String lockId = "f1be78a2-2d61-458c-80f0-9dd8f2f8625f";
private FakeResponse response;
private DomainBase domain;
private AuthResult authResult;
private RegistryLockVerifyAction action;
@Before
public void setup() {
createTlds("tld", "net");
HostResource host = persistActiveHost("ns1.example.net");
domain = persistResource(newDomainBase("example.tld", host));
when(request.getRequestURI()).thenReturn("https://registry.example/registry-lock-verification");
action = createAction(lockId, true);
}
@Test
public void testSuccess_lockDomain() {
RegistryLockDao.save(createLock());
action.run();
assertThat(response.getStatus()).isEqualTo(SC_OK);
assertThat(reloadDomain().getStatusValues()).containsExactlyElementsIn(REGISTRY_LOCK_STATUSES);
assertThat(response.getPayload()).contains("Success: lock has been applied to example.tld");
HistoryEntry historyEntry = getOnlyHistoryEntryOfType(domain, HistoryEntry.Type.DOMAIN_UPDATE);
assertThat(historyEntry.getRequestedByRegistrar()).isTrue();
assertThat(historyEntry.getBySuperuser()).isFalse();
assertThat(historyEntry.getReason())
.isEqualTo("Lock or unlock of a domain through a RegistryLock operation");
assertBillingEvent(historyEntry);
}
@Test
public void testSuccess_unlockDomain() {
action = createAction(lockId, false);
domain = persistResource(domain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build());
RegistryLockDao.save(
createLock().asBuilder().setUnlockRequestTimestamp(fakeClock.nowUtc()).build());
action.run();
assertThat(response.getStatus()).isEqualTo(SC_OK);
assertThat(response.getPayload()).contains("Success: unlock has been applied to example.tld");
assertThat(reloadDomain().getStatusValues()).containsNoneIn(REGISTRY_LOCK_STATUSES);
HistoryEntry historyEntry = getOnlyHistoryEntryOfType(domain, HistoryEntry.Type.DOMAIN_UPDATE);
assertThat(historyEntry.getRequestedByRegistrar()).isTrue();
assertThat(historyEntry.getBySuperuser()).isFalse();
assertThat(historyEntry.getReason())
.isEqualTo("Lock or unlock of a domain through a RegistryLock operation");
assertBillingEvent(historyEntry);
}
@Test
public void testSuccess_adminLock_createsOnlyHistoryEntry() {
action.authResult = AuthResult.create(AuthLevel.USER, UserAuthInfo.create(user, true));
RegistryLockDao.save(createLock().asBuilder().isSuperuser(true).build());
action.run();
HistoryEntry historyEntry = getOnlyHistoryEntryOfType(domain, HistoryEntry.Type.DOMAIN_UPDATE);
assertThat(historyEntry.getRequestedByRegistrar()).isFalse();
assertThat(historyEntry.getBySuperuser()).isTrue();
DatastoreHelper.assertNoBillingEvents();
}
@Test
public void testFailure_badVerificationCode() {
RegistryLockDao.save(
createLock().asBuilder().setVerificationCode(UUID.randomUUID().toString()).build());
action.run();
assertThat(response.getPayload()).contains("Failed: Invalid verification code");
assertNoDomainChanges();
}
@Test
public void testFailure_alreadyVerified() {
RegistryLockDao.save(
createLock().asBuilder().setLockCompletionTimestamp(fakeClock.nowUtc()).build());
action.run();
assertThat(response.getPayload()).contains("Failed: Domain example.tld is already locked");
assertNoDomainChanges();
}
@Test
public void testFailure_expired() {
RegistryLockDao.save(createLock());
fakeClock.advanceBy(Duration.standardHours(2));
action.run();
assertThat(response.getPayload())
.contains("Failed: The pending lock has expired; please try again");
assertNoDomainChanges();
}
@Test
public void testFailure_nonAdmin_verifyingAdminLock() {
RegistryLockDao.save(createLock().asBuilder().isSuperuser(true).build());
action.run();
assertThat(response.getPayload()).contains("Failed: Non-admin user cannot complete admin lock");
assertNoDomainChanges();
}
@Test
public void testFailure_alreadyUnlocked() {
action = createAction(lockId, false);
RegistryLockDao.save(
createLock()
.asBuilder()
.setLockCompletionTimestamp(fakeClock.nowUtc())
.setUnlockRequestTimestamp(fakeClock.nowUtc())
.setUnlockCompletionTimestamp(fakeClock.nowUtc())
.build());
action.run();
assertThat(response.getPayload()).contains("Failed: Domain example.tld is already unlocked");
assertNoDomainChanges();
}
@Test
public void testFailure_alreadyLocked() {
RegistryLockDao.save(createLock());
domain = persistResource(domain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build());
action.run();
assertThat(response.getPayload()).contains("Failed: Domain example.tld is already locked");
assertNoDomainChanges();
}
@Test
public void testFailure_notLoggedIn() {
action.authResult = AuthResult.NOT_AUTHENTICATED;
action.run();
assertThat(response.getStatus()).isEqualTo(SC_MOVED_TEMPORARILY);
assertThat(response.getHeaders()).containsKey("Location");
assertNoDomainChanges();
}
@Test
public void testFailure_doesNotChangeLockObject() {
// A failure when performing Datastore actions means that no actions should be taken in the
// Cloud SQL RegistryLock object
RegistryLock lock = createLock();
RegistryLockDao.save(lock);
// reload the lock to pick up creation time
lock = RegistryLockDao.getByVerificationCode(lock.getVerificationCode()).get();
fakeClock.advanceOneMilli();
domain = persistResource(domain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build());
action.run();
// we would have failed during the Datastore segment of the action
assertThat(response.getPayload()).contains("Failed: Domain example.tld is already locked");
// verify that the changes to the SQL object were rolled back
RegistryLock afterAction =
RegistryLockDao.getByVerificationCode(lock.getVerificationCode()).get();
assertThat(afterAction).isEqualTo(lock);
}
@Test
public void testFailure_isLockTrue_shouldBeFalse() {
domain = persistResource(domain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build());
RegistryLockDao.save(
createLock()
.asBuilder()
.setLockCompletionTimestamp(fakeClock.nowUtc())
.setUnlockRequestTimestamp(fakeClock.nowUtc())
.build());
action.run();
assertThat(response.getPayload()).contains("Failed: Domain example.tld is already locked");
}
@Test
public void testFailure_isLockFalse_shouldBeTrue() {
action = createAction(lockId, false);
RegistryLockDao.save(createLock());
action.run();
assertThat(response.getPayload()).contains("Failed: Domain example.tld is already unlocked");
}
@Test
public void testFailure_lock_unlock_lockAgain() {
RegistryLock lock = RegistryLockDao.save(createLock());
action.run();
assertThat(response.getPayload()).contains("Success: lock has been applied to example.tld");
String unlockVerificationCode = "some-unlock-code";
RegistryLockDao.save(
lock.asBuilder()
.setVerificationCode(unlockVerificationCode)
.setUnlockRequestTimestamp(fakeClock.nowUtc())
.build());
action = createAction(unlockVerificationCode, false);
action.run();
assertThat(response.getPayload()).contains("Success: unlock has been applied to example.tld");
action = createAction(lockId, true);
action.run();
assertThat(response.getPayload()).contains("Failed: Invalid verification code");
}
@Test
public void testFailure_lock_lockAgain() {
RegistryLockDao.save(createLock());
action.run();
assertThat(response.getPayload()).contains("Success: lock has been applied to example.tld");
action = createAction(lockId, true);
action.run();
assertThat(response.getPayload()).contains("Failed: Domain example.tld is already locked");
}
@Test
public void testFailure_unlock_unlockAgain() {
action = createAction(lockId, false);
domain = persistResource(domain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build());
RegistryLockDao.save(
createLock().asBuilder().setUnlockRequestTimestamp(fakeClock.nowUtc()).build());
action.run();
assertThat(response.getStatus()).isEqualTo(SC_OK);
assertThat(response.getPayload()).contains("Success: unlock has been applied to example.tld");
action = createAction(lockId, false);
action.run();
assertThat(response.getPayload()).contains("Failed: Domain example.tld is already unlocked");
}
private RegistryLock createLock() {
return new RegistryLock.Builder()
.setDomainName("example.tld")
.setRegistrarId("TheRegistrar")
.setRepoId("repoId")
.setRegistrarPocId("marla.singer@example.com")
.isSuperuser(false)
.setVerificationCode(lockId)
.build();
}
private DomainBase reloadDomain() {
return ofy().load().entity(domain).now();
}
private void assertNoDomainChanges() {
assertThat(reloadDomain()).isEqualTo(domain);
}
private void assertBillingEvent(HistoryEntry historyEntry) {
DatastoreHelper.assertBillingEvents(
new BillingEvent.OneTime.Builder()
.setReason(Reason.SERVER_STATUS)
.setTargetId(domain.getForeignKey())
.setClientId(domain.getCurrentSponsorClientId())
.setCost(Registry.get(domain.getTld()).getServerStatusChangeCost())
.setEventTime(fakeClock.nowUtc())
.setBillingTime(fakeClock.nowUtc())
.setParent(historyEntry)
.build());
}
private RegistryLockVerifyAction createAction(String lockVerificationCode, Boolean isLock) {
response = new FakeResponse();
RegistryLockVerifyAction action =
new RegistryLockVerifyAction(fakeClock, lockVerificationCode, isLock);
authResult = AuthResult.create(AuthLevel.USER, UserAuthInfo.create(user, false));
action.req = request;
action.response = response;
action.authResult = authResult;
action.userService = userService;
action.logoFilename = "logo.png";
action.productName = "Nomulus";
action.analyticsConfig = ImmutableMap.of("googleAnalyticsId", "sampleId");
action.xsrfTokenManager = new XsrfTokenManager(new FakeClock(), action.userService);
return action;
}
}

View file

@ -18,7 +18,9 @@ import static google.registry.server.Fixture.BASIC;
import static google.registry.server.Route.route;
import static google.registry.testing.AppEngineRule.makeRegistrar2;
import static google.registry.testing.AppEngineRule.makeRegistrarContact2;
import static google.registry.testing.DatastoreHelper.createTld;
import static google.registry.testing.DatastoreHelper.loadRegistrar;
import static google.registry.testing.DatastoreHelper.newDomainBase;
import static google.registry.testing.DatastoreHelper.persistResource;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
@ -26,7 +28,9 @@ import com.google.common.collect.ImmutableMap;
import com.googlecode.objectify.ObjectifyFilter;
import google.registry.model.ofy.OfyFilter;
import google.registry.model.registrar.Registrar.State;
import google.registry.model.registry.RegistryLockDao;
import google.registry.module.frontend.FrontendServlet;
import google.registry.schema.domain.RegistryLock;
import google.registry.server.RegistryTestServer;
import google.registry.testing.CertificateSamples;
import org.junit.Rule;
@ -46,7 +50,8 @@ public class RegistrarConsoleScreenshotTest extends WebDriverTestCase {
.setRoutes(
route("/registrar", FrontendServlet.class),
route("/registrar-ote-status", FrontendServlet.class),
route("/registrar-settings", FrontendServlet.class))
route("/registrar-settings", FrontendServlet.class),
route("/registry-lock-verify", FrontendServlet.class))
.setFilters(ObjectifyFilter.class, OfyFilter.class)
.setFixtures(BASIC)
.setEmail("Marla.Singer@google.com")
@ -370,4 +375,36 @@ public class RegistrarConsoleScreenshotTest extends WebDriverTestCase {
Thread.sleep(500);
driver.diffPage("page");
}
@Test
public void registryLockVerify_success() throws Throwable {
String lockVerificationCode = "f1be78a2-2d61-458c-80f0-9dd8f2f8625f";
server.runInAppEngineEnvironment(
() -> {
createTld("tld");
persistResource(newDomainBase("example.tld"));
RegistryLockDao.save(
new RegistryLock.Builder()
.setRegistrarPocId("johndoe@theregistrar.com")
.setRepoId("repoId")
.setRegistrarId("TheRegistrar")
.setVerificationCode("f1be78a2-2d61-458c-80f0-9dd8f2f8625f")
.isSuperuser(false)
.setDomainName("example.tld")
.build());
return null;
});
driver.get(
server.getUrl(
"/registry-lock-verify?isLock=true&lockVerificationCode=" + lockVerificationCode));
driver.waitForElement(By.id("reg-content"));
driver.diffPage("page");
}
@Test
public void registryLockVerify_unknownLock() throws Throwable {
driver.get(server.getUrl("/registry-lock-verify?isLock=true&lockVerificationCode=asdfasdf"));
driver.waitForElement(By.id("reg-content"));
driver.diffPage("page");
}
}

View file

@ -6,3 +6,4 @@ PATH CLASS METHODS OK AUTH_METHODS
/registrar-ote-status OteStatusAction POST n API,LEGACY USER PUBLIC
/registrar-settings RegistrarSettingsAction POST n API,LEGACY USER PUBLIC
/registry-lock-get RegistryLockGetAction GET n API,LEGACY USER PUBLIC
/registry-lock-verify RegistryLockVerifyAction GET n API,LEGACY USER PUBLIC