From e2e9d4cfc7a7d72489e6d457b7409c652d056c61 Mon Sep 17 00:00:00 2001 From: Pavlo Tkach <3469726+ptkach@users.noreply.github.com> Date: Fri, 18 Jul 2025 14:46:21 -0400 Subject: [PATCH] Add console history api (#2782) --- .../model/console/ConsolePermission.java | 2 + .../model/console/ConsoleRoleDefinitions.java | 3 + .../model/console/ConsoleUpdateHistory.java | 14 +- .../registry/module/RequestComponent.java | 3 + .../console/ConsoleDumDownloadAction.java | 8 + .../console/ConsoleHistoryDataAction.java | 116 +++++++++++++ .../ConsoleRegistryLockVerifyAction.java | 17 ++ .../ui/server/console/ConsoleUsersAction.java | 22 +++ .../console/settings/ContactAction.java | 14 ++ .../settings/RdapRegistrarFieldsAction.java | 37 +++-- .../console/settings/SecurityAction.java | 20 ++- .../registry/module/RequestComponentTest.java | 2 + .../console/ConsoleHistoryDataActionTest.java | 153 ++++++++++++++++++ .../ConsoleRegistryLockVerifyActionTest.java | 21 +-- .../RdapRegistrarFieldsActionTest.java | 2 +- .../console/settings/SecurityActionTest.java | 2 +- .../google/registry/module/routing.txt | 1 + .../sql/schema/db-schema.sql.generated | 2 +- 18 files changed, 410 insertions(+), 29 deletions(-) create mode 100644 core/src/main/java/google/registry/ui/server/console/ConsoleHistoryDataAction.java create mode 100644 core/src/test/java/google/registry/ui/server/console/ConsoleHistoryDataActionTest.java diff --git a/core/src/main/java/google/registry/model/console/ConsolePermission.java b/core/src/main/java/google/registry/model/console/ConsolePermission.java index 16a472887..bfff63cb0 100644 --- a/core/src/main/java/google/registry/model/console/ConsolePermission.java +++ b/core/src/main/java/google/registry/model/console/ConsolePermission.java @@ -16,6 +16,8 @@ package google.registry.model.console; /** Permissions that users may have in the UI, either per-registrar or globally. */ public enum ConsolePermission { + AUDIT_ACTIVITY_BY_USER, + AUDIT_ACTIVITY_BY_REGISTRAR, /** View basic information about a registrar. */ VIEW_REGISTRAR_DETAILS, /** Edit basic information about a registrar. */ diff --git a/core/src/main/java/google/registry/model/console/ConsoleRoleDefinitions.java b/core/src/main/java/google/registry/model/console/ConsoleRoleDefinitions.java index e2c23b0ce..34a3da5d1 100644 --- a/core/src/main/java/google/registry/model/console/ConsoleRoleDefinitions.java +++ b/core/src/main/java/google/registry/model/console/ConsoleRoleDefinitions.java @@ -55,6 +55,8 @@ public class ConsoleRoleDefinitions { new ImmutableSet.Builder() .addAll(SUPPORT_AGENT_PERMISSIONS) .add( + ConsolePermission.AUDIT_ACTIVITY_BY_USER, + ConsolePermission.AUDIT_ACTIVITY_BY_REGISTRAR, ConsolePermission.MANAGE_REGISTRARS, ConsolePermission.GET_REGISTRANT_EMAIL, ConsolePermission.SUSPEND_DOMAIN, @@ -111,6 +113,7 @@ public class ConsoleRoleDefinitions { new ImmutableSet.Builder() .addAll(TECH_CONTACT_PERMISSIONS) .add(ConsolePermission.MANAGE_USERS) + .add(ConsolePermission.AUDIT_ACTIVITY_BY_REGISTRAR) .build(); private ConsoleRoleDefinitions() {} diff --git a/core/src/main/java/google/registry/model/console/ConsoleUpdateHistory.java b/core/src/main/java/google/registry/model/console/ConsoleUpdateHistory.java index aa08bbffc..6a60d40c6 100644 --- a/core/src/main/java/google/registry/model/console/ConsoleUpdateHistory.java +++ b/core/src/main/java/google/registry/model/console/ConsoleUpdateHistory.java @@ -16,6 +16,7 @@ package google.registry.model.console; import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; +import com.google.gson.annotations.Expose; import google.registry.model.Buildable; import google.registry.model.ImmutableObject; import google.registry.model.annotations.IdAllocation; @@ -45,6 +46,7 @@ public class ConsoleUpdateHistory extends ImmutableObject implements Buildable { @Id @IdAllocation @Column Long revisionId; @Column(nullable = false) + @Expose DateTime modificationTime; /** The HTTP method (e.g. POST, PUT) used to make this modification. */ @@ -54,6 +56,7 @@ public class ConsoleUpdateHistory extends ImmutableObject implements Buildable { /** The type of modification. */ @Column(nullable = false) @Enumerated(EnumType.STRING) + @Expose Type type; /** The URL of the action that was used to make the modification. */ @@ -61,11 +64,12 @@ public class ConsoleUpdateHistory extends ImmutableObject implements Buildable { String url; /** An optional further description of the action. */ - String description; + @Expose String description; /** The user that performed the modification. */ @JoinColumn(name = "actingUser", referencedColumnName = "emailAddress", nullable = false) @ManyToOne + @Expose User actingUser; public Long getRevisionId() { @@ -102,18 +106,24 @@ public class ConsoleUpdateHistory extends ImmutableObject implements Buildable { } public enum Type { + DUM_DOWNLOAD, DOMAIN_DELETE, DOMAIN_SUSPEND, DOMAIN_UNSUSPEND, EPP_PASSWORD_UPDATE, REGISTRAR_CREATE, + REGISTRAR_CONTACTS_UPDATE, REGISTRAR_SECURITY_UPDATE, REGISTRAR_UPDATE, + REGISTRY_LOCK, + REGISTRY_UNLOCK, USER_CREATE, USER_DELETE, - USER_UPDATE + USER_UPDATE, } + public static final String DESCRIPTION_SEPARATOR = "|"; + public static class Builder extends Buildable.Builder { public Builder() {} diff --git a/core/src/main/java/google/registry/module/RequestComponent.java b/core/src/main/java/google/registry/module/RequestComponent.java index a945d3efb..9fca8fdf9 100644 --- a/core/src/main/java/google/registry/module/RequestComponent.java +++ b/core/src/main/java/google/registry/module/RequestComponent.java @@ -115,6 +115,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.ConsoleHistoryDataAction; import google.registry.ui.server.console.ConsoleModule; import google.registry.ui.server.console.ConsoleOteAction; import google.registry.ui.server.console.ConsoleRegistryLockAction; @@ -185,6 +186,8 @@ interface RequestComponent { ConsoleEppPasswordAction consoleEppPasswordAction(); + ConsoleHistoryDataAction consoleHistoryDataAction(); + ConsoleOteAction consoleOteAction(); ConsoleRegistryLockAction consoleRegistryLockAction(); diff --git a/core/src/main/java/google/registry/ui/server/console/ConsoleDumDownloadAction.java b/core/src/main/java/google/registry/ui/server/console/ConsoleDumDownloadAction.java index 1db652e2f..102aa06e5 100644 --- a/core/src/main/java/google/registry/ui/server/console/ConsoleDumDownloadAction.java +++ b/core/src/main/java/google/registry/ui/server/console/ConsoleDumDownloadAction.java @@ -23,6 +23,7 @@ import com.google.common.flogger.FluentLogger; import com.google.common.net.MediaType; import google.registry.config.RegistryConfig.Config; import google.registry.model.console.ConsolePermission; +import google.registry.model.console.ConsoleUpdateHistory; import google.registry.model.console.User; import google.registry.request.Action; import google.registry.request.Action.GaeService; @@ -98,6 +99,13 @@ public class ConsoleDumDownloadAction extends ConsoleApiAction { consoleApiParams.response().setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); return; } + tm().transact( + () -> { + finishAndPersistConsoleUpdateHistory( + new ConsoleUpdateHistory.Builder() + .setType(ConsoleUpdateHistory.Type.DUM_DOWNLOAD) + .setDescription(registrarId)); + }); consoleApiParams.response().setStatus(HttpServletResponse.SC_OK); } diff --git a/core/src/main/java/google/registry/ui/server/console/ConsoleHistoryDataAction.java b/core/src/main/java/google/registry/ui/server/console/ConsoleHistoryDataAction.java new file mode 100644 index 000000000..b1240e8c3 --- /dev/null +++ b/core/src/main/java/google/registry/ui/server/console/ConsoleHistoryDataAction.java @@ -0,0 +1,116 @@ +// Copyright 2025 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.base.Preconditions.checkArgument; +import static google.registry.persistence.transaction.TransactionManagerFactory.tm; +import static google.registry.request.Action.Method.GET; +import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +import static jakarta.servlet.http.HttpServletResponse.SC_OK; + +import com.google.common.base.Strings; +import google.registry.model.console.ConsolePermission; +import google.registry.model.console.ConsoleUpdateHistory; +import google.registry.model.console.User; +import google.registry.request.Action; +import google.registry.request.Action.GaeService; +import google.registry.request.Action.GkeService; +import google.registry.request.Parameter; +import google.registry.request.auth.Auth; +import jakarta.inject.Inject; +import java.util.List; +import java.util.Optional; + +@Action( + service = GaeService.DEFAULT, + gkeService = GkeService.CONSOLE, + path = ConsoleHistoryDataAction.PATH, + method = {GET}, + auth = Auth.AUTH_PUBLIC_LOGGED_IN) +public class ConsoleHistoryDataAction extends ConsoleApiAction { + + private static final String SQL_USER_HISTORY = + """ + SELECT * FROM "ConsoleUpdateHistory" + WHERE acting_user = :actingUser + """; + + private static final String SQL_REGISTRAR_HISTORY = + """ + SELECT * + FROM "ConsoleUpdateHistory" + WHERE SPLIT_PART(description, '|', 1) = :registrarId; + """; + + public static final String PATH = "/console-api/history"; + + private final String registrarId; + private final Optional consoleUserEmail; + + @Inject + public ConsoleHistoryDataAction( + ConsoleApiParams consoleApiParams, + @Parameter("registrarId") String registrarId, + @Parameter("consoleUserEmail") Optional consoleUserEmail) { + super(consoleApiParams); + this.registrarId = registrarId; + this.consoleUserEmail = consoleUserEmail; + } + + @Override + protected void getHandler(User user) { + if (this.consoleUserEmail.isPresent()) { + this.historyByUser(user, this.consoleUserEmail.get()); + return; + } + + this.historyByRegistrarId(user, this.registrarId); + } + + private void historyByUser(User user, String consoleUserEmail) { + if (!user.getUserRoles().hasGlobalPermission(ConsolePermission.AUDIT_ACTIVITY_BY_USER)) { + setFailedResponse( + "User doesn't have a permission to check audit activity by user", SC_BAD_REQUEST); + return; + } + + List queryResult = + tm().transact( + () -> + tm().getEntityManager() + .createNativeQuery(SQL_USER_HISTORY, ConsoleUpdateHistory.class) + .setParameter("actingUser", consoleUserEmail) + .setHint("org.hibernate.fetchSize", 1000) + .getResultList()); + + consoleApiParams.response().setPayload(consoleApiParams.gson().toJson(queryResult)); + consoleApiParams.response().setStatus(SC_OK); + } + + private void historyByRegistrarId(User user, String registrarId) { + checkArgument(!Strings.isNullOrEmpty(registrarId), "Empty registrarId param"); + checkPermission(user, registrarId, ConsolePermission.AUDIT_ACTIVITY_BY_REGISTRAR); + List queryResult = + tm().transact( + () -> + tm().getEntityManager() + .createNativeQuery(SQL_REGISTRAR_HISTORY, ConsoleUpdateHistory.class) + .setParameter("registrarId", registrarId) + .setHint("org.hibernate.fetchSize", 1000) + .getResultList()); + consoleApiParams.response().setPayload(consoleApiParams.gson().toJson(queryResult)); + consoleApiParams.response().setStatus(SC_OK); + } +} diff --git a/core/src/main/java/google/registry/ui/server/console/ConsoleRegistryLockVerifyAction.java b/core/src/main/java/google/registry/ui/server/console/ConsoleRegistryLockVerifyAction.java index 8f32f6098..12817522e 100644 --- a/core/src/main/java/google/registry/ui/server/console/ConsoleRegistryLockVerifyAction.java +++ b/core/src/main/java/google/registry/ui/server/console/ConsoleRegistryLockVerifyAction.java @@ -14,10 +14,12 @@ package google.registry.ui.server.console; +import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.request.Action.Method.GET; import com.google.common.base.Ascii; import com.google.gson.annotations.Expose; +import google.registry.model.console.ConsoleUpdateHistory; import google.registry.model.console.User; import google.registry.model.domain.RegistryLock; import google.registry.request.Action; @@ -64,6 +66,21 @@ public class ConsoleRegistryLockVerifyAction extends ConsoleApiAction { RegistryLockVerificationResponse lockResponse = new RegistryLockVerificationResponse( Ascii.toLowerCase(action.toString()), lock.getDomainName(), lock.getRegistrarId()); + tm().transact( + () -> { + finishAndPersistConsoleUpdateHistory( + new ConsoleUpdateHistory.Builder() + .setType( + action == RegistryLockAction.LOCKED + ? ConsoleUpdateHistory.Type.REGISTRY_LOCK + : ConsoleUpdateHistory.Type.REGISTRY_UNLOCK) + .setDescription( + String.format( + "%s%s%s", + lock.getRegistrarId(), + ConsoleUpdateHistory.DESCRIPTION_SEPARATOR, + lockResponse))); + }); consoleApiParams.response().setPayload(consoleApiParams.gson().toJson(lockResponse)); consoleApiParams.response().setStatus(HttpServletResponse.SC_OK); } diff --git a/core/src/main/java/google/registry/ui/server/console/ConsoleUsersAction.java b/core/src/main/java/google/registry/ui/server/console/ConsoleUsersAction.java index b59750206..8ae98c09d 100644 --- a/core/src/main/java/google/registry/ui/server/console/ConsoleUsersAction.java +++ b/core/src/main/java/google/registry/ui/server/console/ConsoleUsersAction.java @@ -35,6 +35,7 @@ import com.google.common.collect.ImmutableSet; import com.google.gson.annotations.Expose; import google.registry.config.RegistryConfig.Config; import google.registry.model.console.ConsolePermission; +import google.registry.model.console.ConsoleUpdateHistory; import google.registry.model.console.RegistrarRole; import google.registry.model.console.User; import google.registry.model.console.UserRoles; @@ -177,6 +178,12 @@ public class ConsoleUsersAction extends ConsoleApiAction { tm().delete(key); User.revokeIapPermission(email, maybeGroupEmailAddress, cloudTasksUtils, null, iamClient); sendConfirmationEmail(registrarId, email, "Deleted user"); + finishAndPersistConsoleUpdateHistory( + new ConsoleUpdateHistory.Builder() + .setType(ConsoleUpdateHistory.Type.USER_DELETE) + .setDescription( + String.format( + "%s%s%s", registrarId, ConsoleUpdateHistory.DESCRIPTION_SEPARATOR, email))); } consoleApiParams.response().setStatus(SC_OK); @@ -231,6 +238,12 @@ public class ConsoleUsersAction extends ConsoleApiAction { consoleApiParams .gson() .toJson(new UserData(newEmail, ACCOUNT_MANAGER.toString(), newUser.getPassword()))); + finishAndPersistConsoleUpdateHistory( + new ConsoleUpdateHistory.Builder() + .setType(ConsoleUpdateHistory.Type.USER_CREATE) + .setDescription( + String.format( + "%s%s%s", registrarId, ConsoleUpdateHistory.DESCRIPTION_SEPARATOR, newEmail))); } private void runUpdateInTransaction() { @@ -245,6 +258,15 @@ public class ConsoleUsersAction extends ConsoleApiAction { sendConfirmationEmail(registrarId, this.userData.get().emailAddress, "Updated user"); consoleApiParams.response().setStatus(SC_OK); + finishAndPersistConsoleUpdateHistory( + new ConsoleUpdateHistory.Builder() + .setType(ConsoleUpdateHistory.Type.USER_UPDATE) + .setDescription( + String.format( + "%s%s%s", + registrarId, + ConsoleUpdateHistory.DESCRIPTION_SEPARATOR, + this.userData.get().emailAddress))); } private boolean isModifyingRequestValid() { diff --git a/core/src/main/java/google/registry/ui/server/console/settings/ContactAction.java b/core/src/main/java/google/registry/ui/server/console/settings/ContactAction.java index 1c9291b71..e7c961cb7 100644 --- a/core/src/main/java/google/registry/ui/server/console/settings/ContactAction.java +++ b/core/src/main/java/google/registry/ui/server/console/settings/ContactAction.java @@ -30,6 +30,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Multimap; import com.google.common.flogger.FluentLogger; import google.registry.model.console.ConsolePermission; +import google.registry.model.console.ConsoleUpdateHistory; import google.registry.model.console.User; import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarPoc; @@ -84,6 +85,7 @@ public class ContactAction extends ConsoleApiAction { protected void deleteHandler(User user) { updateContacts( user, + "Deleted " + contact.get().getEmailAddress(), (registrar, oldContacts) -> oldContacts.stream() .filter( @@ -96,6 +98,7 @@ public class ContactAction extends ConsoleApiAction { protected void postHandler(User user) { updateContacts( user, + "Created " + contact.get().getEmailAddress(), (registrar, oldContacts) -> { RegistrarPoc newContact = contact.get(); return ImmutableSet.builder() @@ -121,6 +124,7 @@ public class ContactAction extends ConsoleApiAction { protected void putHandler(User user) { updateContacts( user, + "Updated " + contact.get().getEmailAddress(), (registrar, oldContacts) -> { RegistrarPoc updatedContact = contact.get(); return oldContacts.stream() @@ -146,6 +150,7 @@ public class ContactAction extends ConsoleApiAction { private void updateContacts( User user, + String historyDescription, BiFunction, ImmutableSet> contactsUpdater) { checkPermission(user, registrarId, ConsolePermission.EDIT_REGISTRAR_DETAILS); @@ -176,6 +181,15 @@ public class ContactAction extends ConsoleApiAction { tm().put(updatedRegistrar); sendExternalUpdatesIfNecessary( EmailInfo.create(registrar, updatedRegistrar, oldContacts, newContacts)); + finishAndPersistConsoleUpdateHistory( + new ConsoleUpdateHistory.Builder() + .setType(ConsoleUpdateHistory.Type.REGISTRAR_CONTACTS_UPDATE) + .setDescription( + String.format( + "%s%s%s", + registrarId, + ConsoleUpdateHistory.DESCRIPTION_SEPARATOR, + historyDescription))); }); consoleApiParams.response().setStatus(SC_OK); } diff --git a/core/src/main/java/google/registry/ui/server/console/settings/RdapRegistrarFieldsAction.java b/core/src/main/java/google/registry/ui/server/console/settings/RdapRegistrarFieldsAction.java index 7a211624b..b6ccb2d94 100644 --- a/core/src/main/java/google/registry/ui/server/console/settings/RdapRegistrarFieldsAction.java +++ b/core/src/main/java/google/registry/ui/server/console/settings/RdapRegistrarFieldsAction.java @@ -35,6 +35,7 @@ import google.registry.ui.server.console.ConsoleApiAction; import google.registry.ui.server.console.ConsoleApiParams; import jakarta.inject.Inject; import java.util.Optional; +import java.util.StringJoiner; /** * Console action for editing fields on a registrar that are visible in WHOIS/RDAP. @@ -82,19 +83,37 @@ public class RdapRegistrarFieldsAction extends ConsoleApiAction { return; } - Registrar newRegistrar = - savedRegistrar - .asBuilder() - .setLocalizedAddress(providedRegistrar.getLocalizedAddress()) - .setPhoneNumber(providedRegistrar.getPhoneNumber()) - .setFaxNumber(providedRegistrar.getFaxNumber()) - .setEmailAddress(providedRegistrar.getEmailAddress()) - .build(); + StringJoiner updates = new StringJoiner(","); + + var newRegistrarBuilder = savedRegistrar.asBuilder(); + + if (!providedRegistrar.getLocalizedAddress().equals(savedRegistrar.getLocalizedAddress())) { + newRegistrarBuilder.setLocalizedAddress(providedRegistrar.getLocalizedAddress()); + updates.add("ADDRESS"); + } + if (!providedRegistrar.getPhoneNumber().equals(savedRegistrar.getPhoneNumber())) { + newRegistrarBuilder.setPhoneNumber(providedRegistrar.getPhoneNumber()); + updates.add("PHONE"); + } + if (!providedRegistrar.getFaxNumber().equals(savedRegistrar.getPhoneNumber())) { + newRegistrarBuilder.setFaxNumber(providedRegistrar.getFaxNumber()); + updates.add("FAX"); + } + if (!providedRegistrar.getEmailAddress().equals(savedRegistrar.getEmailAddress())) { + newRegistrarBuilder.setEmailAddress(providedRegistrar.getEmailAddress()); + updates.add("EMAIL"); + } + var newRegistrar = newRegistrarBuilder.build(); tm().put(newRegistrar); finishAndPersistConsoleUpdateHistory( new ConsoleUpdateHistory.Builder() .setType(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE) - .setDescription(newRegistrar.getRegistrarId())); + .setDescription( + String.format( + "%s%s%s", + newRegistrar.getRegistrarId(), + ConsoleUpdateHistory.DESCRIPTION_SEPARATOR, + updates))); sendExternalUpdatesIfNecessary( EmailInfo.create( savedRegistrar, diff --git a/core/src/main/java/google/registry/ui/server/console/settings/SecurityAction.java b/core/src/main/java/google/registry/ui/server/console/settings/SecurityAction.java index 742314248..80341d567 100644 --- a/core/src/main/java/google/registry/ui/server/console/settings/SecurityAction.java +++ b/core/src/main/java/google/registry/ui/server/console/settings/SecurityAction.java @@ -39,6 +39,7 @@ import google.registry.ui.server.console.ConsoleApiAction; import google.registry.ui.server.console.ConsoleApiParams; import jakarta.inject.Inject; import java.util.Optional; +import java.util.StringJoiner; @Action( service = GaeService.DEFAULT, @@ -86,10 +87,15 @@ public class SecurityAction extends ConsoleApiAction { private void setResponse(Registrar savedRegistrar) { Registrar registrarParameter = registrar.get(); - Registrar.Builder updatedRegistrarBuilder = - savedRegistrar - .asBuilder() - .setIpAddressAllowList(registrarParameter.getIpAddressAllowList()); + Registrar.Builder updatedRegistrarBuilder = savedRegistrar.asBuilder(); + StringJoiner updates = new StringJoiner(","); + + if (!savedRegistrar + .getIpAddressAllowList() + .equals(registrarParameter.getIpAddressAllowList())) { + updatedRegistrarBuilder.setIpAddressAllowList(registrarParameter.getIpAddressAllowList()); + updates.add("IP_CHANGE"); + } try { if (!savedRegistrar @@ -99,6 +105,7 @@ public class SecurityAction extends ConsoleApiAction { String newClientCert = registrarParameter.getClientCertificate().get(); certificateChecker.validateCertificate(newClientCert); updatedRegistrarBuilder.setClientCertificate(newClientCert, tm().getTransactionTime()); + updates.add("PRIMARY_SSL_CERT_CHANGE"); } } if (!savedRegistrar @@ -109,6 +116,7 @@ public class SecurityAction extends ConsoleApiAction { certificateChecker.validateCertificate(newFailoverCert); updatedRegistrarBuilder.setFailoverClientCertificate( newFailoverCert, tm().getTransactionTime()); + updates.add("FAILOVER_SSL_CERT_CHANGE"); } } } catch (InsecureCertificateException e) { @@ -121,7 +129,9 @@ public class SecurityAction extends ConsoleApiAction { finishAndPersistConsoleUpdateHistory( new ConsoleUpdateHistory.Builder() .setType(ConsoleUpdateHistory.Type.REGISTRAR_SECURITY_UPDATE) - .setDescription(registrarId)); + .setDescription( + String.format( + "%s%s%s", registrarId, ConsoleUpdateHistory.DESCRIPTION_SEPARATOR, updates))); sendExternalUpdatesIfNecessary( EmailInfo.create(savedRegistrar, updatedRegistrar, ImmutableSet.of(), ImmutableSet.of())); diff --git a/core/src/test/java/google/registry/module/RequestComponentTest.java b/core/src/test/java/google/registry/module/RequestComponentTest.java index e1a2fbd54..5b0f5d048 100644 --- a/core/src/test/java/google/registry/module/RequestComponentTest.java +++ b/core/src/test/java/google/registry/module/RequestComponentTest.java @@ -29,6 +29,7 @@ import google.registry.testing.TestDataHelper; import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; /** Unit tests for {@link RequestComponent}. */ @@ -49,6 +50,7 @@ public class RequestComponentTest { } @Test + @Disabled("To be removed with GAE components") void testGaeToJettyRoutingCoverage() { Set jettyRoutes = getRoutes(RequestComponent.class, "routing.txt"); Set gaeRoutes = new HashSet<>(); diff --git a/core/src/test/java/google/registry/ui/server/console/ConsoleHistoryDataActionTest.java b/core/src/test/java/google/registry/ui/server/console/ConsoleHistoryDataActionTest.java new file mode 100644 index 000000000..b513f139d --- /dev/null +++ b/core/src/test/java/google/registry/ui/server/console/ConsoleHistoryDataActionTest.java @@ -0,0 +1,153 @@ +// Copyright 2025 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 com.google.common.truth.Truth.assertThat; +import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN; +import static jakarta.servlet.http.HttpServletResponse.SC_OK; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; +import google.registry.model.console.ConsoleUpdateHistory; +import google.registry.model.console.RegistrarRole; +import google.registry.model.console.User; +import google.registry.model.console.UserRoles; +import google.registry.request.auth.AuthResult; +import google.registry.testing.ConsoleApiParamsUtils; +import google.registry.testing.DatabaseHelper; +import google.registry.testing.FakeResponse; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ConsoleHistoryDataActionTest extends ConsoleActionBaseTestCase { + + private static final Gson GSON = new Gson(); + private User noPermissionUser; + + @BeforeEach + void beforeEach() { + noPermissionUser = + DatabaseHelper.persistResource( + new User.Builder() + .setEmailAddress("no.perms@example.com") + .setUserRoles( + new UserRoles.Builder() + .setRegistrarRoles( + ImmutableMap.of("TheRegistrar", RegistrarRole.ACCOUNT_MANAGER)) + .build()) + .build()); + + DatabaseHelper.persistResources( + ImmutableList.of( + new ConsoleUpdateHistory.Builder() + .setType(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE) + .setActingUser(fteUser) + .setDescription("TheRegistrar|Some change") + .setModificationTime(clock.nowUtc()) + .setUrl("/test") + .setMethod("POST") + .build(), + new ConsoleUpdateHistory.Builder() + .setType(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE) + .setActingUser(noPermissionUser) + .setDescription("TheRegistrar|Another change") + .setModificationTime(clock.nowUtc()) + .setUrl("/test") + .setMethod("POST") + .build(), + new ConsoleUpdateHistory.Builder() + .setType(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE) + .setActingUser(fteUser) + .setDescription("OtherRegistrar|Some change") + .setModificationTime(clock.nowUtc()) + .setUrl("/test") + .setMethod("POST") + .build())); + } + + @Test + void testSuccess_getByRegistrar() { + ConsoleHistoryDataAction action = + createAction(AuthResult.createUser(fteUser), "TheRegistrar", Optional.empty()); + action.run(); + assertThat(response.getStatus()).isEqualTo(SC_OK); + List> payload = GSON.fromJson(response.getPayload(), List.class); + assertThat(payload.stream().map(record -> record.get("description")).collect(toImmutableList())) + .containsExactly("TheRegistrar|Some change", "TheRegistrar|Another change"); + } + + @Test + void testSuccess_getByUser() { + ConsoleHistoryDataAction action = + createAction(AuthResult.createUser(fteUser), "TheRegistrar", Optional.of("fte@email.tld")); + action.run(); + assertThat(response.getStatus()).isEqualTo(SC_OK); + List> payload = GSON.fromJson(response.getPayload(), List.class); + assertThat(payload.stream().map(record -> record.get("description")).collect(toImmutableList())) + .containsExactly("TheRegistrar|Some change", "OtherRegistrar|Some change"); + } + + @Test + void testSuccess_noResults() { + ConsoleHistoryDataAction action = + createAction(AuthResult.createUser(fteUser), "NoHistoryRegistrar", Optional.empty()); + action.run(); + assertThat(response.getStatus()).isEqualTo(SC_OK); + assertThat(response.getPayload()).isEqualTo("[]"); + } + + @Test + void testFailure_getByRegistrar_noPermission() { + ConsoleHistoryDataAction action = + createAction(AuthResult.createUser(noPermissionUser), "TheRegistrar", Optional.empty()); + action.run(); + assertThat(response.getStatus()).isEqualTo(SC_FORBIDDEN); + } + + @Test + void testFailure_getByUser_noPermission() { + ConsoleHistoryDataAction action = + createAction( + AuthResult.createUser(noPermissionUser), "TheRegistrar", Optional.of("fte@email.tld")); + action.run(); + assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); + assertThat(response.getPayload()) + .contains("User doesn't have a permission to check audit activity by user"); + } + + @Test + void testFailure_emptyRegistrarId() { + ConsoleHistoryDataAction action = + createAction(AuthResult.createUser(fteUser), "", Optional.empty()); + action.run(); + assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); + assertThat(response.getPayload()).contains("Empty registrarId param"); + } + + private ConsoleHistoryDataAction createAction( + AuthResult authResult, String registrarId, Optional consoleUserEmail) { + consoleApiParams = ConsoleApiParamsUtils.createFake(authResult); + when(consoleApiParams.request().getMethod()).thenReturn("GET"); + response = (FakeResponse) consoleApiParams.response(); + return new ConsoleHistoryDataAction(consoleApiParams, registrarId, consoleUserEmail); + } +} diff --git a/core/src/test/java/google/registry/ui/server/console/ConsoleRegistryLockVerifyActionTest.java b/core/src/test/java/google/registry/ui/server/console/ConsoleRegistryLockVerifyActionTest.java index 5618a87f3..e5a69bafa 100644 --- a/core/src/test/java/google/registry/ui/server/console/ConsoleRegistryLockVerifyActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/ConsoleRegistryLockVerifyActionTest.java @@ -56,16 +56,17 @@ public class ConsoleRegistryLockVerifyActionTest extends ConsoleActionBaseTestCa 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(); + persistResource( + 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); } diff --git a/core/src/test/java/google/registry/ui/server/console/settings/RdapRegistrarFieldsActionTest.java b/core/src/test/java/google/registry/ui/server/console/settings/RdapRegistrarFieldsActionTest.java index f95113548..01358c45f 100644 --- a/core/src/test/java/google/registry/ui/server/console/settings/RdapRegistrarFieldsActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/settings/RdapRegistrarFieldsActionTest.java @@ -112,7 +112,7 @@ public class RdapRegistrarFieldsActionTest extends ConsoleActionBaseTestCase { .isEqualExceptFields(oldRegistrar, "localizedAddress", "phoneNumber", "faxNumber"); ConsoleUpdateHistory history = loadSingleton(ConsoleUpdateHistory.class).get(); assertThat(history.getType()).isEqualTo(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE); - assertThat(history.getDescription()).hasValue("TheRegistrar"); + assertThat(history.getDescription()).hasValue("TheRegistrar|ADDRESS,PHONE,FAX"); } @Test diff --git a/core/src/test/java/google/registry/ui/server/console/settings/SecurityActionTest.java b/core/src/test/java/google/registry/ui/server/console/settings/SecurityActionTest.java index 99d9f194c..350a8e944 100644 --- a/core/src/test/java/google/registry/ui/server/console/settings/SecurityActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/settings/SecurityActionTest.java @@ -87,7 +87,7 @@ class SecurityActionTest extends ConsoleActionBaseTestCase { assertThat(r.getIpAddressAllowList().get(0).getNetmask()).isEqualTo(32); ConsoleUpdateHistory history = loadSingleton(ConsoleUpdateHistory.class).get(); assertThat(history.getType()).isEqualTo(ConsoleUpdateHistory.Type.REGISTRAR_SECURITY_UPDATE); - assertThat(history.getDescription()).hasValue("registrarId"); + assertThat(history.getDescription()).hasValue("registrarId|IP_CHANGE,PRIMARY_SSL_CERT_CHANGE"); } private SecurityAction createAction(String registrarId) throws IOException { diff --git a/core/src/test/resources/google/registry/module/routing.txt b/core/src/test/resources/google/registry/module/routing.txt index 7b2412394..5250a322c 100644 --- a/core/src/test/resources/google/registry/module/routing.txt +++ b/core/src/test/resources/google/registry/module/routing.txt @@ -74,6 +74,7 @@ CONSOLE /console-api/domain ConsoleDomainGetActi CONSOLE /console-api/domain-list ConsoleDomainListAction GET n USER PUBLIC CONSOLE /console-api/dum-download ConsoleDumDownloadAction GET n USER PUBLIC CONSOLE /console-api/eppPassword ConsoleEppPasswordAction POST n USER PUBLIC +CONSOLE /console-api/history ConsoleHistoryDataAction GET n USER PUBLIC CONSOLE /console-api/ote ConsoleOteAction GET,POST n USER PUBLIC CONSOLE /console-api/password-reset-request PasswordResetRequestAction POST n USER PUBLIC CONSOLE /console-api/password-reset-verify PasswordResetVerifyAction GET,POST n USER PUBLIC diff --git a/db/src/main/resources/sql/schema/db-schema.sql.generated b/db/src/main/resources/sql/schema/db-schema.sql.generated index 078675fa8..3755f6116 100644 --- a/db/src/main/resources/sql/schema/db-schema.sql.generated +++ b/db/src/main/resources/sql/schema/db-schema.sql.generated @@ -139,7 +139,7 @@ description text, method text not null, modification_time timestamp(6) with time zone not null, - type text not null check (type in ('DOMAIN_DELETE','DOMAIN_SUSPEND','DOMAIN_UNSUSPEND','EPP_PASSWORD_UPDATE','REGISTRAR_CREATE','REGISTRAR_SECURITY_UPDATE','REGISTRAR_UPDATE','USER_CREATE','USER_DELETE','USER_UPDATE')), + type text not null check (type in ('DUM_DOWNLOAD','DOMAIN_DELETE','DOMAIN_SUSPEND','DOMAIN_UNSUSPEND','EPP_PASSWORD_UPDATE','REGISTRAR_CREATE','REGISTRAR_CONTACTS_UPDATE','REGISTRAR_SECURITY_UPDATE','REGISTRAR_UPDATE','REGISTRY_LOCK','REGISTRY_UNLOCK','USER_CREATE','USER_DELETE','USER_UPDATE')), url text not null, acting_user text not null, primary key (revision_id)