1
0
mirror of https://github.com/google/nomulus synced 2025-12-23 06:15:42 +00:00

Add a separate RegistryLock action for the console (#2411)

This handles both GET and POST requests. For POST requests it doesn't
actually change anything about the domains because we will need to add a
verification action (this will be done in a future PR).
This commit is contained in:
gbrodman
2024-05-03 18:37:22 -04:00
committed by GitHub
parent 5511b41f93
commit 49d2e34e12
10 changed files with 795 additions and 18 deletions

View File

@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.util.DateTimeUtils.isBeforeOrAt;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.gson.annotations.Expose;
import google.registry.model.Buildable;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.UpdateAutoTimestampEntity;
@@ -90,6 +91,7 @@ public final class RegistryLock extends UpdateAutoTimestampEntity implements Bui
// TODO (b/140568328): remove this when everything is in Cloud SQL and we can join on "domain"
@Column(nullable = false)
@Expose
private String domainName;
/**
@@ -100,7 +102,7 @@ public final class RegistryLock extends UpdateAutoTimestampEntity implements Bui
private String registrarId;
/** The POC that performed the action, or null if it was a superuser. */
private String registrarPocId;
@Expose private String registrarPocId;
/** When the lock is first requested. */
@AttributeOverrides({
@@ -108,22 +110,23 @@ public final class RegistryLock extends UpdateAutoTimestampEntity implements Bui
name = "creationTime",
column = @Column(name = "lockRequestTime", nullable = false))
})
@Expose
private final CreateAutoTimestamp lockRequestTime = CreateAutoTimestamp.create(null);
/** When the unlock is first requested. */
private DateTime unlockRequestTime;
@Expose private DateTime unlockRequestTime;
/**
* When the user has verified the lock. If this field is null, it means the lock has not been
* verified yet (and thus not been put into effect).
*/
private DateTime lockCompletionTime;
@Expose private DateTime lockCompletionTime;
/**
* When the user has verified the unlock of this lock. If this field is null, it means the unlock
* action has not been verified yet (and has not been put into effect).
*/
private DateTime unlockCompletionTime;
@Expose private DateTime unlockCompletionTime;
/** The user must provide the random verification code in order to complete the action. */
@Column(nullable = false)
@@ -134,6 +137,7 @@ public final class RegistryLock extends UpdateAutoTimestampEntity implements Bui
* this case, the action was performed by a registry admin rather than a registrar.
*/
@Column(nullable = false)
@Expose
private boolean isSuperuser;
/** The lock that undoes this lock, if this lock has been unlocked and the domain locked again. */

View File

@@ -112,6 +112,7 @@ import google.registry.ui.server.console.ConsoleDomainGetAction;
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.ConsoleUserDataAction;
import google.registry.ui.server.console.RegistrarsAction;
import google.registry.ui.server.console.settings.ContactAction;
@@ -186,6 +187,8 @@ interface RequestComponent {
ConsoleRegistrarCreatorAction consoleRegistrarCreatorAction();
ConsoleRegistryLockAction consoleRegistryLockAction();
ConsoleUiAction consoleUiAction();
ConsoleUserDataAction consoleUserDataAction();

View File

@@ -29,6 +29,7 @@ import google.registry.ui.server.console.ConsoleDomainGetAction;
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.ConsoleUserDataAction;
import google.registry.ui.server.console.RegistrarsAction;
import google.registry.ui.server.console.settings.ContactAction;
@@ -64,6 +65,9 @@ public interface FrontendRequestComponent {
ConsoleOteSetupAction consoleOteSetupAction();
ConsoleRegistrarCreatorAction consoleRegistrarCreatorAction();
ConsoleRegistryLockAction consoleRegistryLockAction();
ConsoleUiAction consoleUiAction();
ConsoleUserDataAction consoleUserDataAction();

View File

@@ -106,6 +106,20 @@ public final class RequestParameters {
}
}
/**
* Returns first GET or POST parameter associated with {@code name} as a long.
*
* @throws BadRequestException if request parameter is present but not a valid long
*/
public static Optional<Long> extractOptionalLongParameter(HttpServletRequest req, String name) {
String stringParam = req.getParameter(name);
try {
return isNullOrEmpty(stringParam) ? Optional.empty() : Optional.of(Long.valueOf(stringParam));
} catch (NumberFormatException e) {
throw new BadRequestException("Expected long: " + name);
}
}
/**
* Returns first GET or POST parameter associated with {@code name} as a long.
*

View File

@@ -0,0 +1,215 @@
// 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.collect.ImmutableList.toImmutableList;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.POST;
import static google.registry.request.RequestParameters.extractBooleanParameter;
import static google.registry.request.RequestParameters.extractOptionalLongParameter;
import static google.registry.request.RequestParameters.extractOptionalParameter;
import static google.registry.request.RequestParameters.extractRequiredParameter;
import static google.registry.ui.server.registrar.RegistryLockPostAction.VERIFICATION_EMAIL_TEMPLATE;
import com.google.api.client.http.HttpStatusCodes;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import com.google.gson.Gson;
import google.registry.flows.EppException;
import google.registry.flows.domain.DomainFlowUtils;
import google.registry.groups.GmailClient;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.User;
import google.registry.model.domain.RegistryLock;
import google.registry.model.registrar.Registrar;
import google.registry.model.tld.RegistryLockDao;
import google.registry.request.Action;
import google.registry.request.HttpException;
import google.registry.request.Parameter;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.tools.DomainLockUtils;
import google.registry.ui.server.registrar.ConsoleApiParams;
import google.registry.util.EmailMessage;
import jakarta.servlet.http.HttpServletRequest;
import java.net.URISyntaxException;
import java.util.Optional;
import javax.inject.Inject;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import org.apache.http.client.utils.URIBuilder;
import org.joda.time.Duration;
/**
* Handler for retrieving / creating registry lock requests in the console.
*
* <p>Note: two-factor verification of the locks occurs separately (TODO: link the verification
* action).
*/
@Action(
service = Action.Service.DEFAULT,
path = ConsoleRegistryLockAction.PATH,
method = {GET, POST},
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public class ConsoleRegistryLockAction extends ConsoleApiAction {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
static final String PATH = "/console-api/registry-lock";
private final DomainLockUtils domainLockUtils;
private final GmailClient gmailClient;
private final Gson gson;
private final String registrarId;
@Inject
public ConsoleRegistryLockAction(
ConsoleApiParams consoleApiParams,
DomainLockUtils domainLockUtils,
GmailClient gmailClient,
Gson gson,
@Parameter("registrarId") String registrarId) {
super(consoleApiParams);
this.domainLockUtils = domainLockUtils;
this.gmailClient = gmailClient;
this.gson = gson;
this.registrarId = registrarId;
}
@Override
protected void getHandler(User user) {
if (!user.getUserRoles().hasPermission(registrarId, ConsolePermission.REGISTRY_LOCK)) {
consoleApiParams.response().setStatus(HttpStatusCodes.STATUS_CODE_FORBIDDEN);
return;
}
consoleApiParams.response().setPayload(gson.toJson(getLockedDomains()));
consoleApiParams.response().setStatus(HttpStatusCodes.STATUS_CODE_OK);
}
@Override
protected void postHandler(User user) {
HttpServletRequest req = consoleApiParams.request();
Response response = consoleApiParams.response();
// User must have the proper permission on the registrar
if (!user.getUserRoles().hasPermission(registrarId, ConsolePermission.REGISTRY_LOCK)) {
setFailedResponse("", HttpStatusCodes.STATUS_CODE_FORBIDDEN);
return;
}
// Shouldn't happen, but double-check the registrar has registry lock enabled
Registrar registrar = Registrar.loadByRegistrarIdCached(registrarId).get();
if (!registrar.isRegistryLockAllowed()) {
setFailedResponse(
String.format("Registry lock not allowed for registrar %s", registrarId),
HttpStatusCodes.STATUS_CODE_BAD_REQUEST);
return;
}
// Retrieve and validate the necessary params
String domainName;
boolean isLock;
Optional<String> maybePassword;
Optional<Long> relockDurationMillis;
try {
domainName = extractRequiredParameter(req, "domainName");
isLock = extractBooleanParameter(req, "isLock");
maybePassword = extractOptionalParameter(req, "password");
relockDurationMillis = extractOptionalLongParameter(req, "relockDurationMillis");
DomainFlowUtils.validateDomainName(domainName);
} catch (HttpException.BadRequestException | EppException e) {
logger.atWarning().withCause(e).log("Bad request when attempting registry lock/unlock");
setFailedResponse(e.getMessage(), HttpStatusCodes.STATUS_CODE_BAD_REQUEST);
return;
}
// Passwords aren't required for admin users, otherwise we need to validate it
boolean isAdmin = user.getUserRoles().isAdmin();
if (!isAdmin) {
if (maybePassword.isEmpty()) {
setFailedResponse("No password provided", HttpStatusCodes.STATUS_CODE_BAD_REQUEST);
return;
}
if (!user.verifyRegistryLockPassword(maybePassword.get())) {
setFailedResponse(
"Incorrect registry lock password", HttpStatusCodes.STATUS_CODE_UNAUTHORIZED);
return;
}
}
String userEmail = user.getEmailAddress();
try {
tm().transact(
() -> {
RegistryLock registryLock =
isLock
? domainLockUtils.saveNewRegistryLockRequest(
domainName, registrarId, userEmail, isAdmin)
: domainLockUtils.saveNewRegistryUnlockRequest(
domainName,
registrarId,
isAdmin,
relockDurationMillis.map(Duration::new));
sendVerificationEmail(registryLock, userEmail, isLock);
});
} catch (IllegalArgumentException e) {
// Catch IllegalArgumentExceptions separately to give a nicer error message and code
logger.atWarning().withCause(e).log("Failed to lock/unlock domain");
setFailedResponse(e.getMessage(), HttpStatusCodes.STATUS_CODE_BAD_REQUEST);
return;
} catch (Throwable t) {
logger.atWarning().withCause(t).log("Failed to lock/unlock domain");
setFailedResponse("Internal server error", HttpStatusCodes.STATUS_CODE_SERVER_ERROR);
return;
}
response.setStatus(HttpStatusCodes.STATUS_CODE_OK);
}
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())
.setParameter("isLock", String.valueOf(isLock))
.build()
.toString();
String body = String.format(VERIFICATION_EMAIL_TEMPLATE, lock.getDomainName(), url);
ImmutableList<InternetAddress> recipients =
ImmutableList.of(new InternetAddress(userEmail, true));
String action = isLock ? "lock" : "unlock";
gmailClient.sendEmail(
EmailMessage.newBuilder()
.setBody(body)
.setSubject(String.format("Registry %s verification", action))
.setRecipients(recipients)
.build());
} catch (AddressException | URISyntaxException e) {
throw new RuntimeException(e); // caught above -- this is so we can run in a transaction
}
}
private ImmutableList<RegistryLock> getLockedDomains() {
return tm().transact(
() ->
RegistryLockDao.getLocksByRegistrarId(registrarId).stream()
.filter(lock -> !lock.isLockRequestExpired(tm().getTransactionTime()))
.collect(toImmutableList()));
}
}

View File

@@ -70,17 +70,16 @@ import org.joda.time.Duration;
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public class RegistryLockPostAction implements Runnable, JsonActionRunner.JsonAction {
public static final String PATH = "/registry-lock-post";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final Gson GSON = new Gson();
private static final String VERIFICATION_EMAIL_TEMPLATE =
public static final String VERIFICATION_EMAIL_TEMPLATE =
"""
Please click the link below to perform the lock / unlock action on domain %s. Note: this\
code will expire in one hour.
%s""";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final Gson GSON = new Gson();
private final HttpServletRequest req;
private final JsonActionRunner jsonActionRunner;
private final AuthResult authResult;

View File

@@ -0,0 +1,539 @@
// 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.loadRegistrar;
import static google.registry.testing.DatabaseHelper.newDomain;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.SqlHelper.getMostRecentRegistryLockByRepoId;
import static google.registry.testing.SqlHelper.saveRegistryLock;
import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STATUSES;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import com.google.api.client.http.HttpStatusCodes;
import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import google.registry.groups.GmailClient;
import google.registry.model.console.GlobalRole;
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.request.auth.UserAuthInfo;
import google.registry.testing.CloudTasksHelper;
import google.registry.testing.DeterministicStringGenerator;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeConsoleApiParams;
import google.registry.testing.FakeResponse;
import google.registry.tools.DomainLockUtils;
import google.registry.ui.server.registrar.ConsoleApiParams;
import google.registry.util.EmailMessage;
import google.registry.util.StringGenerator;
import java.io.IOException;
import java.util.Optional;
import javax.mail.internet.InternetAddress;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
/** Tests for {@link ConsoleRegistryLockAction}. */
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
public class ConsoleRegistryLockActionTest {
private static final String EMAIL_MESSAGE_TEMPLATE =
"Please click the link below to perform the lock \\/ unlock action on domain example.test."
+ " Note: this code will expire in one hour.\n\n"
+ "https:\\/\\/registrarconsole.tld\\/console-api\\/registry-lock-verify\\?lockVerificationCode="
+ "[0-9a-zA-Z_\\-]+&isLock=(true|false)";
private static final Gson GSON = RequestModule.provideGson();
private final FakeClock fakeClock = new FakeClock(DateTime.parse("2024-04-18T12:00:00.000Z"));
@RegisterExtension
final JpaTestExtensions.JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().withClock(fakeClock).buildIntegrationTestExtension();
@Mock GmailClient gmailClient;
private ConsoleRegistryLockAction action;
private Domain defaultDomain;
private FakeResponse response;
private User user;
@BeforeEach
void beforeEach() throws Exception {
createTld("test");
defaultDomain = persistActiveDomain("example.test");
user =
new User.Builder()
.setEmailAddress("user@theregistrar.com")
.setUserRoles(
new UserRoles.Builder()
.setRegistrarRoles(
ImmutableMap.of("TheRegistrar", RegistrarRole.PRIMARY_CONTACT))
.build())
.setRegistryLockPassword("registryLockPassword")
.build();
action = createGetAction();
}
@AfterEach
void afterEach() {
verifyNoMoreInteractions(gmailClient);
}
@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());
action.run();
assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_OK);
assertThat(response.getPayload())
.isEqualTo(
"""
[{"domainName":"example.test","registrarPocId":"johndoe@theregistrar.com","lockRequestTime":\
{"creationTime":"2024-04-18T12:00:00.000Z"},"unlockRequestTime":"null","lockCompletionTime":\
"2024-04-18T12:00:00.000Z","unlockCompletionTime":"null","isSuperuser":false}]\
""");
}
@Test
void testGet_allCurrentlyValidLocks() {
RegistryLock expiredLock =
new RegistryLock.Builder()
.setRepoId("repoId")
.setDomainName("expired.test")
.setRegistrarId("TheRegistrar")
.setVerificationCode("123456789ABCDEFGHJKLMNPQRSTUVWXY")
.setRegistrarPocId("johndoe@theregistrar.com")
.build();
saveRegistryLock(expiredLock);
RegistryLock expiredUnlock =
new RegistryLock.Builder()
.setRepoId("repoId")
.setDomainName("expiredunlock.test")
.setRegistrarId("TheRegistrar")
.setVerificationCode("123456789ABCDEFGHJKLMNPQRSTUVWXY")
.setRegistrarPocId("johndoe@theregistrar.com")
.setLockCompletionTime(fakeClock.nowUtc())
.setUnlockRequestTime(fakeClock.nowUtc())
.build();
saveRegistryLock(expiredUnlock);
fakeClock.advanceBy(Duration.standardDays(1));
RegistryLock regularLock =
new RegistryLock.Builder()
.setRepoId("repoId")
.setDomainName("example.test")
.setRegistrarId("TheRegistrar")
.setVerificationCode("123456789ABCDEFGHJKLMNPQRSTUVWXY")
.setRegistrarPocId("johndoe@theregistrar.com")
.setLockCompletionTime(fakeClock.nowUtc())
.build();
fakeClock.advanceOneMilli();
RegistryLock adminLock =
new RegistryLock.Builder()
.setRepoId("repoId")
.setDomainName("adminexample.test")
.setRegistrarId("TheRegistrar")
.setVerificationCode("122222222ABCDEFGHJKLMNPQRSTUVWXY")
.isSuperuser(true)
.setLockCompletionTime(fakeClock.nowUtc())
.build();
RegistryLock incompleteLock =
new RegistryLock.Builder()
.setRepoId("repoId")
.setDomainName("pending.test")
.setRegistrarId("TheRegistrar")
.setVerificationCode("111111111ABCDEFGHJKLMNPQRSTUVWXY")
.setRegistrarPocId("johndoe@theregistrar.com")
.build();
RegistryLock incompleteUnlock =
new RegistryLock.Builder()
.setRepoId("repoId")
.setDomainName("incompleteunlock.test")
.setRegistrarId("TheRegistrar")
.setVerificationCode("123456789ABCDEFGHJKLMNPQRSTUVWXY")
.setRegistrarPocId("johndoe@theregistrar.com")
.setLockCompletionTime(fakeClock.nowUtc())
.setUnlockRequestTime(fakeClock.nowUtc())
.build();
RegistryLock unlockedLock =
new RegistryLock.Builder()
.setRepoId("repoId")
.setDomainName("unlocked.test")
.setRegistrarId("TheRegistrar")
.setRegistrarPocId("johndoe@theregistrar.com")
.setVerificationCode("123456789ABCDEFGHJKLMNPQRSTUUUUU")
.setLockCompletionTime(fakeClock.nowUtc())
.setUnlockRequestTime(fakeClock.nowUtc())
.setUnlockCompletionTime(fakeClock.nowUtc())
.build();
saveRegistryLock(regularLock);
saveRegistryLock(adminLock);
saveRegistryLock(incompleteLock);
saveRegistryLock(incompleteUnlock);
saveRegistryLock(unlockedLock);
action.run();
assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_OK);
// We should include all the locks that are currently locked, which does not include pending
// locks or completed unlocks
assertThat(response.getPayload())
.isEqualTo(
"""
[{"domainName":"adminexample.test","lockRequestTime":{"creationTime":"2024-04-19T12:00:00.001Z"},\
"unlockRequestTime":"null","lockCompletionTime":"2024-04-19T12:00:00.001Z","unlockCompletionTime":\
"null","isSuperuser":true},\
\
{"domainName":"example.test","registrarPocId":"johndoe@theregistrar.com","lockRequestTime":\
{"creationTime":"2024-04-19T12:00:00.001Z"},"unlockRequestTime":"null","lockCompletionTime":\
"2024-04-19T12:00:00.000Z","unlockCompletionTime":"null","isSuperuser":false},\
\
{"domainName":"expiredunlock.test","registrarPocId":"johndoe@theregistrar.com","lockRequestTime":\
{"creationTime":"2024-04-18T12:00:00.000Z"},"unlockRequestTime":"2024-04-18T12:00:00.000Z",\
"lockCompletionTime":"2024-04-18T12:00:00.000Z","unlockCompletionTime":"null","isSuperuser":false},\
\
{"domainName":"incompleteunlock.test","registrarPocId":"johndoe@theregistrar.com","lockRequestTime":\
{"creationTime":"2024-04-19T12:00:00.001Z"},"unlockRequestTime":"2024-04-19T12:00:00.001Z",\
"lockCompletionTime":"2024-04-19T12:00:00.001Z","unlockCompletionTime":"null","isSuperuser":false},\
\
{"domainName":"pending.test","registrarPocId":"johndoe@theregistrar.com","lockRequestTime":\
{"creationTime":"2024-04-19T12:00:00.001Z"},"unlockRequestTime":"null","lockCompletionTime":"null",\
"unlockCompletionTime":"null","isSuperuser":false}]""");
}
@Test
void testGet_noLocks() {
action.run();
assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_OK);
assertThat(response.getPayload()).isEqualTo("[]");
}
@Test
void testGet_failure_noRegistrarAccess() throws Exception {
user =
user.asBuilder()
.setUserRoles(
user.getUserRoles().asBuilder().setRegistrarRoles(ImmutableMap.of()).build())
.build();
action = createGetAction();
action.run();
assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_FORBIDDEN);
}
@Test
void testGet_failure_noRegistryLockAccess() throws Exception {
// User has access to the registrar, but not to do locks
user =
user.asBuilder()
.setUserRoles(
user.getUserRoles()
.asBuilder()
.setRegistrarRoles(
ImmutableMap.of("TheRegistrar", RegistrarRole.ACCOUNT_MANAGER))
.build())
.build();
action = createGetAction();
action.run();
assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_FORBIDDEN);
}
@Test
void testPost_lock() throws Exception {
action = createDefaultPostAction(true);
action.run();
assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_OK);
assertThat(getMostRecentRegistryLockByRepoId(defaultDomain.getRepoId())).isPresent();
verifyEmail();
// Doesn't actually change the status values (hasn't been verified)
assertThat(loadByEntity(defaultDomain).getStatusValues()).containsExactly(StatusValue.INACTIVE);
}
@Test
void testPost_unlock() throws Exception {
saveRegistryLock(createDefaultLockBuilder().setLockCompletionTime(fakeClock.nowUtc()).build());
persistResource(defaultDomain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build());
action = createDefaultPostAction(false);
action.run();
assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_OK);
verifyEmail();
// Doesn't actually change the status values (hasn't been verified)
assertThat(loadByEntity(defaultDomain).getStatusValues())
.containsAtLeastElementsIn(REGISTRY_LOCK_STATUSES);
}
@Test
void testPost_unlock_relockDuration() throws Exception {
saveRegistryLock(createDefaultLockBuilder().setLockCompletionTime(fakeClock.nowUtc()).build());
persistResource(defaultDomain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build());
action =
createPostAction(
"example.test",
false,
"registryLockPassword",
Optional.of(Duration.standardDays(1).getMillis()));
action.run();
assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_OK);
verifyEmail();
RegistryLock savedUnlockRequest =
getMostRecentRegistryLockByRepoId(defaultDomain.getRepoId()).get();
assertThat(savedUnlockRequest.getRelockDuration())
.isEqualTo(Optional.of(Duration.standardDays(1)));
}
@Test
void testPost_adminUnlockingAdmin() throws Exception {
saveRegistryLock(
createDefaultLockBuilder()
.setLockCompletionTime(fakeClock.nowUtc())
.isSuperuser(true)
.build());
persistResource(defaultDomain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build());
user =
user.asBuilder()
.setUserRoles(
new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).setIsAdmin(true).build())
.build();
action = createDefaultPostAction(false);
action.run();
assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_OK);
verifyEmail();
}
@Test
void testPost_success_noPasswordForAdmin() throws Exception {
user =
user.asBuilder()
.setUserRoles(
new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).setIsAdmin(true).build())
.build();
action = createPostAction("example.test", true, "", Optional.empty());
action.run();
assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_OK);
verifyEmail();
}
@Test
void testPost_failure_noRegistrarAccess() throws Exception {
user =
user.asBuilder()
.setUserRoles(
user.getUserRoles().asBuilder().setRegistrarRoles(ImmutableMap.of()).build())
.build();
action = createDefaultPostAction(true);
action.run();
assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_FORBIDDEN);
}
@Test
void testPost_failure_noRegistryLockAccess() throws Exception {
// User has access to the registrar, but not to do locks
user =
user.asBuilder()
.setUserRoles(
user.getUserRoles()
.asBuilder()
.setRegistrarRoles(
ImmutableMap.of("TheRegistrar", RegistrarRole.ACCOUNT_MANAGER))
.build())
.build();
action = createDefaultPostAction(true);
action.run();
assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_FORBIDDEN);
}
@Test
void testPost_failure_unlock_noLock() throws Exception {
action = createDefaultPostAction(false);
action.run();
assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_BAD_REQUEST);
assertThat(response.getPayload()).isEqualTo("Domain example.test is already unlocked");
}
@Test
void testPost_failure_nonAdminUnlockingAdmin() throws Exception {
saveRegistryLock(
createDefaultLockBuilder()
.setLockCompletionTime(fakeClock.nowUtc())
.isSuperuser(true)
.build());
persistResource(defaultDomain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build());
action = createDefaultPostAction(false);
action.run();
assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_BAD_REQUEST);
assertThat(response.getPayload())
.isEqualTo("Non-admin user cannot unlock admin-locked domain example.test");
}
@Test
void testPost_failure_wrongRegistrarForDomain() throws Exception {
persistResource(
newDomain("otherregistrar.test")
.asBuilder()
.setCreationRegistrarId("NewRegistrar")
.setPersistedCurrentSponsorRegistrarId("NewRegistrar")
.build());
action =
createPostAction("otherregistrar.test", true, "registryLockPassword", Optional.empty());
action.run();
assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_BAD_REQUEST);
assertThat(response.getPayload())
.isEqualTo("Domain otherregistrar.test is not owned by registrar TheRegistrar");
}
@Test
void testPost_failure_notAllowedForRegistrar() throws Exception {
persistResource(
loadRegistrar("TheRegistrar").asBuilder().setRegistryLockAllowed(false).build());
action = createDefaultPostAction(true);
action.run();
assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_BAD_REQUEST);
assertThat(response.getPayload())
.isEqualTo("Registry lock not allowed for registrar TheRegistrar");
}
@Test
void testPost_failure_badPassword() throws Exception {
action = createPostAction("example.test", true, "badPassword", Optional.empty());
action.run();
assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED);
}
@Test
void testPost_failure_lock_alreadyPendingLock() throws Exception {
saveRegistryLock(createDefaultLockBuilder().build());
action = createDefaultPostAction(true);
action.run();
assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_BAD_REQUEST);
assertThat(response.getPayload())
.isEqualTo("A pending or completed lock action already exists for example.test");
}
@Test
void testPost_failure_alreadyLocked() throws Exception {
persistResource(defaultDomain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build());
action = createDefaultPostAction(true);
action.run();
assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_BAD_REQUEST);
assertThat(response.getPayload()).isEqualTo("Domain example.test is already locked");
}
@Test
void testPost_failure_alreadyUnlocked() throws Exception {
saveRegistryLock(
createDefaultLockBuilder()
.setLockCompletionTime(fakeClock.nowUtc())
.setUnlockRequestTime(fakeClock.nowUtc())
.setUnlockCompletionTime(fakeClock.nowUtc())
.build());
action = createDefaultPostAction(false);
action.run();
assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_BAD_REQUEST);
assertThat(response.getPayload()).isEqualTo("Domain example.test is already unlocked");
}
private ConsoleRegistryLockAction createDefaultPostAction(boolean isLock) {
return createPostAction("example.test", isLock, "registryLockPassword", Optional.empty());
}
private ConsoleRegistryLockAction createPostAction(
String domainName, boolean isLock, String password, Optional<Long> relockDurationMillis) {
ConsoleApiParams params = createParams();
when(params.request().getParameter("domainName")).thenReturn(domainName);
when(params.request().getParameterMap())
.thenReturn(ImmutableMap.of("isLock", new String[] {String.valueOf(isLock)}));
when(params.request().getParameter("isLock")).thenReturn(String.valueOf(isLock));
when(params.request().getParameter("password")).thenReturn(password);
relockDurationMillis.ifPresent(
duration ->
when(params.request().getParameter("relockDurationMillis"))
.thenReturn(String.valueOf(duration)));
return createGenericAction(params, "POST");
}
private ConsoleRegistryLockAction createGetAction() throws IOException {
return createGenericAction(createParams(), "GET");
}
private ConsoleRegistryLockAction createGenericAction(ConsoleApiParams params, String method) {
when(params.request().getMethod()).thenReturn(method);
when(params.request().getServerName()).thenReturn("registrarconsole.tld");
when(params.request().getParameter("registrarId")).thenReturn("TheRegistrar");
DomainLockUtils domainLockUtils =
new DomainLockUtils(
new DeterministicStringGenerator(StringGenerator.Alphabets.BASE_58),
"adminreg",
new CloudTasksHelper(fakeClock).getTestCloudTasksUtils());
response = (FakeResponse) params.response();
return new ConsoleRegistryLockAction(
params, domainLockUtils, gmailClient, GSON, "TheRegistrar");
}
private ConsoleApiParams createParams() {
AuthResult authResult = AuthResult.createUser(UserAuthInfo.create(user));
return FakeConsoleApiParams.get(Optional.of(authResult));
}
private RegistryLock.Builder createDefaultLockBuilder() {
return new RegistryLock.Builder()
.setRepoId(defaultDomain.getRepoId())
.setDomainName(defaultDomain.getDomainName())
.setRegistrarId(defaultDomain.getCurrentSponsorRegistrarId())
.setRegistrarPocId("johndoe@theregistrar.com")
.setVerificationCode("123456789ABCDEFGHJKLMNPQRSTUUUUU");
}
private void verifyEmail() throws Exception {
ArgumentCaptor<EmailMessage> emailCaptor = ArgumentCaptor.forClass(EmailMessage.class);
verify(gmailClient).sendEmail(emailCaptor.capture());
EmailMessage sentMessage = emailCaptor.getValue();
assertThat(sentMessage.subject()).matches("Registry (un)?lock verification");
assertThat(sentMessage.body()).matches(EMAIL_MESSAGE_TEMPLATE);
assertThat(sentMessage.recipients())
.containsExactly(new InternetAddress("user@theregistrar.com"));
}
}

View File

@@ -22,7 +22,6 @@ import com.google.api.client.http.HttpStatusCodes;
import com.google.gson.Gson;
import google.registry.model.console.User;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.request.Action;
import google.registry.request.RequestModule;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.UserAuthInfo;
@@ -54,8 +53,7 @@ class ConsoleUserDataActionTest {
User user = DatabaseHelper.createAdminUser("email@email.com");
AuthResult authResult = AuthResult.createUser(UserAuthInfo.create(user));
ConsoleUserDataAction action =
createAction(
Optional.of(FakeConsoleApiParams.get(Optional.of(authResult))), Action.Method.GET);
createAction(Optional.of(FakeConsoleApiParams.get(Optional.of(authResult))));
action.run();
List<Cookie> cookies = ((FakeResponse) consoleApiParams.response()).getCookies();
assertThat(cookies.stream().map(cookie -> cookie.getName()).collect(toImmutableList()))
@@ -67,8 +65,7 @@ class ConsoleUserDataActionTest {
User user = DatabaseHelper.createAdminUser("email@email.com");
AuthResult authResult = AuthResult.createUser(UserAuthInfo.create(user));
ConsoleUserDataAction action =
createAction(
Optional.of(FakeConsoleApiParams.get(Optional.of(authResult))), Action.Method.GET);
createAction(Optional.of(FakeConsoleApiParams.get(Optional.of(authResult))));
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus())
.isEqualTo(HttpStatusCodes.STATUS_CODE_OK);
@@ -92,17 +89,17 @@ class ConsoleUserDataActionTest {
@Test
void testFailure_notAConsoleUser() throws IOException {
ConsoleUserDataAction action = createAction(Optional.empty(), Action.Method.GET);
ConsoleUserDataAction action = createAction(Optional.empty());
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus())
.isEqualTo(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED);
}
private ConsoleUserDataAction createAction(
Optional<ConsoleApiParams> maybeConsoleApiParams, Action.Method method) throws IOException {
private ConsoleUserDataAction createAction(Optional<ConsoleApiParams> maybeConsoleApiParams)
throws IOException {
consoleApiParams =
maybeConsoleApiParams.orElseGet(() -> FakeConsoleApiParams.get(Optional.empty()));
when(consoleApiParams.request().getMethod()).thenReturn(method.toString());
when(consoleApiParams.request().getMethod()).thenReturn("GET");
return new ConsoleUserDataAction(
consoleApiParams, "Nomulus", "support@example.com", "+1 (212) 867 5309", "test");
}

View File

@@ -5,6 +5,7 @@ PATH CLASS METHODS OK AUT
/console-api/dum-download ConsoleDumDownloadAction GET n API,LEGACY USER PUBLIC
/console-api/eppPassword ConsoleEppPasswordAction POST n API,LEGACY USER PUBLIC
/console-api/registrars RegistrarsAction GET,POST n API,LEGACY USER PUBLIC
/console-api/registry-lock ConsoleRegistryLockAction GET,POST n API,LEGACY USER PUBLIC
/console-api/settings/contacts ContactAction GET,POST n API,LEGACY USER PUBLIC
/console-api/settings/security SecurityAction POST n API,LEGACY USER PUBLIC
/console-api/settings/whois-fields WhoisRegistrarFieldsAction POST n API,LEGACY USER PUBLIC

View File

@@ -60,6 +60,7 @@ PATH CLASS
/console-api/dum-download ConsoleDumDownloadAction GET n API,LEGACY USER PUBLIC
/console-api/eppPassword ConsoleEppPasswordAction POST n API,LEGACY USER PUBLIC
/console-api/registrars RegistrarsAction GET,POST n API,LEGACY USER PUBLIC
/console-api/registry-lock ConsoleRegistryLockAction GET,POST n API,LEGACY USER PUBLIC
/console-api/settings/contacts ContactAction GET,POST n API,LEGACY USER PUBLIC
/console-api/settings/security SecurityAction POST n API,LEGACY USER PUBLIC
/console-api/settings/whois-fields WhoisRegistrarFieldsAction POST n API,LEGACY USER PUBLIC