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 a4fb4497d..2a29dac83 100644 --- a/core/src/main/java/google/registry/model/console/ConsoleUpdateHistory.java +++ b/core/src/main/java/google/registry/model/console/ConsoleUpdateHistory.java @@ -41,10 +41,12 @@ import org.joda.time.DateTime; public abstract class ConsoleUpdateHistory extends ImmutableObject implements Buildable { public enum Type { - EPP_ACTION, - POC_CREATE, - POC_UPDATE, - POC_DELETE, + DOMAIN_DELETE, + DOMAIN_SUSPEND, + DOMAIN_UNSUSPEND, + EPP_PASSWORD_UPDATE, + REGISTRAR_CREATE, + REGISTRAR_SECURITY_UPDATE, REGISTRAR_UPDATE, USER_CREATE, USER_DELETE, diff --git a/core/src/main/java/google/registry/model/console/SimpleConsoleUpdateHistory.java b/core/src/main/java/google/registry/model/console/SimpleConsoleUpdateHistory.java new file mode 100644 index 000000000..b172bc9e4 --- /dev/null +++ b/core/src/main/java/google/registry/model/console/SimpleConsoleUpdateHistory.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.model.console; + +import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; + +import google.registry.model.Buildable; +import google.registry.model.ImmutableObject; +import google.registry.model.annotations.IdAllocation; +import google.registry.persistence.WithVKey; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.util.Optional; +import org.joda.time.DateTime; + +@Entity +@WithVKey(Long.class) +@Table( + name = "ConsoleUpdateHistory", + indexes = { + @Index(columnList = "actingUser", name = "idx_console_update_history_acting_user"), + @Index(columnList = "type", name = "idx_console_update_history_type"), + @Index(columnList = "modificationTime", name = "idx_console_update_history_modification_time") + }) +// TODO: rename this to ConsoleUpdateHistory when that class is removed +public class SimpleConsoleUpdateHistory extends ImmutableObject implements Buildable { + + @Id @IdAllocation @Column Long revisionId; + + @Column(nullable = false) + DateTime modificationTime; + + /** The HTTP method (e.g. POST, PUT) used to make this modification. */ + @Column(nullable = false) + String method; + + /** The type of modification. */ + @Column(nullable = false) + @Enumerated(EnumType.STRING) + ConsoleUpdateHistory.Type type; + + /** The URL of the action that was used to make the modification. */ + @Column(nullable = false) + String url; + + /** An optional further description of the action. */ + String description; + + /** The user that performed the modification. */ + @JoinColumn(name = "actingUser", referencedColumnName = "emailAddress", nullable = false) + @ManyToOne + User actingUser; + + public Long getRevisionId() { + return revisionId; + } + + public DateTime getModificationTime() { + return modificationTime; + } + + public Optional getDescription() { + return Optional.ofNullable(description); + } + + public String getMethod() { + return method; + } + + public ConsoleUpdateHistory.Type getType() { + return type; + } + + public String getUrl() { + return url; + } + + public User getActingUser() { + return actingUser; + } + + @Override + public Builder asBuilder() { + return new Builder(clone(this)); + } + + public static class Builder extends Buildable.Builder { + public Builder() {} + + private Builder(SimpleConsoleUpdateHistory instance) { + super(instance); + } + + @Override + public SimpleConsoleUpdateHistory build() { + checkArgumentNotNull(getInstance().modificationTime, "Modification time must be specified"); + checkArgumentNotNull(getInstance().actingUser, "Acting user must be specified"); + checkArgumentNotNull(getInstance().url, "URL must be specified"); + checkArgumentNotNull(getInstance().method, "HTTP method must be specified"); + checkArgumentNotNull(getInstance().type, "ConsoleUpdateHistory type must be specified"); + return super.build(); + } + + public Builder setModificationTime(DateTime modificationTime) { + getInstance().modificationTime = modificationTime; + return this; + } + + public Builder setActingUser(User actingUser) { + getInstance().actingUser = actingUser; + return this; + } + + public Builder setUrl(String url) { + getInstance().url = url; + return this; + } + + public Builder setMethod(String method) { + getInstance().method = method; + return this; + } + + public Builder setDescription(String description) { + getInstance().description = description; + return this; + } + + public Builder setType(ConsoleUpdateHistory.Type type) { + getInstance().type = type; + return this; + } + } +} diff --git a/core/src/main/java/google/registry/ui/server/console/ConsoleApiAction.java b/core/src/main/java/google/registry/ui/server/console/ConsoleApiAction.java index f2ff52459..969c8f9b4 100644 --- a/core/src/main/java/google/registry/ui/server/console/ConsoleApiAction.java +++ b/core/src/main/java/google/registry/ui/server/console/ConsoleApiAction.java @@ -37,7 +37,7 @@ import google.registry.batch.CloudTasksUtils; import google.registry.config.RegistryConfig; import google.registry.export.sheet.SyncRegistrarsSheetAction; import google.registry.model.console.ConsolePermission; -import google.registry.model.console.ConsoleUpdateHistory; +import google.registry.model.console.SimpleConsoleUpdateHistory; import google.registry.model.console.User; import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarPoc; @@ -262,7 +262,7 @@ public abstract class ConsoleApiAction implements Runnable { } } - protected void finishAndPersistConsoleUpdateHistory(ConsoleUpdateHistory.Builder builder) { + protected void finishAndPersistConsoleUpdateHistory(SimpleConsoleUpdateHistory.Builder builder) { builder.setActingUser(consoleApiParams.authResult().user().get()); builder.setUrl(consoleApiParams.request().getRequestURI()); builder.setMethod(consoleApiParams.request().getMethod()); diff --git a/core/src/main/java/google/registry/ui/server/console/ConsoleEppPasswordAction.java b/core/src/main/java/google/registry/ui/server/console/ConsoleEppPasswordAction.java index 790f3e0c3..9817886d5 100644 --- a/core/src/main/java/google/registry/ui/server/console/ConsoleEppPasswordAction.java +++ b/core/src/main/java/google/registry/ui/server/console/ConsoleEppPasswordAction.java @@ -28,7 +28,7 @@ import com.google.gson.annotations.Expose; import google.registry.flows.EppException.AuthenticationErrorException; import google.registry.flows.PasswordOnlyTransportCredentials; import google.registry.model.console.ConsoleUpdateHistory; -import google.registry.model.console.RegistrarUpdateHistory; +import google.registry.model.console.SimpleConsoleUpdateHistory; import google.registry.model.console.User; import google.registry.model.registrar.Registrar; import google.registry.request.Action; @@ -107,14 +107,10 @@ public class ConsoleEppPasswordAction extends ConsoleApiAction { Registrar updatedRegistrar = registrar.asBuilder().setPassword(eppRequestBody.newPassword()).build(); tm().put(updatedRegistrar); - EppPasswordData sanitizedData = - new EppPasswordData( - eppRequestBody.registrarId, "********", "••••••••", "••••••••"); finishAndPersistConsoleUpdateHistory( - new RegistrarUpdateHistory.Builder() - .setType(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE) - .setRegistrar(updatedRegistrar) - .setRequestBody(consoleApiParams.gson().toJson(sanitizedData))); + new SimpleConsoleUpdateHistory.Builder() + .setType(ConsoleUpdateHistory.Type.EPP_PASSWORD_UPDATE) + .setDescription(registrar.getRegistrarId())); sendExternalUpdates( ImmutableMap.of("password", new DiffUtils.DiffPair("********", "••••••••")), registrar, diff --git a/core/src/main/java/google/registry/ui/server/console/ConsoleUpdateRegistrarAction.java b/core/src/main/java/google/registry/ui/server/console/ConsoleUpdateRegistrarAction.java index 7082ddd36..476c9aa9c 100644 --- a/core/src/main/java/google/registry/ui/server/console/ConsoleUpdateRegistrarAction.java +++ b/core/src/main/java/google/registry/ui/server/console/ConsoleUpdateRegistrarAction.java @@ -24,7 +24,7 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; import google.registry.model.console.ConsolePermission; import google.registry.model.console.ConsoleUpdateHistory; -import google.registry.model.console.RegistrarUpdateHistory; +import google.registry.model.console.SimpleConsoleUpdateHistory; import google.registry.model.console.User; import google.registry.model.registrar.Registrar; import google.registry.request.Action; @@ -102,10 +102,9 @@ public class ConsoleUpdateRegistrarAction extends ConsoleApiAction { tm().put(updatedRegistrar); finishAndPersistConsoleUpdateHistory( - new RegistrarUpdateHistory.Builder() + new SimpleConsoleUpdateHistory.Builder() .setType(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE) - .setRegistrar(updatedRegistrar) - .setRequestBody(consoleApiParams.gson().toJson(registrarParam))); + .setDescription(updatedRegistrar.getRegistrarId())); sendExternalUpdatesIfNecessary( EmailInfo.create( existingRegistrar.get(), diff --git a/core/src/main/java/google/registry/ui/server/console/RegistrarsAction.java b/core/src/main/java/google/registry/ui/server/console/RegistrarsAction.java index 96aeb0e4b..f0772dc2b 100644 --- a/core/src/main/java/google/registry/ui/server/console/RegistrarsAction.java +++ b/core/src/main/java/google/registry/ui/server/console/RegistrarsAction.java @@ -28,7 +28,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Streams; import google.registry.model.console.ConsolePermission; import google.registry.model.console.ConsoleUpdateHistory; -import google.registry.model.console.RegistrarUpdateHistory; +import google.registry.model.console.SimpleConsoleUpdateHistory; import google.registry.model.console.User; import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarBase; @@ -174,10 +174,9 @@ public class RegistrarsAction extends ConsoleApiAction { registrar.getRegistrarId()); tm().putAll(registrar, contact); finishAndPersistConsoleUpdateHistory( - new RegistrarUpdateHistory.Builder() - .setType(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE) - .setRegistrar(registrar) - .setRequestBody(consoleApiParams.gson().toJson(registrar))); + new SimpleConsoleUpdateHistory.Builder() + .setType(ConsoleUpdateHistory.Type.REGISTRAR_CREATE) + .setDescription(registrar.getRegistrarId())); }); } diff --git a/core/src/main/java/google/registry/ui/server/console/domains/ConsoleBulkDomainAction.java b/core/src/main/java/google/registry/ui/server/console/domains/ConsoleBulkDomainAction.java index 64b4e97f9..199918d96 100644 --- a/core/src/main/java/google/registry/ui/server/console/domains/ConsoleBulkDomainAction.java +++ b/core/src/main/java/google/registry/ui/server/console/domains/ConsoleBulkDomainAction.java @@ -15,6 +15,7 @@ package google.registry.ui.server.console.domains; import static com.google.common.collect.ImmutableMap.toImmutableMap; +import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static jakarta.servlet.http.HttpServletResponse.SC_OK; import static java.nio.charset.StandardCharsets.UTF_8; @@ -25,6 +26,7 @@ import google.registry.flows.EppController; import google.registry.flows.EppRequestSource; import google.registry.flows.PasswordOnlyTransportCredentials; import google.registry.flows.StatelessRequestSessionMetadata; +import google.registry.model.console.SimpleConsoleUpdateHistory; import google.registry.model.console.User; import google.registry.model.eppcommon.ProtocolDefinition; import google.registry.model.eppoutput.EppOutput; @@ -93,11 +95,29 @@ public class ConsoleBulkDomainAction extends ConsoleApiAction { domainList.domainList.stream() .collect( toImmutableMap(d -> d, d -> executeEpp(actionType.getXmlContentsToRun(d), user))); + handleHistoryAdditions(result, actionType); // Front end should parse situations where only some commands worked consoleApiParams.response().setPayload(consoleApiParams.gson().toJson(result)); consoleApiParams.response().setStatus(SC_OK); } + private void handleHistoryAdditions( + ImmutableMap result, ConsoleDomainActionType actionType) { + if (result.values().stream().noneMatch(ConsoleEppOutput::isSuccess)) { + return; + } + tm().transact( + () -> + result.entrySet().stream() + .filter(e -> e.getValue().isSuccess()) + .forEach( + e -> + finishAndPersistConsoleUpdateHistory( + new SimpleConsoleUpdateHistory.Builder() + .setDescription(e.getKey()) + .setType(actionType.getConsoleUpdateHistoryType())))); + } + private ConsoleEppOutput executeEpp(String xml, User user) { return ConsoleEppOutput.fromEppOutput( eppController.handleEppCommand( @@ -115,5 +135,9 @@ public class ConsoleBulkDomainAction extends ConsoleApiAction { Result result = eppOutput.getResponse().getResult(); return new ConsoleEppOutput(result.getMsg(), result.getCode().code); } + + boolean isSuccess() { + return responseCode < 2000; + } } } diff --git a/core/src/main/java/google/registry/ui/server/console/domains/ConsoleBulkDomainDeleteActionType.java b/core/src/main/java/google/registry/ui/server/console/domains/ConsoleBulkDomainDeleteActionType.java index 84f1327b1..b42dbaa08 100644 --- a/core/src/main/java/google/registry/ui/server/console/domains/ConsoleBulkDomainDeleteActionType.java +++ b/core/src/main/java/google/registry/ui/server/console/domains/ConsoleBulkDomainDeleteActionType.java @@ -16,6 +16,7 @@ package google.registry.ui.server.console.domains; import com.google.gson.JsonElement; import google.registry.model.console.ConsolePermission; +import google.registry.model.console.ConsoleUpdateHistory; /** An action that will run a delete EPP command on the given domain. */ public class ConsoleBulkDomainDeleteActionType extends ConsoleDomainActionType { @@ -54,4 +55,9 @@ public class ConsoleBulkDomainDeleteActionType extends ConsoleDomainActionType { public ConsolePermission getNecessaryPermission() { return ConsolePermission.EXECUTE_EPP_COMMANDS; } + + @Override + public ConsoleUpdateHistory.Type getConsoleUpdateHistoryType() { + return ConsoleUpdateHistory.Type.DOMAIN_DELETE; + } } diff --git a/core/src/main/java/google/registry/ui/server/console/domains/ConsoleBulkDomainSuspendActionType.java b/core/src/main/java/google/registry/ui/server/console/domains/ConsoleBulkDomainSuspendActionType.java index b690438dd..71b56fe43 100644 --- a/core/src/main/java/google/registry/ui/server/console/domains/ConsoleBulkDomainSuspendActionType.java +++ b/core/src/main/java/google/registry/ui/server/console/domains/ConsoleBulkDomainSuspendActionType.java @@ -16,6 +16,7 @@ package google.registry.ui.server.console.domains; import com.google.gson.JsonElement; import google.registry.model.console.ConsolePermission; +import google.registry.model.console.ConsoleUpdateHistory; /** An action that will suspend the given domain, assigning all 5 server*Prohibited statuses. */ public class ConsoleBulkDomainSuspendActionType extends ConsoleDomainActionType { @@ -64,4 +65,9 @@ public class ConsoleBulkDomainSuspendActionType extends ConsoleDomainActionType public ConsolePermission getNecessaryPermission() { return ConsolePermission.SUSPEND_DOMAIN; } + + @Override + public ConsoleUpdateHistory.Type getConsoleUpdateHistoryType() { + return ConsoleUpdateHistory.Type.DOMAIN_SUSPEND; + } } diff --git a/core/src/main/java/google/registry/ui/server/console/domains/ConsoleBulkDomainUnsuspendActionType.java b/core/src/main/java/google/registry/ui/server/console/domains/ConsoleBulkDomainUnsuspendActionType.java index 0d3a959e1..9ae158406 100644 --- a/core/src/main/java/google/registry/ui/server/console/domains/ConsoleBulkDomainUnsuspendActionType.java +++ b/core/src/main/java/google/registry/ui/server/console/domains/ConsoleBulkDomainUnsuspendActionType.java @@ -16,6 +16,7 @@ package google.registry.ui.server.console.domains; import com.google.gson.JsonElement; import google.registry.model.console.ConsolePermission; +import google.registry.model.console.ConsoleUpdateHistory; /** An action that will unsuspend the given domain, removing all 5 server*Prohibited statuses. */ public class ConsoleBulkDomainUnsuspendActionType extends ConsoleDomainActionType { @@ -64,4 +65,9 @@ public class ConsoleBulkDomainUnsuspendActionType extends ConsoleDomainActionTyp public ConsolePermission getNecessaryPermission() { return ConsolePermission.SUSPEND_DOMAIN; } + + @Override + public ConsoleUpdateHistory.Type getConsoleUpdateHistoryType() { + return ConsoleUpdateHistory.Type.DOMAIN_UNSUSPEND; + } } diff --git a/core/src/main/java/google/registry/ui/server/console/domains/ConsoleDomainActionType.java b/core/src/main/java/google/registry/ui/server/console/domains/ConsoleDomainActionType.java index 47fdd953e..cbf528b31 100644 --- a/core/src/main/java/google/registry/ui/server/console/domains/ConsoleDomainActionType.java +++ b/core/src/main/java/google/registry/ui/server/console/domains/ConsoleDomainActionType.java @@ -18,6 +18,7 @@ import com.google.common.escape.Escaper; import com.google.common.xml.XmlEscapers; import com.google.gson.JsonElement; import google.registry.model.console.ConsolePermission; +import google.registry.model.console.ConsoleUpdateHistory; /** * A type of EPP action to perform on domain(s), run by the {@link ConsoleBulkDomainAction}. @@ -68,6 +69,9 @@ public abstract class ConsoleDomainActionType { /** Returns the permission necessary to successfully perform this action. */ public abstract ConsolePermission getNecessaryPermission(); + /** Returns the type of history / audit logging object to save. */ + public abstract ConsoleUpdateHistory.Type getConsoleUpdateHistoryType(); + /** Returns the XML template contents for this action. */ protected abstract String getXmlTemplate(); 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 d4603e0f4..6f23f7dc7 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 @@ -26,7 +26,7 @@ import google.registry.flows.certs.CertificateChecker; import google.registry.flows.certs.CertificateChecker.InsecureCertificateException; import google.registry.model.console.ConsolePermission; import google.registry.model.console.ConsoleUpdateHistory; -import google.registry.model.console.RegistrarUpdateHistory; +import google.registry.model.console.SimpleConsoleUpdateHistory; import google.registry.model.console.User; import google.registry.model.registrar.Registrar; import google.registry.request.Action; @@ -120,10 +120,9 @@ public class SecurityAction extends ConsoleApiAction { Registrar updatedRegistrar = updatedRegistrarBuilder.build(); tm().put(updatedRegistrar); finishAndPersistConsoleUpdateHistory( - new RegistrarUpdateHistory.Builder() - .setType(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE) - .setRegistrar(updatedRegistrar) - .setRequestBody(consoleApiParams.gson().toJson(registrar.get()))); + new SimpleConsoleUpdateHistory.Builder() + .setType(ConsoleUpdateHistory.Type.REGISTRAR_SECURITY_UPDATE) + .setDescription(registrarId)); sendExternalUpdatesIfNecessary( EmailInfo.create(savedRegistrar, updatedRegistrar, ImmutableSet.of(), ImmutableSet.of())); diff --git a/core/src/main/java/google/registry/ui/server/console/settings/WhoisRegistrarFieldsAction.java b/core/src/main/java/google/registry/ui/server/console/settings/WhoisRegistrarFieldsAction.java index 7c90427d8..356e1e7b5 100644 --- a/core/src/main/java/google/registry/ui/server/console/settings/WhoisRegistrarFieldsAction.java +++ b/core/src/main/java/google/registry/ui/server/console/settings/WhoisRegistrarFieldsAction.java @@ -23,7 +23,7 @@ import static jakarta.servlet.http.HttpServletResponse.SC_OK; import google.registry.model.console.ConsolePermission; import google.registry.model.console.ConsoleUpdateHistory; -import google.registry.model.console.RegistrarUpdateHistory; +import google.registry.model.console.SimpleConsoleUpdateHistory; import google.registry.model.console.User; import google.registry.model.registrar.Registrar; import google.registry.request.Action; @@ -107,10 +107,9 @@ public class WhoisRegistrarFieldsAction extends ConsoleApiAction { .build(); tm().put(newRegistrar); finishAndPersistConsoleUpdateHistory( - new RegistrarUpdateHistory.Builder() + new SimpleConsoleUpdateHistory.Builder() .setType(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE) - .setRegistrar(newRegistrar) - .setRequestBody(consoleApiParams.gson().toJson(registrar.get()))); + .setDescription(newRegistrar.getRegistrarId())); sendExternalUpdatesIfNecessary( EmailInfo.create( savedRegistrar, diff --git a/core/src/main/resources/META-INF/persistence.xml b/core/src/main/resources/META-INF/persistence.xml index 77d690e0a..ee8777230 100644 --- a/core/src/main/resources/META-INF/persistence.xml +++ b/core/src/main/resources/META-INF/persistence.xml @@ -50,6 +50,7 @@ google.registry.model.console.ConsoleEppActionHistory google.registry.model.console.RegistrarPocUpdateHistory google.registry.model.console.RegistrarUpdateHistory + google.registry.model.console.SimpleConsoleUpdateHistory google.registry.model.console.User google.registry.model.console.UserUpdateHistory google.registry.model.contact.ContactHistory diff --git a/core/src/test/java/google/registry/model/console/ConsoleEppActionHistoryTest.java b/core/src/test/java/google/registry/model/console/ConsoleEppActionHistoryTest.java index 53bcdd669..59ce65c94 100644 --- a/core/src/test/java/google/registry/model/console/ConsoleEppActionHistoryTest.java +++ b/core/src/test/java/google/registry/model/console/ConsoleEppActionHistoryTest.java @@ -53,7 +53,7 @@ public class ConsoleEppActionHistoryTest extends EntityTestCase { DomainHistory domainHistory = getOnlyElement(DatabaseHelper.loadAllOf(DomainHistory.class)); ConsoleEppActionHistory history = new ConsoleEppActionHistory.Builder() - .setType(ConsoleUpdateHistory.Type.EPP_ACTION) + .setType(ConsoleUpdateHistory.Type.DOMAIN_DELETE) .setActingUser(user) .setModificationTime(fakeClock.nowUtc()) .setMethod("POST") diff --git a/core/src/test/java/google/registry/model/console/SimpleConsoleUpdateHistoryTest.java b/core/src/test/java/google/registry/model/console/SimpleConsoleUpdateHistoryTest.java new file mode 100644 index 000000000..784d560f5 --- /dev/null +++ b/core/src/test/java/google/registry/model/console/SimpleConsoleUpdateHistoryTest.java @@ -0,0 +1,62 @@ +// 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.model.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.persistActiveContact; +import static google.registry.testing.DatabaseHelper.persistDomainWithDependentResources; +import static google.registry.testing.DatabaseHelper.persistResource; + +import google.registry.model.EntityTestCase; +import google.registry.testing.DatabaseHelper; +import google.registry.util.DateTimeUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class SimpleConsoleUpdateHistoryTest extends EntityTestCase { + SimpleConsoleUpdateHistoryTest() { + super(JpaEntityCoverageCheck.ENABLED); + } + + @BeforeEach + void beforeEach() { + createTld("tld"); + persistDomainWithDependentResources( + "example", + "tld", + persistActiveContact("contact1234"), + fakeClock.nowUtc(), + fakeClock.nowUtc(), + DateTimeUtils.END_OF_TIME); + } + + @Test + void testPersistence() { + User user = persistResource(DatabaseHelper.createAdminUser("email@email.com")); + SimpleConsoleUpdateHistory history = + new SimpleConsoleUpdateHistory.Builder() + .setType(ConsoleUpdateHistory.Type.DOMAIN_SUSPEND) + .setActingUser(user) + .setMethod("POST") + .setUrl("/console-api/bulk-domain") + .setDescription("example.tld") + .setModificationTime(fakeClock.nowUtc()) + .build(); + persistResource(history); + assertThat(loadByEntity(history)).isEqualTo(history); + } +} diff --git a/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java b/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java index ccf1aceeb..527be6b54 100644 --- a/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java +++ b/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java @@ -27,6 +27,7 @@ import google.registry.model.common.FeatureFlagTest; import google.registry.model.console.ConsoleEppActionHistoryTest; import google.registry.model.console.RegistrarPocUpdateHistoryTest; import google.registry.model.console.RegistrarUpdateHistoryTest; +import google.registry.model.console.SimpleConsoleUpdateHistoryTest; import google.registry.model.console.UserTest; import google.registry.model.console.UserUpdateHistoryTest; import google.registry.model.contact.ContactTest; @@ -113,12 +114,13 @@ import org.junit.runner.RunWith; RegistrarDaoTest.class, RegistrarPocUpdateHistoryTest.class, RegistrarUpdateHistoryTest.class, - TldTest.class, ReservedListDaoTest.class, RegistryLockDaoTest.class, ServerSecretTest.class, + SimpleConsoleUpdateHistoryTest.class, SignedMarkRevocationListDaoTest.class, Spec11ThreatMatchTest.class, + TldTest.class, TmchCrlTest.class, UserTest.class, UserUpdateHistoryTest.class, diff --git a/core/src/test/java/google/registry/ui/server/console/ConsoleEppPasswordActionTest.java b/core/src/test/java/google/registry/ui/server/console/ConsoleEppPasswordActionTest.java index 89ed1fa63..49737f9ba 100644 --- a/core/src/test/java/google/registry/ui/server/console/ConsoleEppPasswordActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/ConsoleEppPasswordActionTest.java @@ -32,8 +32,9 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSetMultimap; import com.google.gson.Gson; import google.registry.flows.PasswordOnlyTransportCredentials; +import google.registry.model.console.ConsoleUpdateHistory; import google.registry.model.console.GlobalRole; -import google.registry.model.console.RegistrarUpdateHistory; +import google.registry.model.console.SimpleConsoleUpdateHistory; import google.registry.model.console.User; import google.registry.model.console.UserRoles; import google.registry.model.registrar.Registrar; @@ -138,14 +139,10 @@ class ConsoleEppPasswordActionTest { createAction("TheRegistrar", "foobar", "randomPassword", "randomPassword"); action.run(); assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK); - assertDoesNotThrow( - () -> { - credentials.validate(loadRegistrar("TheRegistrar"), "randomPassword"); - }); - assertThat(loadSingleton(RegistrarUpdateHistory.class).get().getRequestBody()) - .isEqualTo( - "{\"registrarId\":\"TheRegistrar\",\"oldPassword\":\"********\",\"newPassword\":" - + "\"••••••••\",\"newPasswordRepeat\":\"••••••••\"}"); + assertDoesNotThrow(() -> credentials.validate(loadRegistrar("TheRegistrar"), "randomPassword")); + SimpleConsoleUpdateHistory history = loadSingleton(SimpleConsoleUpdateHistory.class).get(); + assertThat(history.getType()).isEqualTo(ConsoleUpdateHistory.Type.EPP_PASSWORD_UPDATE); + assertThat(history.getDescription()).hasValue("TheRegistrar"); } private ConsoleEppPasswordAction createAction( diff --git a/core/src/test/java/google/registry/ui/server/console/ConsoleUpdateRegistrarActionTest.java b/core/src/test/java/google/registry/ui/server/console/ConsoleUpdateRegistrarActionTest.java index 456e3d5f3..e2361d955 100644 --- a/core/src/test/java/google/registry/ui/server/console/ConsoleUpdateRegistrarActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/ConsoleUpdateRegistrarActionTest.java @@ -15,7 +15,6 @@ package google.registry.ui.server.console; import static com.google.common.truth.Truth.assertThat; -import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects; import static google.registry.model.registrar.RegistrarPocBase.Type.WHOIS; import static google.registry.testing.DatabaseHelper.createTlds; import static google.registry.testing.DatabaseHelper.loadSingleton; @@ -30,8 +29,9 @@ import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.gson.Gson; +import google.registry.model.console.ConsoleUpdateHistory; import google.registry.model.console.GlobalRole; -import google.registry.model.console.RegistrarUpdateHistory; +import google.registry.model.console.SimpleConsoleUpdateHistory; import google.registry.model.console.User; import google.registry.model.console.UserRoles; import google.registry.model.registrar.Registrar; @@ -108,9 +108,9 @@ class ConsoleUpdateRegistrarActionTest { assertThat(newRegistrar.getAllowedTlds()).containsExactly("app", "dev"); assertThat(newRegistrar.isRegistryLockAllowed()).isFalse(); assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK); - assertAboutImmutableObjects() - .that(newRegistrar) - .hasFieldsEqualTo(loadSingleton(RegistrarUpdateHistory.class).get().getRegistrar()); + SimpleConsoleUpdateHistory history = loadSingleton(SimpleConsoleUpdateHistory.class).get(); + assertThat(history.getType()).isEqualTo(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE); + assertThat(history.getDescription()).hasValue("TheRegistrar"); } @Test diff --git a/core/src/test/java/google/registry/ui/server/console/RegistrarsActionTest.java b/core/src/test/java/google/registry/ui/server/console/RegistrarsActionTest.java index d5ae005d2..c6f040487 100644 --- a/core/src/test/java/google/registry/ui/server/console/RegistrarsActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/RegistrarsActionTest.java @@ -15,7 +15,6 @@ package google.registry.ui.server.console; import static com.google.common.truth.Truth.assertThat; -import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects; import static google.registry.testing.DatabaseHelper.loadAllOf; import static google.registry.testing.DatabaseHelper.loadRegistrar; import static google.registry.testing.DatabaseHelper.loadSingleton; @@ -30,9 +29,10 @@ 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.GlobalRole; import google.registry.model.console.RegistrarRole; -import google.registry.model.console.RegistrarUpdateHistory; +import google.registry.model.console.SimpleConsoleUpdateHistory; import google.registry.model.console.User; import google.registry.model.console.UserRoles; import google.registry.model.registrar.Registrar; @@ -186,9 +186,9 @@ class RegistrarsActionTest { .findAny() .isPresent()) .isTrue(); - assertAboutImmutableObjects() - .that(r) - .isEqualExceptFields(loadSingleton(RegistrarUpdateHistory.class).get().getRegistrar()); + SimpleConsoleUpdateHistory history = loadSingleton(SimpleConsoleUpdateHistory.class).get(); + assertThat(history.getType()).isEqualTo(ConsoleUpdateHistory.Type.REGISTRAR_CREATE); + assertThat(history.getDescription()).hasValue("regIdTest"); } @Test diff --git a/core/src/test/java/google/registry/ui/server/console/domains/ConsoleBulkDomainActionTest.java b/core/src/test/java/google/registry/ui/server/console/domains/ConsoleBulkDomainActionTest.java index e808d948e..69df63afd 100644 --- a/core/src/test/java/google/registry/ui/server/console/domains/ConsoleBulkDomainActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/domains/ConsoleBulkDomainActionTest.java @@ -19,6 +19,7 @@ import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATAS import static google.registry.model.common.FeatureFlag.FeatureStatus.INACTIVE; import static google.registry.testing.DatabaseHelper.createTld; import static google.registry.testing.DatabaseHelper.loadByEntity; +import static google.registry.testing.DatabaseHelper.loadSingleton; import static google.registry.testing.DatabaseHelper.persistActiveContact; import static google.registry.testing.DatabaseHelper.persistDomainWithDependentResources; import static google.registry.testing.DatabaseHelper.persistResource; @@ -38,8 +39,10 @@ import google.registry.flows.DaggerEppTestComponent; import google.registry.flows.EppController; import google.registry.flows.EppTestComponent; import google.registry.model.common.FeatureFlag; +import google.registry.model.console.ConsoleUpdateHistory; import google.registry.model.console.GlobalRole; import google.registry.model.console.RegistrarRole; +import google.registry.model.console.SimpleConsoleUpdateHistory; import google.registry.model.console.User; import google.registry.model.console.UserRoles; import google.registry.model.domain.Domain; @@ -120,6 +123,9 @@ public class ConsoleBulkDomainActionTest { {"example.tld":{"message":"Command completed successfully; action pending",\ "responseCode":1001}}"""); assertThat(loadByEntity(domain).getDeletionTime()).isEqualTo(clock.nowUtc().plusDays(35)); + SimpleConsoleUpdateHistory history = loadSingleton(SimpleConsoleUpdateHistory.class).get(); + assertThat(history.getType()).isEqualTo(ConsoleUpdateHistory.Type.DOMAIN_DELETE); + assertThat(history.getDescription()).hasValue("example.tld"); } @Test @@ -145,6 +151,9 @@ public class ConsoleBulkDomainActionTest { {"example.tld":{"message":"Command completed successfully","responseCode":1000}}"""); assertThat(loadByEntity(domain).getStatusValues()) .containsAtLeastElementsIn(serverSuspensionStatuses); + SimpleConsoleUpdateHistory history = loadSingleton(SimpleConsoleUpdateHistory.class).get(); + assertThat(history.getType()).isEqualTo(ConsoleUpdateHistory.Type.DOMAIN_SUSPEND); + assertThat(history.getDescription()).hasValue("example.tld"); } @Test @@ -172,6 +181,9 @@ public class ConsoleBulkDomainActionTest { """ {"example.tld":{"message":"Command completed successfully","responseCode":1000}}"""); assertThat(loadByEntity(domain).getStatusValues()).containsNoneIn(serverSuspensionStatuses); + SimpleConsoleUpdateHistory history = loadSingleton(SimpleConsoleUpdateHistory.class).get(); + assertThat(history.getType()).isEqualTo(ConsoleUpdateHistory.Type.DOMAIN_UNSUSPEND); + assertThat(history.getDescription()).hasValue("example.tld"); } @Test @@ -194,6 +206,9 @@ public class ConsoleBulkDomainActionTest { "nonexistent.tld":{"message":"The domain with given ID (nonexistent.tld) doesn\\u0027t exist.",\ "responseCode":2303}}"""); assertThat(loadByEntity(domain).getDeletionTime()).isEqualTo(clock.nowUtc().plusDays(35)); + SimpleConsoleUpdateHistory history = loadSingleton(SimpleConsoleUpdateHistory.class).get(); + assertThat(history.getType()).isEqualTo(ConsoleUpdateHistory.Type.DOMAIN_DELETE); + assertThat(history.getDescription()).hasValue("example.tld"); } @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 e7bda7b48..11d827582 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 @@ -15,7 +15,6 @@ package google.registry.ui.server.console.settings; import static com.google.common.truth.Truth.assertThat; -import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects; import static google.registry.testing.CertificateSamples.SAMPLE_CERT2; import static google.registry.testing.DatabaseHelper.loadRegistrar; import static google.registry.testing.DatabaseHelper.loadSingleton; @@ -30,7 +29,8 @@ import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.ImmutableSortedMap; import com.google.gson.Gson; import google.registry.flows.certs.CertificateChecker; -import google.registry.model.console.RegistrarUpdateHistory; +import google.registry.model.console.ConsoleUpdateHistory; +import google.registry.model.console.SimpleConsoleUpdateHistory; import google.registry.model.registrar.Registrar; import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.request.Action; @@ -101,9 +101,9 @@ class SecurityActionTest { .isEqualTo("GNd6ZP8/n91t9UTnpxR8aH7aAW4+CpvufYx9ViGbcMY"); assertThat(r.getIpAddressAllowList().get(0).getIp()).isEqualTo("192.168.1.1"); assertThat(r.getIpAddressAllowList().get(0).getNetmask()).isEqualTo(32); - assertAboutImmutableObjects() - .that(loadSingleton(RegistrarUpdateHistory.class).get().getRegistrar()) - .hasFieldsEqualTo(r); + SimpleConsoleUpdateHistory history = loadSingleton(SimpleConsoleUpdateHistory.class).get(); + assertThat(history.getType()).isEqualTo(ConsoleUpdateHistory.Type.REGISTRAR_SECURITY_UPDATE); + assertThat(history.getDescription()).hasValue("registrarId"); } private SecurityAction createAction(AuthResult authResult, String registrarId) diff --git a/core/src/test/java/google/registry/ui/server/console/settings/WhoisRegistrarFieldsActionTest.java b/core/src/test/java/google/registry/ui/server/console/settings/WhoisRegistrarFieldsActionTest.java index 298f68cf9..200f65d89 100644 --- a/core/src/test/java/google/registry/ui/server/console/settings/WhoisRegistrarFieldsActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/settings/WhoisRegistrarFieldsActionTest.java @@ -27,8 +27,9 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.Maps; import com.google.gson.Gson; +import google.registry.model.console.ConsoleUpdateHistory; import google.registry.model.console.RegistrarRole; -import google.registry.model.console.RegistrarUpdateHistory; +import google.registry.model.console.SimpleConsoleUpdateHistory; import google.registry.model.console.User; import google.registry.model.console.UserRoles; import google.registry.model.registrar.Registrar; @@ -131,9 +132,9 @@ public class WhoisRegistrarFieldsActionTest { .that(newRegistrar) .isEqualExceptFields( oldRegistrar, "whoisServer", "url", "localizedAddress", "phoneNumber", "faxNumber"); - assertAboutImmutableObjects() - .that(loadSingleton(RegistrarUpdateHistory.class).get().getRegistrar()) - .hasFieldsEqualTo(newRegistrar); + SimpleConsoleUpdateHistory history = loadSingleton(SimpleConsoleUpdateHistory.class).get(); + assertThat(history.getType()).isEqualTo(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE); + assertThat(history.getDescription()).hasValue("TheRegistrar"); } @Test diff --git a/db/src/main/resources/sql/er_diagram/brief_er_diagram.html b/db/src/main/resources/sql/er_diagram/brief_er_diagram.html index 4cb28c0c9..91f21d391 100644 --- a/db/src/main/resources/sql/er_diagram/brief_er_diagram.html +++ b/db/src/main/resources/sql/er_diagram/brief_er_diagram.html @@ -261,7 +261,7 @@ td.section { generated on - 2025-03-07 18:24:45 + 2025-03-10 15:53:40 last flyway file @@ -280,7 +280,7 @@ td.section { generated by SchemaCrawler 16.25.2 generated on - 2025-03-07 18:24:45 + 2025-03-10 15:53:40 diff --git a/db/src/main/resources/sql/er_diagram/full_er_diagram.html b/db/src/main/resources/sql/er_diagram/full_er_diagram.html index 575c0c7c4..d947102f1 100644 --- a/db/src/main/resources/sql/er_diagram/full_er_diagram.html +++ b/db/src/main/resources/sql/er_diagram/full_er_diagram.html @@ -261,7 +261,7 @@ td.section { </tr> <tr> <td class="property_name">generated on</td> - <td class="property_value">2025-03-07 18:24:42</td> + <td class="property_value">2025-03-10 15:53:36</td> </tr> <tr> <td class="property_name">last flyway file</td> @@ -280,7 +280,7 @@ td.section { <text text-anchor="start" x="5321" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">generated by</text> <text text-anchor="start" x="5404" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">SchemaCrawler 16.25.2</text> <text text-anchor="start" x="5320" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">generated on</text> - <text text-anchor="start" x="5404" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">2025-03-07 18:24:42</text> + <text text-anchor="start" x="5404" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">2025-03-10 15:53:36</text> <polygon fill="none" stroke="#888888" points="5317,-4 5317,-44 5553,-44 5553,-4 5317,-4" /> <!-- allocationtoken_a08ccbef --> <g id="node1" class="node"> <title> 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 f887e8b32..9b1706df8 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 @@ history_method text not null, history_modification_time timestamp(6) with time zone not null, history_request_body text, - history_type text not null check (history_type in ('EPP_ACTION','POC_CREATE','POC_UPDATE','POC_DELETE','REGISTRAR_UPDATE','USER_CREATE','USER_DELETE','USER_UPDATE')), + history_type text not null check (history_type in ('DOMAIN_DELETE','DOMAIN_SUSPEND','DOMAIN_UNSUSPEND','EPP_PASSWORD_UPDATE','REGISTRAR_CREATE','REGISTRAR_SECURITY_UPDATE','REGISTRAR_UPDATE','USER_CREATE','USER_DELETE','USER_UPDATE')), history_url text not null, history_entry_class text not null, repo_id text not null, @@ -148,6 +148,17 @@ primary key (history_revision_id) ); + create table "ConsoleUpdateHistory" ( + revision_id bigint not null, + 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')), + url text not null, + acting_user text not null, + primary key (revision_id) + ); + create table "Contact" ( repo_id text not null, update_timestamp timestamp(6) with time zone, @@ -693,7 +704,7 @@ history_method text not null, history_modification_time timestamp(6) with time zone not null, history_request_body text, - history_type text not null check (history_type in ('EPP_ACTION','POC_CREATE','POC_UPDATE','POC_DELETE','REGISTRAR_UPDATE','USER_CREATE','USER_DELETE','USER_UPDATE')), + history_type text not null check (history_type in ('DOMAIN_DELETE','DOMAIN_SUSPEND','DOMAIN_UNSUSPEND','EPP_PASSWORD_UPDATE','REGISTRAR_CREATE','REGISTRAR_SECURITY_UPDATE','REGISTRAR_UPDATE','USER_CREATE','USER_DELETE','USER_UPDATE')), history_url text not null, email_address text not null, registrar_id text not null, @@ -717,7 +728,7 @@ history_method text not null, history_modification_time timestamp(6) with time zone not null, history_request_body text, - history_type text not null check (history_type in ('EPP_ACTION','POC_CREATE','POC_UPDATE','POC_DELETE','REGISTRAR_UPDATE','USER_CREATE','USER_DELETE','USER_UPDATE')), + history_type text not null check (history_type in ('DOMAIN_DELETE','DOMAIN_SUSPEND','DOMAIN_UNSUSPEND','EPP_PASSWORD_UPDATE','REGISTRAR_CREATE','REGISTRAR_SECURITY_UPDATE','REGISTRAR_UPDATE','USER_CREATE','USER_DELETE','USER_UPDATE')), history_url text not null, allowed_tlds text[], billing_account_map hstore, @@ -906,7 +917,7 @@ history_method text not null, history_modification_time timestamp(6) with time zone not null, history_request_body text, - history_type text not null check (history_type in ('EPP_ACTION','POC_CREATE','POC_UPDATE','POC_DELETE','REGISTRAR_UPDATE','USER_CREATE','USER_DELETE','USER_UPDATE')), + history_type text not null check (history_type in ('DOMAIN_DELETE','DOMAIN_SUSPEND','DOMAIN_UNSUSPEND','EPP_PASSWORD_UPDATE','REGISTRAR_CREATE','REGISTRAR_SECURITY_UPDATE','REGISTRAR_UPDATE','USER_CREATE','USER_DELETE','USER_UPDATE')), history_url text not null, email_address text not null, registry_lock_email_address text, @@ -1010,6 +1021,15 @@ create index IDXiahqo1d1fqdfknywmj2xbxl7t on "ConsoleEppActionHistory" (revision_id); + create index idx_console_update_history_acting_user + on "ConsoleUpdateHistory" (acting_user); + + create index idx_console_update_history_type + on "ConsoleUpdateHistory" (type); + + create index idx_console_update_history_modification_time + on "ConsoleUpdateHistory" (modification_time); + create index IDX3y752kr9uh4kh6uig54vemx0l on "Contact" (creation_time); @@ -1228,6 +1248,11 @@ foreign key (history_acting_user) references "User"; + alter table if exists "ConsoleUpdateHistory" + add constraint FKnhl1eolgix64u90xv3pj6xa3x + foreign key (acting_user) + references "User"; + alter table if exists "DelegationSignerData" add constraint FKtr24j9v14ph2mfuw2gsmt12kq foreign key (domain_repo_id)