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

Add console backend for EPP password change (#2396)

This commit is contained in:
Pavlo Tkach
2024-04-20 06:44:26 -04:00
committed by GitHub
parent b5629ff16f
commit 4de2bd5901
7 changed files with 352 additions and 2 deletions

View File

@@ -110,6 +110,7 @@ import google.registry.tools.server.ToolsServerModule;
import google.registry.tools.server.VerifyOteAction;
import google.registry.ui.server.console.ConsoleDomainGetAction;
import google.registry.ui.server.console.ConsoleDomainListAction;
import google.registry.ui.server.console.ConsoleEppPasswordAction;
import google.registry.ui.server.console.ConsoleUserDataAction;
import google.registry.ui.server.console.RegistrarsAction;
import google.registry.ui.server.console.settings.ContactAction;
@@ -178,6 +179,8 @@ interface RequestComponent {
ConsoleDomainListAction consoleDomainListAction();
ConsoleEppPasswordAction consoleEppPasswordAction();
ConsoleOteSetupAction consoleOteSetupAction();
ConsoleRegistrarCreatorAction consoleRegistrarCreatorAction();

View File

@@ -27,6 +27,7 @@ import google.registry.request.RequestModule;
import google.registry.request.RequestScope;
import google.registry.ui.server.console.ConsoleDomainGetAction;
import google.registry.ui.server.console.ConsoleDomainListAction;
import google.registry.ui.server.console.ConsoleEppPasswordAction;
import google.registry.ui.server.console.ConsoleUserDataAction;
import google.registry.ui.server.console.RegistrarsAction;
import google.registry.ui.server.console.settings.ContactAction;
@@ -58,6 +59,8 @@ public interface FrontendRequestComponent {
ConsoleDomainListAction consoleDomainListAction();
ConsoleEppPasswordAction consoleEppPasswordAction();
ConsoleOteSetupAction consoleOteSetupAction();
ConsoleRegistrarCreatorAction consoleRegistrarCreatorAction();
ConsoleUiAction consoleUiAction();

View File

@@ -49,6 +49,7 @@ public abstract class ConsoleApiAction implements Runnable {
}
}
protected void postHandler(User user) {
throw new UnsupportedOperationException("Console API POST handler not implemented");
}
@@ -57,6 +58,11 @@ public abstract class ConsoleApiAction implements Runnable {
throw new UnsupportedOperationException("Console API GET handler not implemented");
}
protected void setFailedResponse(String message, int code) {
consoleApiParams.response().setStatus(code);
consoleApiParams.response().setPayload(message);
}
private boolean verifyXSRF() {
Optional<Cookie> maybeCookie =
Arrays.stream(consoleApiParams.request().getCookies())

View File

@@ -0,0 +1,126 @@
// 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.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.POST;
import static google.registry.request.RequestParameters.extractRequiredParameter;
import com.google.api.client.http.HttpStatusCodes;
import com.google.common.base.Throwables;
import com.google.common.flogger.FluentLogger;
import google.registry.flows.EppException.AuthenticationErrorException;
import google.registry.flows.PasswordOnlyTransportCredentials;
import google.registry.groups.GmailClient;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.request.Action;
import google.registry.request.HttpException.BadRequestException;
import google.registry.request.auth.Auth;
import google.registry.request.auth.AuthenticatedRegistrarAccessor;
import google.registry.request.auth.AuthenticatedRegistrarAccessor.RegistrarAccessDeniedException;
import google.registry.ui.server.registrar.ConsoleApiParams;
import google.registry.util.EmailMessage;
import java.util.Optional;
import javax.inject.Inject;
import javax.mail.internet.InternetAddress;
@Action(
service = Action.Service.DEFAULT,
path = ConsoleEppPasswordAction.PATH,
method = {POST},
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public class ConsoleEppPasswordAction extends ConsoleApiAction {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
protected static final String EMAIL_SUBJ = "EPP password update confirmation";
protected static final String EMAIL_BODY =
"Dear %s,\n" + "This is to confirm that your account password has been changed.";
public static final String PATH = "/console-api/eppPassword";
private final PasswordOnlyTransportCredentials credentials =
new PasswordOnlyTransportCredentials();
private final AuthenticatedRegistrarAccessor registrarAccessor;
private final GmailClient gmailClient;
@Inject
public ConsoleEppPasswordAction(
ConsoleApiParams consoleApiParams,
AuthenticatedRegistrarAccessor registrarAccessor,
GmailClient gmailClient) {
super(consoleApiParams);
this.registrarAccessor = registrarAccessor;
this.gmailClient = gmailClient;
}
@Override
protected void postHandler(User user) {
String registrarId;
String oldPassword;
String newPassword;
String newPasswordRepeat;
try {
registrarId = extractRequiredParameter(consoleApiParams.request(), "registrarId");
oldPassword = extractRequiredParameter(consoleApiParams.request(), "oldPassword");
newPassword = extractRequiredParameter(consoleApiParams.request(), "newPassword");
newPasswordRepeat = extractRequiredParameter(consoleApiParams.request(), "newPasswordRepeat");
} catch (BadRequestException e) {
setFailedResponse(e.getMessage(), HttpStatusCodes.STATUS_CODE_BAD_REQUEST);
return;
}
if (!newPassword.equals(newPasswordRepeat)) {
setFailedResponse("New password fields don't match", HttpStatusCodes.STATUS_CODE_BAD_REQUEST);
return;
}
Registrar registrar;
try {
registrar = registrarAccessor.getRegistrar(registrarId);
} catch (RegistrarAccessDeniedException e) {
setFailedResponse(e.getMessage(), HttpStatusCodes.STATUS_CODE_NOT_FOUND);
return;
}
try {
credentials.validate(registrar, oldPassword);
} catch (AuthenticationErrorException e) {
setFailedResponse(e.getMessage(), HttpStatusCodes.STATUS_CODE_FORBIDDEN);
return;
}
try {
tm().transact(
() -> {
tm().put(registrar.asBuilder().setPassword(newPassword).build());
this.gmailClient.sendEmail(
EmailMessage.create(
EMAIL_SUBJ,
String.format(EMAIL_BODY, registrar.getRegistrarName()),
new InternetAddress(registrar.getEmailAddress(), true)));
});
} catch (Throwable e) {
logger.atWarning().withCause(e).log("Failed to update password.");
String message =
Optional.ofNullable(Throwables.getRootCause(e).getMessage()).orElse("Unspecified error");
setFailedResponse(message, HttpStatusCodes.STATUS_CODE_SERVER_ERROR);
}
consoleApiParams.response().setStatus(HttpStatusCodes.STATUS_CODE_OK);
}
}

View File

@@ -0,0 +1,210 @@
// 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.request.auth.AuthenticatedRegistrarAccessor.Role.OWNER;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.persistNewRegistrar;
import static google.registry.testing.DatabaseHelper.persistResource;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.api.client.http.HttpStatusCodes;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.gson.Gson;
import google.registry.flows.PasswordOnlyTransportCredentials;
import google.registry.groups.GmailClient;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.model.registrar.Registrar;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.request.Action;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.AuthenticatedRegistrarAccessor;
import google.registry.request.auth.UserAuthInfo;
import google.registry.testing.FakeConsoleApiParams;
import google.registry.testing.FakeResponse;
import google.registry.tools.GsonUtils;
import google.registry.ui.server.registrar.ConsoleApiParams;
import google.registry.util.EmailMessage;
import jakarta.servlet.http.Cookie;
import java.util.Optional;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
class ConsoleEppPasswordActionTest {
private static final Gson GSON = GsonUtils.provideGson();
private ConsoleApiParams consoleApiParams;
protected PasswordOnlyTransportCredentials credentials = new PasswordOnlyTransportCredentials();
private FakeResponse response;
private GmailClient gmailClient = mock(GmailClient.class);
@RegisterExtension
final JpaTestExtensions.JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
@BeforeEach
void beforeEach() {
Registrar registrar = persistNewRegistrar("registrarId");
registrar =
registrar
.asBuilder()
.setType(Registrar.Type.TEST)
.setIanaIdentifier(null)
.setPassword("foobar")
.setEmailAddress("testEmail@google.com")
.build();
persistResource(registrar);
}
@Test
void testFailure_emptyParams() {
ConsoleEppPasswordAction action = createAction();
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus())
.isEqualTo(HttpStatusCodes.STATUS_CODE_BAD_REQUEST);
assertThat(((FakeResponse) consoleApiParams.response()).getPayload())
.isEqualTo("Missing parameter: registrarId");
}
@Test
void testFailure_passwordsDontMatch() {
ConsoleEppPasswordAction action = createAction();
setParams(
ImmutableMap.of(
"registrarId",
"registrarId",
"oldPassword",
"oldPassword",
"newPassword",
"newPassword",
"newPasswordRepeat",
"newPasswordRepeat"));
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus())
.isEqualTo(HttpStatusCodes.STATUS_CODE_BAD_REQUEST);
assertThat(((FakeResponse) consoleApiParams.response()).getPayload())
.contains("New password fields don't match");
}
@Test
void testFailure_existingPasswordIncorrect() {
ConsoleEppPasswordAction action = createAction();
setParams(
ImmutableMap.of(
"registrarId",
"registrarId",
"oldPassword",
"oldPassword",
"newPassword",
"randomPasword",
"newPasswordRepeat",
"randomPasword"));
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus())
.isEqualTo(HttpStatusCodes.STATUS_CODE_FORBIDDEN);
assertThat(((FakeResponse) consoleApiParams.response()).getPayload())
.contains("Registrar password is incorrect");
}
@Test
void testSuccess_sendsConfirmationEmail() throws AddressException {
ConsoleEppPasswordAction action = createAction();
setParams(
ImmutableMap.of(
"registrarId",
"registrarId",
"oldPassword",
"foobar",
"newPassword",
"randomPassword",
"newPasswordRepeat",
"randomPassword"));
action.run();
verify(gmailClient, times(1))
.sendEmail(
EmailMessage.create(
"EPP password update confirmation",
"Dear registrarId name,\n"
+ "This is to confirm that your account password has been changed.",
new InternetAddress("testEmail@google.com")));
assertThat(((FakeResponse) consoleApiParams.response()).getStatus())
.isEqualTo(HttpStatusCodes.STATUS_CODE_OK);
}
@Test
void testSuccess_passwordUpdated() throws AddressException {
ConsoleEppPasswordAction action = createAction();
setParams(
ImmutableMap.of(
"registrarId",
"registrarId",
"oldPassword",
"foobar",
"newPassword",
"randomPassword",
"newPasswordRepeat",
"randomPassword"));
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus())
.isEqualTo(HttpStatusCodes.STATUS_CODE_OK);
assertDoesNotThrow(
() -> {
credentials.validate(loadRegistrar("registrarId"), "randomPassword");
});
}
private void setParams(ImmutableMap<String, String> params) {
params.entrySet().stream()
.forEach(
entry -> {
when(consoleApiParams.request().getParameter(entry.getKey()))
.thenReturn(entry.getValue());
});
}
private ConsoleEppPasswordAction createAction() {
response = new FakeResponse();
User user =
new User.Builder()
.setEmailAddress("email@email.com")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build())
.build();
AuthResult authResult = AuthResult.createUser(UserAuthInfo.create(user));
consoleApiParams = FakeConsoleApiParams.get(Optional.of(authResult));
AuthenticatedRegistrarAccessor authenticatedRegistrarAccessor =
AuthenticatedRegistrarAccessor.createForTesting(
ImmutableSetMultimap.of("registrarId", OWNER));
Cookie cookie =
new Cookie(
consoleApiParams.xsrfTokenManager().X_CSRF_TOKEN,
consoleApiParams.xsrfTokenManager().generateToken(""));
when(consoleApiParams.request().getMethod()).thenReturn(Action.Method.POST.toString());
when(consoleApiParams.request().getCookies()).thenReturn(new Cookie[] {cookie});
return new ConsoleEppPasswordAction(
consoleApiParams, authenticatedRegistrarAccessor, gmailClient);
}
}

View File

@@ -2,6 +2,7 @@ PATH CLASS METHODS OK AUT
/_dr/epp EppTlsAction POST n API APP ADMIN
/console-api/domain ConsoleDomainGetAction GET n API,LEGACY USER PUBLIC
/console-api/domain-list ConsoleDomainListAction 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/settings/contacts ContactAction GET,POST n API,LEGACY USER PUBLIC
/console-api/settings/security SecurityAction POST n API,LEGACY USER PUBLIC
@@ -14,4 +15,4 @@ PATH CLASS METHODS OK AUT
/registrar-settings RegistrarSettingsAction POST n API,LEGACY USER PUBLIC
/registry-lock-get RegistryLockGetAction GET n API,LEGACY USER PUBLIC
/registry-lock-post RegistryLockPostAction POST n API,LEGACY USER PUBLIC
/registry-lock-verify RegistryLockVerifyAction GET n API,LEGACY NONE PUBLIC
/registry-lock-verify RegistryLockVerifyAction GET n API,LEGACY NONE PUBLIC

View File

@@ -57,6 +57,7 @@ PATH CLASS
/check CheckApiAction GET n API NONE PUBLIC
/console-api/domain ConsoleDomainGetAction GET n API,LEGACY USER PUBLIC
/console-api/domain-list ConsoleDomainListAction 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/settings/contacts ContactAction GET,POST n API,LEGACY USER PUBLIC
/console-api/settings/security SecurityAction POST n API,LEGACY USER PUBLIC
@@ -79,4 +80,4 @@ PATH CLASS
/registry-lock-get RegistryLockGetAction GET n API,LEGACY USER PUBLIC
/registry-lock-post RegistryLockPostAction POST n API,LEGACY USER PUBLIC
/registry-lock-verify RegistryLockVerifyAction GET n API,LEGACY NONE PUBLIC
/whois/(*) WhoisHttpAction GET n API NONE PUBLIC
/whois/(*) WhoisHttpAction GET n API NONE PUBLIC