1
0
mirror of https://github.com/google/nomulus synced 2026-01-04 04:04:22 +00:00

Add a reg-lock verification action to the new console (#2467)

The front end will have a (hidden) page that passes the verification
code to this API endpoint and displays the result.
This commit is contained in:
gbrodman
2024-07-08 17:25:22 -04:00
committed by GitHub
parent b602aac09a
commit b8a6ac72dd
9 changed files with 337 additions and 43 deletions

View File

@@ -114,6 +114,7 @@ import google.registry.ui.server.console.ConsoleDomainListAction;
import google.registry.ui.server.console.ConsoleDumDownloadAction;
import google.registry.ui.server.console.ConsoleEppPasswordAction;
import google.registry.ui.server.console.ConsoleRegistryLockAction;
import google.registry.ui.server.console.ConsoleRegistryLockVerifyAction;
import google.registry.ui.server.console.ConsoleUpdateRegistrarAction;
import google.registry.ui.server.console.ConsoleUserDataAction;
import google.registry.ui.server.console.RegistrarsAction;
@@ -191,6 +192,8 @@ interface RequestComponent {
ConsoleRegistryLockAction consoleRegistryLockAction();
ConsoleRegistryLockVerifyAction consoleRegistryLockVerifyAction();
ConsoleUiAction consoleUiAction();
ConsoleUpdateRegistrarAction consoleUpdateRegistrarAction();

View File

@@ -30,6 +30,7 @@ import google.registry.ui.server.console.ConsoleDomainListAction;
import google.registry.ui.server.console.ConsoleDumDownloadAction;
import google.registry.ui.server.console.ConsoleEppPasswordAction;
import google.registry.ui.server.console.ConsoleRegistryLockAction;
import google.registry.ui.server.console.ConsoleRegistryLockVerifyAction;
import google.registry.ui.server.console.ConsoleUpdateRegistrarAction;
import google.registry.ui.server.console.ConsoleUserDataAction;
import google.registry.ui.server.console.RegistrarsAction;
@@ -69,6 +70,8 @@ public interface FrontendRequestComponent {
ConsoleRegistryLockAction consoleRegistryLockAction();
ConsoleRegistryLockVerifyAction consoleRegistryLockVerifyAction();
ConsoleUiAction consoleUiAction();
ConsoleUpdateRegistrarAction consoleUpdateRegistrarAction();

View File

@@ -47,10 +47,8 @@ import google.registry.util.EmailMessage;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
import jakarta.servlet.http.HttpServletRequest;
import java.net.URISyntaxException;
import java.util.Optional;
import javax.inject.Inject;
import org.apache.http.client.utils.URIBuilder;
import org.joda.time.Duration;
/**
@@ -153,14 +151,9 @@ public class ConsoleRegistryLockAction extends ConsoleApiAction {
private void sendVerificationEmail(RegistryLock lock, String userEmail, boolean isLock) {
try {
String url =
new URIBuilder()
.setScheme("https")
.setHost(consoleApiParams.request().getServerName())
// TODO: replace this with the PATH in ConsoleRegistryLockVerifyAction once it exists
.setPath("/console-api/registry-lock-verify")
.setParameter("lockVerificationCode", lock.getVerificationCode())
.build()
.toString();
String.format(
"https://%s/console/#/registry-lock-verify?lockVerificationCode=%s",
consoleApiParams.request().getServerName(), lock.getVerificationCode());
String body = String.format(VERIFICATION_EMAIL_TEMPLATE, lock.getDomainName(), url);
ImmutableList<InternetAddress> recipients =
ImmutableList.of(new InternetAddress(userEmail, true));
@@ -171,7 +164,7 @@ public class ConsoleRegistryLockAction extends ConsoleApiAction {
.setSubject(String.format("Registry %s verification", action))
.setRecipients(recipients)
.build());
} catch (AddressException | URISyntaxException e) {
} catch (AddressException e) {
throw new RuntimeException(e); // caught above -- this is so we can run in a transaction
}
}

View File

@@ -0,0 +1,80 @@
// Copyright 2024 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.console;
import static google.registry.request.Action.Method.GET;
import com.google.common.base.Ascii;
import com.google.gson.Gson;
import com.google.gson.annotations.Expose;
import google.registry.model.console.User;
import google.registry.model.domain.RegistryLock;
import google.registry.request.Action;
import google.registry.request.Parameter;
import google.registry.request.auth.Auth;
import google.registry.tools.DomainLockUtils;
import google.registry.ui.server.registrar.ConsoleApiParams;
import jakarta.servlet.http.HttpServletResponse;
import javax.inject.Inject;
/** Handler for verifying registry lock requests, a form of 2FA. */
@Action(
service = Action.Service.DEFAULT,
path = ConsoleRegistryLockVerifyAction.PATH,
method = {GET},
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public class ConsoleRegistryLockVerifyAction extends ConsoleApiAction {
static final String PATH = "/console-api/registry-lock-verify";
private final DomainLockUtils domainLockUtils;
private final Gson gson;
private final String lockVerificationCode;
@Inject
public ConsoleRegistryLockVerifyAction(
ConsoleApiParams consoleApiParams,
DomainLockUtils domainLockUtils,
Gson gson,
@Parameter("lockVerificationCode") String lockVerificationCode) {
super(consoleApiParams);
this.domainLockUtils = domainLockUtils;
this.gson = gson;
this.lockVerificationCode = lockVerificationCode;
}
@Override
protected void getHandler(User user) {
RegistryLock lock =
domainLockUtils.verifyVerificationCode(lockVerificationCode, user.getUserRoles().isAdmin());
RegistryLockAction action =
lock.getLockCompletionTime().isPresent()
? RegistryLockAction.UNLOCKED
: RegistryLockAction.LOCKED;
RegistryLockVerificationResponse lockResponse =
new RegistryLockVerificationResponse(
Ascii.toLowerCase(action.toString()), lock.getDomainName(), lock.getRegistrarId());
consoleApiParams.response().setPayload(gson.toJson(lockResponse));
consoleApiParams.response().setStatus(HttpServletResponse.SC_OK);
}
private enum RegistryLockAction {
LOCKED,
UNLOCKED
}
private record RegistryLockVerificationResponse(
@Expose String action, @Expose String domainName, @Expose String registrarId) {}
}

View File

@@ -23,6 +23,7 @@ import com.google.common.base.Throwables;
import com.google.common.net.MediaType;
import google.registry.request.Response;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
@@ -69,7 +70,7 @@ public final class FakeResponse implements Response {
@Override
public void sendRedirect(String url) throws IOException {
status = 302;
status = HttpServletResponse.SC_FOUND;
this.payload = String.format("Redirected to %s", url);
}

View File

@@ -80,7 +80,7 @@ public class ConsoleRegistryLockActionTest {
Please click the link below to perform the lock / unlock action on domain example.test. \
Note: this code will expire in one hour.
https://registrarconsole.tld/console-api/registry-lock-verify?lockVerificationCode=\
https://registrarconsole.tld/console/#/registry-lock-verify?lockVerificationCode=\
123456789ABCDEFGHJKLMNPQRSTUVWXY""";
private static final Gson GSON = RequestModule.provideGson();
@@ -122,15 +122,7 @@ public class ConsoleRegistryLockActionTest {
@Test
void testGet_simpleLock() {
saveRegistryLock(
new RegistryLock.Builder()
.setRepoId("repoId")
.setDomainName("example.test")
.setRegistrarId("TheRegistrar")
.setVerificationCode("123456789ABCDEFGHJKLMNPQRSTUVWXY")
.setRegistrarPocId("johndoe@theregistrar.com")
.setLockCompletionTime(fakeClock.nowUtc())
.build());
saveRegistryLock(createDefaultLockBuilder().setLockCompletionTime(fakeClock.nowUtc()).build());
action.run();
assertThat(response.getStatus()).isEqualTo(SC_OK);
assertThat(response.getPayload())

View File

@@ -0,0 +1,220 @@
// Copyright 2024 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.console;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.loadByEntity;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.SqlHelper.saveRegistryLock;
import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STATUSES;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import google.registry.model.console.RegistrarRole;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.model.domain.Domain;
import google.registry.model.domain.RegistryLock;
import google.registry.model.eppcommon.StatusValue;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.request.RequestModule;
import google.registry.request.auth.AuthResult;
import google.registry.testing.CloudTasksHelper;
import google.registry.testing.ConsoleApiParamsUtils;
import google.registry.testing.DeterministicStringGenerator;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeResponse;
import google.registry.tools.DomainLockUtils;
import google.registry.ui.server.registrar.ConsoleApiParams;
import google.registry.util.StringGenerator;
import jakarta.servlet.http.HttpServletResponse;
import org.joda.time.Duration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Tests for {@link ConsoleRegistryLockVerifyAction}. */
public class ConsoleRegistryLockVerifyActionTest {
private static final String DEFAULT_CODE = "123456789ABCDEFGHJKLMNPQRSTUUUUU";
private static final Gson GSON = RequestModule.provideGson();
private final FakeClock fakeClock = new FakeClock();
@RegisterExtension
final JpaTestExtensions.JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().withClock(fakeClock).buildIntegrationTestExtension();
private FakeResponse response;
private Domain defaultDomain;
private User user;
private ConsoleRegistryLockVerifyAction action;
@BeforeEach
void beforeEach() {
createTld("test");
defaultDomain = persistActiveDomain("example.test");
user =
new User.Builder()
.setEmailAddress("user@theregistrar.com")
.setRegistryLockEmailAddress("registrylock@theregistrar.com")
.setUserRoles(
new UserRoles.Builder()
.setRegistrarRoles(
ImmutableMap.of("TheRegistrar", RegistrarRole.PRIMARY_CONTACT))
.build())
.setRegistryLockPassword("registryLockPassword")
.build();
action = createAction(DEFAULT_CODE);
}
@Test
void testSuccess_lock() {
saveRegistryLock(createDefaultLockBuilder().build());
action.run();
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
assertThat(response.getPayload())
.isEqualTo(
"{\"action\":\"unlocked\",\"domainName\":\"example.test\",\"registrarId\":\"TheRegistrar\"}");
assertThat(loadByEntity(defaultDomain).getStatusValues())
.containsAtLeastElementsIn(REGISTRY_LOCK_STATUSES);
}
@Test
void testSuccess_unlock() {
persistResource(defaultDomain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build());
saveRegistryLock(
createDefaultLockBuilder()
.setLockCompletionTime(fakeClock.nowUtc())
.setUnlockRequestTime(fakeClock.nowUtc())
.build());
action.run();
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
assertThat(response.getPayload())
.isEqualTo(
"{\"action\":\"unlocked\",\"domainName\":\"example.test\",\"registrarId\":\"TheRegistrar\"}");
assertThat(loadByEntity(defaultDomain).getStatusValues()).containsExactly(StatusValue.INACTIVE);
}
@Test
void testSuccess_admin_lock() {
saveRegistryLock(createDefaultLockBuilder().isSuperuser(true).build());
user =
user.asBuilder()
.setUserRoles(user.getUserRoles().asBuilder().setIsAdmin(true).build())
.build();
action = createAction(DEFAULT_CODE);
action.run();
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
assertThat(response.getPayload())
.isEqualTo(
"{\"action\":\"unlocked\",\"domainName\":\"example.test\",\"registrarId\":\"TheRegistrar\"}");
assertThat(loadByEntity(defaultDomain).getStatusValues())
.containsAtLeastElementsIn(REGISTRY_LOCK_STATUSES);
}
@Test
void testSuccess_admin_unlock() {
persistResource(defaultDomain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build());
saveRegistryLock(
createDefaultLockBuilder()
.isSuperuser(true)
.setLockCompletionTime(fakeClock.nowUtc())
.setUnlockRequestTime(fakeClock.nowUtc())
.build());
user =
user.asBuilder()
.setUserRoles(user.getUserRoles().asBuilder().setIsAdmin(true).build())
.build();
action = createAction(DEFAULT_CODE);
action.run();
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
assertThat(response.getPayload())
.isEqualTo(
"{\"action\":\"unlocked\",\"domainName\":\"example.test\",\"registrarId\":\"TheRegistrar\"}");
assertThat(loadByEntity(defaultDomain).getStatusValues()).containsExactly(StatusValue.INACTIVE);
}
@Test
void testFailure_invalidCode() {
saveRegistryLock(createDefaultLockBuilder().setVerificationCode("foobar").build());
action.run();
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST);
assertThat(response.getPayload())
.isEqualTo("Invalid verification code 123456789ABCDEFGHJKLMNPQRSTUUUUU");
assertThat(loadByEntity(defaultDomain).getStatusValues()).containsExactly(StatusValue.INACTIVE);
}
@Test
void testFailure_expiredLock() {
saveRegistryLock(createDefaultLockBuilder().build());
fakeClock.advanceBy(Duration.standardDays(1));
action.run();
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST);
assertThat(response.getPayload()).isEqualTo("The pending lock has expired; please try again");
assertThat(loadByEntity(defaultDomain).getStatusValues()).containsExactly(StatusValue.INACTIVE);
}
@Test
void testFailure_nonAdmin_lock() {
saveRegistryLock(createDefaultLockBuilder().isSuperuser(true).build());
action.run();
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST);
assertThat(response.getPayload()).isEqualTo("Non-admin user cannot complete admin lock");
assertThat(loadByEntity(defaultDomain).getStatusValues()).containsExactly(StatusValue.INACTIVE);
}
@Test
void testFailure_nonAdmin_unlock() {
persistResource(defaultDomain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build());
saveRegistryLock(
createDefaultLockBuilder()
.isSuperuser(true)
.setLockCompletionTime(fakeClock.nowUtc())
.setUnlockRequestTime(fakeClock.nowUtc())
.build());
action.run();
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST);
assertThat(response.getPayload()).isEqualTo("Non-admin user cannot complete admin unlock");
assertThat(loadByEntity(defaultDomain).getStatusValues())
.containsAtLeastElementsIn(REGISTRY_LOCK_STATUSES);
}
private RegistryLock.Builder createDefaultLockBuilder() {
return new RegistryLock.Builder()
.setRepoId(defaultDomain.getRepoId())
.setDomainName(defaultDomain.getDomainName())
.setRegistrarId(defaultDomain.getCurrentSponsorRegistrarId())
.setRegistrarPocId("johndoe@theregistrar.com")
.setVerificationCode(DEFAULT_CODE);
}
private ConsoleRegistryLockVerifyAction createAction(String verificationCode) {
AuthResult authResult = AuthResult.createUser(user);
ConsoleApiParams params = ConsoleApiParamsUtils.createFake(authResult);
when(params.request().getMethod()).thenReturn("GET");
when(params.request().getServerName()).thenReturn("registrarconsole.tld");
DomainLockUtils domainLockUtils =
new DomainLockUtils(
new DeterministicStringGenerator(StringGenerator.Alphabets.BASE_58),
"adminreg",
new CloudTasksHelper(fakeClock).getTestCloudTasksUtils());
response = (FakeResponse) params.response();
return new ConsoleRegistryLockVerifyAction(params, domainLockUtils, GSON, verificationCode);
}
}

View File

@@ -1,21 +1,22 @@
PATH CLASS METHODS OK MIN USER_POLICY
/_dr/epp EppTlsAction POST n APP ADMIN
/console-api/domain ConsoleDomainGetAction GET n USER PUBLIC
/console-api/domain-list ConsoleDomainListAction GET n USER PUBLIC
/console-api/dum-download ConsoleDumDownloadAction GET n USER PUBLIC
/console-api/eppPassword ConsoleEppPasswordAction POST n USER PUBLIC
/console-api/registrar ConsoleUpdateRegistrarAction POST n USER PUBLIC
/console-api/registrars RegistrarsAction GET,POST n USER PUBLIC
/console-api/registry-lock ConsoleRegistryLockAction GET,POST n USER PUBLIC
/console-api/settings/contacts ContactAction GET,POST n USER PUBLIC
/console-api/settings/security SecurityAction POST n USER PUBLIC
/console-api/settings/whois-fields WhoisRegistrarFieldsAction POST n USER PUBLIC
/console-api/userdata ConsoleUserDataAction GET n USER PUBLIC
/registrar ConsoleUiAction GET n USER PUBLIC
/registrar-create ConsoleRegistrarCreatorAction POST,GET n USER PUBLIC
/registrar-ote-setup ConsoleOteSetupAction POST,GET n USER PUBLIC
/registrar-ote-status OteStatusAction POST n USER PUBLIC
/registrar-settings RegistrarSettingsAction POST n USER PUBLIC
/registry-lock-get RegistryLockGetAction GET n USER PUBLIC
/registry-lock-post RegistryLockPostAction POST n USER PUBLIC
/registry-lock-verify RegistryLockVerifyAction GET n USER PUBLIC
PATH CLASS METHODS OK MIN USER_POLICY
/_dr/epp EppTlsAction POST n APP ADMIN
/console-api/domain ConsoleDomainGetAction GET n USER PUBLIC
/console-api/domain-list ConsoleDomainListAction GET n USER PUBLIC
/console-api/dum-download ConsoleDumDownloadAction GET n USER PUBLIC
/console-api/eppPassword ConsoleEppPasswordAction POST n USER PUBLIC
/console-api/registrar ConsoleUpdateRegistrarAction POST n USER PUBLIC
/console-api/registrars RegistrarsAction GET,POST n USER PUBLIC
/console-api/registry-lock ConsoleRegistryLockAction GET,POST n USER PUBLIC
/console-api/registry-lock-verify ConsoleRegistryLockVerifyAction GET n USER PUBLIC
/console-api/settings/contacts ContactAction GET,POST n USER PUBLIC
/console-api/settings/security SecurityAction POST n USER PUBLIC
/console-api/settings/whois-fields WhoisRegistrarFieldsAction POST n USER PUBLIC
/console-api/userdata ConsoleUserDataAction GET n USER PUBLIC
/registrar ConsoleUiAction GET n USER PUBLIC
/registrar-create ConsoleRegistrarCreatorAction POST,GET n USER PUBLIC
/registrar-ote-setup ConsoleOteSetupAction POST,GET n USER PUBLIC
/registrar-ote-status OteStatusAction POST n USER PUBLIC
/registrar-settings RegistrarSettingsAction POST n USER PUBLIC
/registry-lock-get RegistryLockGetAction GET n USER PUBLIC
/registry-lock-post RegistryLockPostAction POST n USER PUBLIC
/registry-lock-verify RegistryLockVerifyAction GET n USER PUBLIC

View File

@@ -63,6 +63,7 @@ PATH CLASS
/console-api/registrar ConsoleUpdateRegistrarAction POST n USER PUBLIC
/console-api/registrars RegistrarsAction GET,POST n USER PUBLIC
/console-api/registry-lock ConsoleRegistryLockAction GET,POST n USER PUBLIC
/console-api/registry-lock-verify ConsoleRegistryLockVerifyAction GET n USER PUBLIC
/console-api/settings/contacts ContactAction GET,POST n USER PUBLIC
/console-api/settings/security SecurityAction POST n USER PUBLIC
/console-api/settings/whois-fields WhoisRegistrarFieldsAction POST n USER PUBLIC