From 03b358726a0dfc336f7262e6e132d9c0cdcca870 Mon Sep 17 00:00:00 2001 From: gbrodman Date: Tue, 30 Apr 2024 16:42:40 -0400 Subject: [PATCH] Add Java classes for console history objects (#2350) This also creates base classes for the objects contained within the history classes, e.g. RegistrarBase. This is the same way that objects stored in the HistoryEntry subclasses have base classes, e.g. DomainBase. --- ...ingCertificateNotificationEmailAction.java | 2 +- .../google/registry/beam/rde/RdePipeline.java | 2 +- .../registry/dns/PublishDnsUpdatesAction.java | 3 +- .../export/SyncGroupMembersAction.java | 5 +- .../export/sheet/SyncRegistrarsSheet.java | 17 +- .../flows/domain/DomainFlowUtils.java | 2 +- .../console/ConsoleEppActionHistory.java | 99 ++ .../model/console/ConsoleUpdateHistory.java | 168 +++ .../console/RegistrarPocUpdateHistory.java | 99 ++ .../model/console/RegistrarUpdateHistory.java | 89 ++ .../google/registry/model/console/User.java | 119 +- .../registry/model/console/UserBase.java | 178 +++ .../registry/model/console/UserRoles.java | 3 + .../model/console/UserUpdateHistory.java | 86 ++ .../registry/model/registrar/Registrar.java | 978 +--------------- .../model/registrar/RegistrarBase.java | 1028 +++++++++++++++++ .../model/registrar/RegistrarPoc.java | 402 +------ .../model/registrar/RegistrarPocBase.java | 441 +++++++ .../converter/RegistrarPocSetConverter.java | 2 +- .../registry/rde/RegistrarToXjcConverter.java | 5 +- .../auth/AuthenticatedRegistrarAccessor.java | 2 +- .../tools/CreateOrUpdateRegistrarCommand.java | 13 +- .../tools/CreateRegistrarCommand.java | 2 +- .../registry/tools/RegistrarPocCommand.java | 5 +- .../tools/server/CreateGroupsAction.java | 4 +- .../ui/server/RegistrarFormFields.java | 19 +- .../ui/server/console/RegistrarsAction.java | 2 +- .../ConsoleRegistrarCreatorAction.java | 2 +- .../ui/server/registrar/OteStatusAction.java | 2 +- .../registrar/RegistrarSettingsAction.java | 2 +- .../main/resources/META-INF/persistence.xml | 4 + ...ertificateNotificationEmailActionTest.java | 17 +- .../registry/beam/rde/RdePipelineTest.java | 2 +- .../export/SyncGroupMembersActionTest.java | 6 +- .../export/sheet/SyncRegistrarsSheetTest.java | 8 +- .../flows/domain/DomainCreateFlowTest.java | 2 +- .../flows/domain/DomainRenewFlowTest.java | 2 +- .../domain/DomainRestoreRequestFlowTest.java | 2 +- .../domain/DomainTransferRequestFlowTest.java | 2 +- .../flows/session/LoginFlowTestCase.java | 2 +- .../console/ConsoleEppActionHistoryTest.java | 68 ++ .../RegistrarPocUpdateHistoryTest.java | 61 + .../console/RegistrarUpdateHistoryTest.java | 55 + .../registry/model/console/UserTest.java | 9 +- .../model/console/UserUpdateHistoryTest.java | 65 ++ .../model/registrar/RegistrarTest.java | 17 +- .../converter/StringValueEnumeratedTest.java | 2 +- .../JpaTransactionManagerExtension.java | 9 +- .../registry/rdap/RdapJsonFormatterTest.java | 9 +- ...UpdateRegistrarRdapBaseUrlsActionTest.java | 3 +- .../rde/RegistrarToXjcConverterTest.java | 2 +- .../AuthenticatedRegistrarAccessorTest.java | 2 +- .../OidcTokenAuthenticationMechanismTest.java | 10 +- .../integration/SqlIntegrationTestSuite.java | 8 + .../schema/registrar/RegistrarPocTest.java | 2 +- .../registry/testing/DatabaseHelper.java | 25 +- .../testing/FullFieldsTestEntityHelper.java | 10 +- .../tools/CheckDomainClaimsCommandTest.java | 2 +- .../tools/CheckDomainCommandTest.java | 2 +- .../registry/tools/DeleteUserCommandTest.java | 12 +- .../registry/tools/LockDomainCommandTest.java | 2 +- .../tools/RegistrarPocCommandTest.java | 8 +- .../registry/tools/SetupOteCommandTest.java | 2 +- .../tools/UnlockDomainCommandTest.java | 2 +- .../tools/UpdateRegistrarCommandTest.java | 4 +- .../ValidateLoginCredentialsCommandTest.java | 4 +- .../console/ConsoleDomainListActionTest.java | 11 +- .../console/ConsoleUserDataActionTest.java | 19 +- .../console/settings/ContactActionTest.java | 47 +- .../console/settings/SecurityActionTest.java | 15 +- .../WhoisRegistrarFieldsActionTest.java | 7 +- .../server/registrar/ContactSettingsTest.java | 2 +- .../server/registrar/OteStatusActionTest.java | 2 +- .../RegistrarSettingsActionTestCase.java | 3 +- .../RegistrarConsoleScreenshotTest.java | 2 +- .../whois/RegistrarWhoisResponseTest.java | 11 +- .../registry/whois/WhoisActionTest.java | 4 +- .../registry/whois/WhoisHttpActionTest.java | 2 +- .../sql/er_diagram/brief_er_diagram.html | 4 +- .../sql/er_diagram/full_er_diagram.html | 4 +- .../sql/schema/db-schema.sql.generated | 148 +++ 81 files changed, 2832 insertions(+), 1672 deletions(-) create mode 100644 core/src/main/java/google/registry/model/console/ConsoleEppActionHistory.java create mode 100644 core/src/main/java/google/registry/model/console/ConsoleUpdateHistory.java create mode 100644 core/src/main/java/google/registry/model/console/RegistrarPocUpdateHistory.java create mode 100644 core/src/main/java/google/registry/model/console/RegistrarUpdateHistory.java create mode 100644 core/src/main/java/google/registry/model/console/UserBase.java create mode 100644 core/src/main/java/google/registry/model/console/UserUpdateHistory.java create mode 100644 core/src/main/java/google/registry/model/registrar/RegistrarBase.java create mode 100644 core/src/main/java/google/registry/model/registrar/RegistrarPocBase.java create mode 100644 core/src/test/java/google/registry/model/console/ConsoleEppActionHistoryTest.java create mode 100644 core/src/test/java/google/registry/model/console/RegistrarPocUpdateHistoryTest.java create mode 100644 core/src/test/java/google/registry/model/console/RegistrarUpdateHistoryTest.java create mode 100644 core/src/test/java/google/registry/model/console/UserUpdateHistoryTest.java diff --git a/core/src/main/java/google/registry/batch/SendExpiringCertificateNotificationEmailAction.java b/core/src/main/java/google/registry/batch/SendExpiringCertificateNotificationEmailAction.java index eaffcaffa..c76119978 100644 --- a/core/src/main/java/google/registry/batch/SendExpiringCertificateNotificationEmailAction.java +++ b/core/src/main/java/google/registry/batch/SendExpiringCertificateNotificationEmailAction.java @@ -33,7 +33,7 @@ import google.registry.flows.certs.CertificateChecker; import google.registry.groups.GmailClient; import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarPoc; -import google.registry.model.registrar.RegistrarPoc.Type; +import google.registry.model.registrar.RegistrarPocBase.Type; import google.registry.request.Action; import google.registry.request.Response; import google.registry.request.auth.Auth; diff --git a/core/src/main/java/google/registry/beam/rde/RdePipeline.java b/core/src/main/java/google/registry/beam/rde/RdePipeline.java index a016e7228..49a628a49 100644 --- a/core/src/main/java/google/registry/beam/rde/RdePipeline.java +++ b/core/src/main/java/google/registry/beam/rde/RdePipeline.java @@ -58,7 +58,7 @@ import google.registry.model.host.Host; import google.registry.model.host.HostHistory; import google.registry.model.rde.RdeMode; import google.registry.model.registrar.Registrar; -import google.registry.model.registrar.Registrar.Type; +import google.registry.model.registrar.RegistrarBase.Type; import google.registry.model.reporting.HistoryEntry; import google.registry.model.reporting.HistoryEntry.HistoryEntryId; import google.registry.persistence.PersistenceModule.TransactionIsolationLevel; diff --git a/core/src/main/java/google/registry/dns/PublishDnsUpdatesAction.java b/core/src/main/java/google/registry/dns/PublishDnsUpdatesAction.java index f0fbb97f3..49e11aff9 100644 --- a/core/src/main/java/google/registry/dns/PublishDnsUpdatesAction.java +++ b/core/src/main/java/google/registry/dns/PublishDnsUpdatesAction.java @@ -50,6 +50,7 @@ import google.registry.model.domain.Domain; import google.registry.model.host.Host; import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarPoc; +import google.registry.model.registrar.RegistrarPocBase; import google.registry.model.tld.Tld; import google.registry.request.Action; import google.registry.request.Action.Service; @@ -295,7 +296,7 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable { ImmutableList recipients = registrar.get().getContacts().stream() - .filter(c -> c.getTypes().contains(RegistrarPoc.Type.ADMIN)) + .filter(c -> c.getTypes().contains(RegistrarPocBase.Type.ADMIN)) .map(RegistrarPoc::getEmailAddress) .map(PublishDnsUpdatesAction::emailToInternetAddress) .collect(toImmutableList()); diff --git a/core/src/main/java/google/registry/export/SyncGroupMembersAction.java b/core/src/main/java/google/registry/export/SyncGroupMembersAction.java index 4db648665..326d88775 100644 --- a/core/src/main/java/google/registry/export/SyncGroupMembersAction.java +++ b/core/src/main/java/google/registry/export/SyncGroupMembersAction.java @@ -33,6 +33,7 @@ import google.registry.groups.GroupsConnection; import google.registry.groups.GroupsConnection.Role; import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarPoc; +import google.registry.model.registrar.RegistrarPocBase; import google.registry.request.Action; import google.registry.request.Response; import google.registry.request.auth.Auth; @@ -99,7 +100,7 @@ public final class SyncGroupMembersAction implements Runnable { * Returns the Google Groups email address for the given registrar ID and RegistrarContact.Type. */ public static String getGroupEmailAddressForContactType( - String registrarId, RegistrarPoc.Type type, String gSuiteDomainName) { + String registrarId, RegistrarPocBase.Type type, String gSuiteDomainName) { // Take the registrar's ID, make it lowercase, and remove all characters that aren't // alphanumeric, hyphens, or underscores. return String.format( @@ -174,7 +175,7 @@ public final class SyncGroupMembersAction implements Runnable { Set registrarPocs = registrar.getContacts(); long totalAdded = 0; long totalRemoved = 0; - for (final RegistrarPoc.Type type : RegistrarPoc.Type.values()) { + for (final RegistrarPocBase.Type type : RegistrarPocBase.Type.values()) { groupKey = getGroupEmailAddressForContactType(registrar.getRegistrarId(), type, gSuiteDomainName); Set currentMembers = groupsConnection.getMembersOfGroup(groupKey); diff --git a/core/src/main/java/google/registry/export/sheet/SyncRegistrarsSheet.java b/core/src/main/java/google/registry/export/sheet/SyncRegistrarsSheet.java index 1dd9bf616..86fc00ab5 100644 --- a/core/src/main/java/google/registry/export/sheet/SyncRegistrarsSheet.java +++ b/core/src/main/java/google/registry/export/sheet/SyncRegistrarsSheet.java @@ -17,13 +17,13 @@ package google.registry.export.sheet; import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.collect.ImmutableList.toImmutableList; import static google.registry.model.common.Cursor.CursorType.SYNC_REGISTRAR_SHEET; -import static google.registry.model.registrar.RegistrarPoc.Type.ABUSE; -import static google.registry.model.registrar.RegistrarPoc.Type.ADMIN; -import static google.registry.model.registrar.RegistrarPoc.Type.BILLING; -import static google.registry.model.registrar.RegistrarPoc.Type.LEGAL; -import static google.registry.model.registrar.RegistrarPoc.Type.MARKETING; -import static google.registry.model.registrar.RegistrarPoc.Type.TECH; -import static google.registry.model.registrar.RegistrarPoc.Type.WHOIS; +import static google.registry.model.registrar.RegistrarPocBase.Type.ABUSE; +import static google.registry.model.registrar.RegistrarPocBase.Type.ADMIN; +import static google.registry.model.registrar.RegistrarPocBase.Type.BILLING; +import static google.registry.model.registrar.RegistrarPocBase.Type.LEGAL; +import static google.registry.model.registrar.RegistrarPocBase.Type.MARKETING; +import static google.registry.model.registrar.RegistrarPocBase.Type.TECH; +import static google.registry.model.registrar.RegistrarPocBase.Type.WHOIS; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.util.DateTimeUtils.START_OF_TIME; @@ -36,6 +36,7 @@ import google.registry.model.common.Cursor; import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarAddress; import google.registry.model.registrar.RegistrarPoc; +import google.registry.model.registrar.RegistrarPocBase; import google.registry.util.Clock; import google.registry.util.DateTimeUtils; import java.io.IOException; @@ -173,7 +174,7 @@ class SyncRegistrarsSheet { return result.toString(); } - private static Predicate byType(final RegistrarPoc.Type type) { + private static Predicate byType(final RegistrarPocBase.Type type) { return contact -> contact.getTypes().contains(type); } diff --git a/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java b/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java index d330c1454..12072ee97 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java +++ b/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java @@ -123,7 +123,7 @@ import google.registry.model.eppoutput.EppResponse.ResponseExtension; import google.registry.model.host.Host; import google.registry.model.poll.PollMessage; import google.registry.model.registrar.Registrar; -import google.registry.model.registrar.Registrar.State; +import google.registry.model.registrar.RegistrarBase.State; import google.registry.model.reporting.DomainTransactionRecord; import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField; import google.registry.model.reporting.HistoryEntry.HistoryEntryId; diff --git a/core/src/main/java/google/registry/model/console/ConsoleEppActionHistory.java b/core/src/main/java/google/registry/model/console/ConsoleEppActionHistory.java new file mode 100644 index 000000000..e0140d4f8 --- /dev/null +++ b/core/src/main/java/google/registry/model/console/ConsoleEppActionHistory.java @@ -0,0 +1,99 @@ +// 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.model.console; + +import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; + +import google.registry.model.reporting.HistoryEntry; +import google.registry.model.reporting.HistoryEntry.HistoryEntryId; +import google.registry.persistence.VKey; +import javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.AttributeOverride; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.Table; + +/** + * A persisted history object representing an EPP action via the console. + * + *

In addition to the generic history fields (time, URL, etc.) we also persist a reference to the + * history entry so that we can refer to it if necessary. + */ +@Access(AccessType.FIELD) +@Entity +@Table( + indexes = { + @Index(columnList = "historyActingUser"), + @Index(columnList = "repoId"), + @Index(columnList = "revisionId") + }) +public class ConsoleEppActionHistory extends ConsoleUpdateHistory { + + @AttributeOverride(name = "repoId", column = @Column(nullable = false)) + HistoryEntryId historyEntryId; + + @Column(nullable = false) + Class historyEntryClass; + + public HistoryEntryId getHistoryEntryId() { + return historyEntryId; + } + + public Class getHistoryEntryClass() { + return historyEntryClass; + } + + /** Creates a {@link VKey} instance for this entity. */ + @Override + public VKey createVKey() { + return VKey.create(ConsoleEppActionHistory.class, getRevisionId()); + } + + @Override + public Builder asBuilder() { + return new Builder(clone(this)); + } + + /** Builder for the immutable UserUpdateHistory. */ + public static class Builder + extends ConsoleUpdateHistory.Builder { + + public Builder() {} + + public Builder(ConsoleEppActionHistory instance) { + super(instance); + } + + @Override + public ConsoleEppActionHistory build() { + checkArgumentNotNull(getInstance().historyEntryId, "History entry ID must be specified"); + checkArgumentNotNull( + getInstance().historyEntryClass, "History entry class must be specified"); + return super.build(); + } + + public Builder setHistoryEntryId(HistoryEntryId historyEntryId) { + getInstance().historyEntryId = historyEntryId; + return this; + } + + public Builder setHistoryEntryClass(Class historyEntryClass) { + getInstance().historyEntryClass = historyEntryClass; + return this; + } + } +} diff --git a/core/src/main/java/google/registry/model/console/ConsoleUpdateHistory.java b/core/src/main/java/google/registry/model/console/ConsoleUpdateHistory.java new file mode 100644 index 000000000..89218d44a --- /dev/null +++ b/core/src/main/java/google/registry/model/console/ConsoleUpdateHistory.java @@ -0,0 +1,168 @@ +// 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.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 javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.Column; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.MappedSuperclass; +import org.joda.time.DateTime; + +/** + * A record of a resource that was updated through the console. + * + *

This abstract class has several subclasses that (mostly) include the modified resource itself + * so that the entire object history is persisted to SQL. + */ +@Access(AccessType.FIELD) +@MappedSuperclass +public abstract class ConsoleUpdateHistory extends ImmutableObject implements Buildable { + + public enum Type { + EPP_ACTION, + POC_CREATE, + POC_UPDATE, + POC_DELETE, + REGISTRAR_UPDATE, + USER_CREATE, + USER_DELETE, + USER_UPDATE + } + + /** Autogenerated ID of this event. */ + @Id + @IdAllocation + @Column(nullable = false, name = "historyRevisionId") + protected Long revisionId; + + /** The user that performed the modification. */ + @JoinColumn(name = "historyActingUser", referencedColumnName = "emailAddress", nullable = false) + @ManyToOne + User actingUser; + + /** The URL of the action that was used to make the modification. */ + @Column(nullable = false, name = "historyUrl") + String url; + + /** The HTTP method (e.g. POST, PUT) used to make this modification. */ + @Column(nullable = false, name = "historyMethod") + String method; + + /** The raw body of the request that was used to make this modification. */ + @Column(name = "historyRequestBody") + String requestBody; + + /** The time at which the modification was mode. */ + @Column(nullable = false, name = "historyModificationTime") + DateTime modificationTime; + + /** The type of modification. */ + @Column(nullable = false, name = "historyType") + @Enumerated(EnumType.STRING) + Type type; + + public long getRevisionId() { + return revisionId; + } + + public User getActingUser() { + return actingUser; + } + + public String getUrl() { + return url; + } + + public String getMethod() { + return method; + } + + public String getRequestBody() { + return requestBody; + } + + public DateTime getModificationTime() { + return modificationTime; + } + + public Type getType() { + return type; + } + + @Override + public abstract Builder asBuilder(); + + /** Builder for the immutable ConsoleUpdateHistory. */ + public abstract static class Builder< + T extends ConsoleUpdateHistory, B extends ConsoleUpdateHistory.Builder> + extends GenericBuilder { + + protected Builder() {} + + protected Builder(T instance) { + super(instance); + } + + @Override + public T build() { + 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().modificationTime, "modificationTime must be specified"); + checkArgumentNotNull(getInstance().type, "Console History type must be specified"); + return super.build(); + } + + public B setActingUser(User actingUser) { + getInstance().actingUser = actingUser; + return thisCastToDerived(); + } + + public B setUrl(String url) { + getInstance().url = url; + return thisCastToDerived(); + } + + public B setMethod(String method) { + getInstance().method = method; + return thisCastToDerived(); + } + + public B setRequestBody(String requestBody) { + getInstance().requestBody = requestBody; + return thisCastToDerived(); + } + + public B setModificationTime(DateTime modificationTime) { + getInstance().modificationTime = modificationTime; + return thisCastToDerived(); + } + + public B setType(Type type) { + getInstance().type = type; + return thisCastToDerived(); + } + } +} diff --git a/core/src/main/java/google/registry/model/console/RegistrarPocUpdateHistory.java b/core/src/main/java/google/registry/model/console/RegistrarPocUpdateHistory.java new file mode 100644 index 000000000..70a863138 --- /dev/null +++ b/core/src/main/java/google/registry/model/console/RegistrarPocUpdateHistory.java @@ -0,0 +1,99 @@ +// 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.model.console; + +import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; + +import google.registry.model.registrar.RegistrarPoc; +import google.registry.model.registrar.RegistrarPocBase; +import google.registry.persistence.VKey; +import javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.PostLoad; +import javax.persistence.Table; + +/** + * A persisted history object representing an update to a RegistrarPoc. + * + *

In addition to the generic history fields (time, URL, etc.) we also persist a copy of the + * modified RegistrarPoc object at this point in time. + */ +@Access(AccessType.FIELD) +@Entity +@Table( + indexes = { + @Index(columnList = "historyActingUser"), + @Index(columnList = "emailAddress"), + @Index(columnList = "registrarId") + }) +public class RegistrarPocUpdateHistory extends ConsoleUpdateHistory { + + RegistrarPocBase registrarPoc; + + // These fields exist so that they can be populated in the SQL table + @Column(nullable = false) + String emailAddress; + + @Column(nullable = false) + String registrarId; + + public RegistrarPocBase getRegistrarPoc() { + return registrarPoc; + } + + @PostLoad + void postLoad() { + registrarPoc.setEmailAddress(emailAddress); + registrarPoc.setRegistrarId(registrarId); + } + + /** Creates a {@link VKey} instance for this entity. */ + @Override + public VKey createVKey() { + return VKey.create(RegistrarPocUpdateHistory.class, getRevisionId()); + } + + @Override + public Builder asBuilder() { + return new Builder(clone(this)); + } + + /** Builder for the immutable UserUpdateHistory. */ + public static class Builder + extends ConsoleUpdateHistory.Builder { + + public Builder() {} + + public Builder(RegistrarPocUpdateHistory instance) { + super(instance); + } + + @Override + public RegistrarPocUpdateHistory build() { + checkArgumentNotNull(getInstance().registrarPoc, "Registrar POC must be specified"); + return super.build(); + } + + public Builder setRegistrarPoc(RegistrarPoc registrarPoc) { + getInstance().registrarPoc = registrarPoc; + getInstance().registrarId = registrarPoc.getRegistrarId(); + getInstance().emailAddress = registrarPoc.getEmailAddress(); + return this; + } + } +} diff --git a/core/src/main/java/google/registry/model/console/RegistrarUpdateHistory.java b/core/src/main/java/google/registry/model/console/RegistrarUpdateHistory.java new file mode 100644 index 000000000..55d0dc89c --- /dev/null +++ b/core/src/main/java/google/registry/model/console/RegistrarUpdateHistory.java @@ -0,0 +1,89 @@ +// 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.model.console; + +import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; + +import google.registry.model.registrar.RegistrarBase; +import google.registry.persistence.VKey; +import javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.PostLoad; +import javax.persistence.Table; + +/** + * A persisted history object representing an update to a Registrar. + * + *

In addition to the generic history fields (time, URL, etc.) we also persist a copy of the + * modified Registrar object at this point in time. + */ +@Access(AccessType.FIELD) +@Entity +@Table(indexes = {@Index(columnList = "historyActingUser"), @Index(columnList = "registrarId")}) +public class RegistrarUpdateHistory extends ConsoleUpdateHistory { + + RegistrarBase registrar; + + // This field exists so that it exists in the SQL table + @Column(nullable = false) + @SuppressWarnings("unused") + private String registrarId; + + public RegistrarBase getRegistrar() { + return registrar; + } + + @PostLoad + void postLoad() { + registrar.setRegistrarId(registrarId); + } + + /** Creates a {@link VKey} instance for this entity. */ + @Override + public VKey createVKey() { + return VKey.create(RegistrarUpdateHistory.class, getRevisionId()); + } + + @Override + public Builder asBuilder() { + return new RegistrarUpdateHistory.Builder(clone(this)); + } + + /** Builder for the immutable UserUpdateHistory. */ + public static class Builder + extends ConsoleUpdateHistory.Builder { + + public Builder() {} + + public Builder(RegistrarUpdateHistory instance) { + super(instance); + } + + @Override + public RegistrarUpdateHistory build() { + checkArgumentNotNull(getInstance().registrar, "Registrar must be specified"); + return super.build(); + } + + public Builder setRegistrar(RegistrarBase registrar) { + getInstance().registrar = registrar; + getInstance().registrarId = registrar.getRegistrarId(); + return this; + } + } +} diff --git a/core/src/main/java/google/registry/model/console/User.java b/core/src/main/java/google/registry/model/console/User.java index efa72f176..70e407924 100644 --- a/core/src/main/java/google/registry/model/console/User.java +++ b/core/src/main/java/google/registry/model/console/User.java @@ -14,19 +14,11 @@ package google.registry.model.console; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Strings.isNullOrEmpty; -import static com.google.common.io.BaseEncoding.base64; -import static google.registry.model.registrar.Registrar.checkValidEmail; -import static google.registry.util.PasswordUtils.SALT_SUPPLIER; -import static google.registry.util.PasswordUtils.hashPassword; -import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; -import google.registry.model.Buildable; -import google.registry.model.UpdateAutoTimestampEntity; import google.registry.persistence.VKey; -import google.registry.util.PasswordUtils; -import javax.persistence.Column; +import javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.Embeddable; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; @@ -35,74 +27,17 @@ import javax.persistence.Index; import javax.persistence.Table; /** A console user, either a registry employee or a registrar partner. */ +@Embeddable @Entity @Table(indexes = {@Index(columnList = "emailAddress", name = "user_email_address_idx")}) -public class User extends UpdateAutoTimestampEntity implements Buildable { +public class User extends UserBase { - private static final long serialVersionUID = 6936728603828566721L; - - /** Autogenerated unique ID of this user. */ + @Override @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - /** Email address of the user in question. */ - @Column(nullable = false) - private String emailAddress; - - /** Roles (which grant permissions) associated with this user. */ - @Column(nullable = false) - private UserRoles userRoles; - - /** - * A hashed password that exists iff this contact is registry-lock-enabled. The hash is a base64 - * encoded SHA256 string. - */ - String registryLockPasswordHash; - - /** Randomly generated hash salt. */ - String registryLockPasswordSalt; - + @Access(AccessType.PROPERTY) public Long getId() { - return id; - } - - public String getEmailAddress() { - return emailAddress; - } - - public UserRoles getUserRoles() { - return userRoles; - } - - public boolean hasRegistryLockPassword() { - return !isNullOrEmpty(registryLockPasswordHash) && !isNullOrEmpty(registryLockPasswordSalt); - } - - public boolean verifyRegistryLockPassword(String registryLockPassword) { - if (isNullOrEmpty(registryLockPassword) - || isNullOrEmpty(registryLockPasswordSalt) - || isNullOrEmpty(registryLockPasswordHash)) { - return false; - } - return PasswordUtils.verifyPassword( - registryLockPassword, registryLockPasswordHash, registryLockPasswordSalt); - } - - /** - * Whether the user has the registry lock permission on any registrar or globally. - * - *

If so, they should be allowed to (re)set their registry lock password. - */ - public boolean hasAnyRegistryLockPermission() { - if (userRoles == null) { - return false; - } - if (userRoles.isAdmin() || userRoles.hasGlobalPermission(ConsolePermission.REGISTRY_LOCK)) { - return true; - } - return userRoles.getRegistrarRoles().values().stream() - .anyMatch(role -> role.hasPermission(ConsolePermission.REGISTRY_LOCK)); + return super.getId(); } @Override @@ -116,7 +51,7 @@ public class User extends UpdateAutoTimestampEntity implements Buildable { } /** Builder for constructing immutable {@link User} objects. */ - public static class Builder extends Buildable.Builder { + public static class Builder extends UserBase.Builder { public Builder() {} @@ -124,41 +59,5 @@ public class User extends UpdateAutoTimestampEntity implements Buildable { super(user); } - @Override - public User build() { - checkArgumentNotNull(getInstance().emailAddress, "Email address cannot be null"); - checkArgumentNotNull(getInstance().userRoles, "User roles cannot be null"); - return super.build(); - } - - public Builder setEmailAddress(String emailAddress) { - getInstance().emailAddress = checkValidEmail(emailAddress); - return this; - } - - public Builder setUserRoles(UserRoles userRoles) { - checkArgumentNotNull(userRoles, "User roles cannot be null"); - getInstance().userRoles = userRoles; - return this; - } - - public Builder removeRegistryLockPassword() { - getInstance().registryLockPasswordHash = null; - getInstance().registryLockPasswordSalt = null; - return this; - } - - public Builder setRegistryLockPassword(String registryLockPassword) { - checkArgument( - getInstance().hasAnyRegistryLockPermission(), "User has no registry lock permission"); - checkArgument( - !getInstance().hasRegistryLockPassword(), "User already has a password, remove it first"); - checkArgument( - !isNullOrEmpty(registryLockPassword), "Registry lock password was null or empty"); - byte[] salt = SALT_SUPPLIER.get(); - getInstance().registryLockPasswordSalt = base64().encode(salt); - getInstance().registryLockPasswordHash = hashPassword(registryLockPassword, salt); - return this; - } } } diff --git a/core/src/main/java/google/registry/model/console/UserBase.java b/core/src/main/java/google/registry/model/console/UserBase.java new file mode 100644 index 000000000..364bc5301 --- /dev/null +++ b/core/src/main/java/google/registry/model/console/UserBase.java @@ -0,0 +1,178 @@ +// 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.model.console; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Strings.isNullOrEmpty; +import static com.google.common.io.BaseEncoding.base64; +import static google.registry.model.registrar.Registrar.checkValidEmail; +import static google.registry.util.PasswordUtils.SALT_SUPPLIER; +import static google.registry.util.PasswordUtils.hashPassword; +import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; + +import google.registry.model.Buildable; +import google.registry.model.UpdateAutoTimestampEntity; +import google.registry.util.PasswordUtils; +import javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.Column; +import javax.persistence.Embeddable; +import javax.persistence.Id; +import javax.persistence.MappedSuperclass; +import javax.persistence.Transient; + +/** + * A console user, either a registry employee or a registrar partner. + * + *

This class deliberately does not include an {@link Id} so that any foreign-keyed fields can + * refer to the proper parent entity's ID, whether we're storing this in the DB itself or as part of + * another entity. + */ +@Access(AccessType.FIELD) +@Embeddable +@MappedSuperclass +public class UserBase extends UpdateAutoTimestampEntity implements Buildable { + + private static final long serialVersionUID = 6936728603828566721L; + + /** Autogenerated unique ID of this user. */ + @Transient private Long id; + + /** Email address of the user in question. */ + @Column(nullable = false) + String emailAddress; + + /** Roles (which grant permissions) associated with this user. */ + @Column(nullable = false) + UserRoles userRoles; + + /** + * A hashed password that exists iff this contact is registry-lock-enabled. The hash is a base64 + * encoded SHA256 string. + */ + String registryLockPasswordHash; + + /** Randomly generated hash salt. */ + String registryLockPasswordSalt; + + public Long getId() { + return id; + } + + /** + * Sets the user ID. + * + *

This should only be used for restoring the user id of an object being loaded in a PostLoad + * method (effectively, when it is still under construction by Hibernate). In all other cases, the + * object should be regarded as immutable and changes should go through a Builder. + * + *

In addition to this special case use, this method must exist to satisfy Hibernate. + */ + @SuppressWarnings("unused") + void setId(Long id) { + this.id = id; + } + + public String getEmailAddress() { + return emailAddress; + } + + public UserRoles getUserRoles() { + return userRoles; + } + + public boolean hasRegistryLockPassword() { + return !isNullOrEmpty(registryLockPasswordHash) && !isNullOrEmpty(registryLockPasswordSalt); + } + + public boolean verifyRegistryLockPassword(String registryLockPassword) { + if (isNullOrEmpty(registryLockPassword) + || isNullOrEmpty(registryLockPasswordSalt) + || isNullOrEmpty(registryLockPasswordHash)) { + return false; + } + return PasswordUtils.verifyPassword( + registryLockPassword, registryLockPasswordHash, registryLockPasswordSalt); + } + + /** + * Whether the user has the registry lock permission on any registrar or globally. + * + *

If so, they should be allowed to (re)set their registry lock password. + */ + public boolean hasAnyRegistryLockPermission() { + if (userRoles == null) { + return false; + } + if (userRoles.isAdmin() || userRoles.hasGlobalPermission(ConsolePermission.REGISTRY_LOCK)) { + return true; + } + return userRoles.getRegistrarRoles().values().stream() + .anyMatch(role -> role.hasPermission(ConsolePermission.REGISTRY_LOCK)); + } + + @Override + public Builder asBuilder() { + return new Builder<>(clone(this)); + } + + /** Builder for constructing immutable {@link UserBase} objects. */ + public static class Builder> + extends GenericBuilder { + + public Builder() {} + + public Builder(T abstractUser) { + super(abstractUser); + } + + @Override + public T build() { + checkArgumentNotNull(getInstance().emailAddress, "Email address cannot be null"); + checkArgumentNotNull(getInstance().userRoles, "User roles cannot be null"); + return super.build(); + } + + public B setEmailAddress(String emailAddress) { + getInstance().emailAddress = checkValidEmail(emailAddress); + return thisCastToDerived(); + } + + public B setUserRoles(UserRoles userRoles) { + checkArgumentNotNull(userRoles, "User roles cannot be null"); + getInstance().userRoles = userRoles; + return thisCastToDerived(); + } + + public B removeRegistryLockPassword() { + getInstance().registryLockPasswordHash = null; + getInstance().registryLockPasswordSalt = null; + return thisCastToDerived(); + } + + public B setRegistryLockPassword(String registryLockPassword) { + checkArgument( + getInstance().hasAnyRegistryLockPermission(), "User has no registry lock permission"); + checkArgument( + !getInstance().hasRegistryLockPassword(), "User already has a password, remove it first"); + checkArgument( + !isNullOrEmpty(registryLockPassword), "Registry lock password was null or empty"); + byte[] salt = SALT_SUPPLIER.get(); + getInstance().registryLockPasswordSalt = base64().encode(salt); + getInstance().registryLockPasswordHash = hashPassword(registryLockPassword, salt); + return thisCastToDerived(); + } + } +} diff --git a/core/src/main/java/google/registry/model/console/UserRoles.java b/core/src/main/java/google/registry/model/console/UserRoles.java index e21d8616f..cb43320e1 100644 --- a/core/src/main/java/google/registry/model/console/UserRoles.java +++ b/core/src/main/java/google/registry/model/console/UserRoles.java @@ -21,6 +21,8 @@ import com.google.common.collect.ImmutableMap; import google.registry.model.Buildable; import google.registry.model.ImmutableObject; import java.util.Map; +import javax.persistence.Access; +import javax.persistence.AccessType; import javax.persistence.Column; import javax.persistence.Embeddable; import javax.persistence.EnumType; @@ -32,6 +34,7 @@ import javax.persistence.Enumerated; *

See go/nomulus-console-authz for more * information. */ +@Access(AccessType.FIELD) @Embeddable public class UserRoles extends ImmutableObject implements Buildable { diff --git a/core/src/main/java/google/registry/model/console/UserUpdateHistory.java b/core/src/main/java/google/registry/model/console/UserUpdateHistory.java new file mode 100644 index 000000000..cbf41083b --- /dev/null +++ b/core/src/main/java/google/registry/model/console/UserUpdateHistory.java @@ -0,0 +1,86 @@ +// 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.model.console; + +import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; + +import google.registry.persistence.VKey; +import javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.PostLoad; +import javax.persistence.Table; + +/** + * A persisted history object representing an update to a User. + * + *

In addition to the generic history fields (time, URL, etc.) we also persist a copy of the + * modified User object at this point in time. + */ +@Access(AccessType.FIELD) +@Entity +@Table(indexes = {@Index(columnList = "historyActingUser"), @Index(columnList = "emailAddress")}) +public class UserUpdateHistory extends ConsoleUpdateHistory { + + UserBase user; + + // This field exists so that it's populated in the SQL table + @Column(nullable = false, name = "userId") + Long id; + + public UserBase getUser() { + return user; + } + + @PostLoad + void postLoad() { + user.setId(id); + } + + /** Creates a {@link VKey} instance for this entity. */ + @Override + public VKey createVKey() { + return VKey.create(UserUpdateHistory.class, getRevisionId()); + } + + @Override + public Builder asBuilder() { + return new Builder(clone(this)); + } + + /** Builder for the immutable UserUpdateHistory. */ + public static class Builder extends ConsoleUpdateHistory.Builder { + + public Builder() {} + + public Builder(UserUpdateHistory instance) { + super(instance); + } + + @Override + public UserUpdateHistory build() { + checkArgumentNotNull(getInstance().user, "User must be specified"); + return super.build(); + } + + public Builder setUser(User user) { + getInstance().user = user; + getInstance().id = user.getId(); + return this; + } + } +} diff --git a/core/src/main/java/google/registry/model/registrar/Registrar.java b/core/src/main/java/google/registry/model/registrar/Registrar.java index 972e2bac4..7d7efa872 100644 --- a/core/src/main/java/google/registry/model/registrar/Registrar.java +++ b/core/src/main/java/google/registry/model/registrar/Registrar.java @@ -14,80 +14,17 @@ package google.registry.model.registrar; -import static com.google.common.base.MoreObjects.firstNonNull; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Strings.emptyToNull; -import static com.google.common.base.Strings.nullToEmpty; -import static com.google.common.collect.ImmutableSet.toImmutableSet; -import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet; -import static com.google.common.collect.Sets.immutableEnumSet; -import static com.google.common.collect.Streams.stream; -import static com.google.common.io.BaseEncoding.base64; -import static google.registry.config.RegistryConfig.getDefaultRegistrarWhoisServer; -import static google.registry.model.CacheUtils.memoizeWithShortExpiration; -import static google.registry.model.tld.Tlds.assertTldsExist; -import static google.registry.persistence.transaction.TransactionManagerFactory.tm; -import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy; -import static google.registry.util.CollectionUtils.nullToEmptyImmutableSortedCopy; -import static google.registry.util.DateTimeUtils.START_OF_TIME; -import static google.registry.util.PasswordUtils.SALT_SUPPLIER; -import static google.registry.util.PasswordUtils.hashPassword; -import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; -import static google.registry.util.X509Utils.getCertificateHash; -import static google.registry.util.X509Utils.loadCertificate; -import static java.util.Comparator.comparing; -import static java.util.function.Predicate.isEqual; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.ImmutableSortedMap; -import com.google.common.collect.ImmutableSortedSet; -import com.google.common.collect.Maps; -import com.google.common.collect.Range; -import com.google.common.collect.Sets; -import com.google.gson.annotations.Expose; -import com.google.re2j.Pattern; -import google.registry.model.Buildable; -import google.registry.model.CreateAutoTimestamp; -import google.registry.model.JsonMapBuilder; -import google.registry.model.Jsonifiable; -import google.registry.model.UpdateAutoTimestamp; -import google.registry.model.UpdateAutoTimestampEntity; -import google.registry.model.tld.Tld; -import google.registry.model.tld.Tld.TldType; -import google.registry.persistence.VKey; -import google.registry.util.CidrAddressBlock; -import google.registry.util.PasswordUtils; -import java.security.cert.CertificateParsingException; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.function.Predicate; -import java.util.function.Supplier; -import javax.annotation.Nullable; -import javax.mail.internet.AddressException; -import javax.mail.internet.InternetAddress; +import javax.persistence.Access; +import javax.persistence.AccessType; import javax.persistence.AttributeOverride; -import javax.persistence.AttributeOverrides; import javax.persistence.Column; -import javax.persistence.Embedded; import javax.persistence.Entity; -import javax.persistence.EnumType; -import javax.persistence.Enumerated; import javax.persistence.Id; import javax.persistence.Index; import javax.persistence.Table; -import org.joda.money.CurrencyUnit; -import org.joda.time.DateTime; /** Information about a registrar. */ +@Access(AccessType.FIELD) @Entity @Table( indexes = { @@ -97,555 +34,13 @@ import org.joda.time.DateTime; @AttributeOverride( name = "updateTimestamp.lastUpdateTime", column = @Column(nullable = false, name = "lastUpdateTime")) -public class Registrar extends UpdateAutoTimestampEntity implements Buildable, Jsonifiable { - - /** Represents the type of registrar entity. */ - public enum Type { - /** A real-world, third-party registrar. Should have non-null IANA and billing account IDs. */ - REAL(Objects::nonNull), - - /** - * A registrar account used by a real third-party registrar undergoing operational testing and - * evaluation. Should only be created in sandbox, and should have null IANA/billing account IDs. - */ - OTE(Objects::isNull), - - /** - * A registrar used for predelegation testing. Should have a null billing account ID. The IANA - * ID should be either 9995 or 9996, which are reserved for predelegation testing. - */ - PDT(n -> ImmutableSet.of(9995L, 9996L).contains(n)), - - /** - * A registrar used for external monitoring by ICANN. Should have IANA ID 9997 and a null - * billing account ID. - */ - EXTERNAL_MONITORING(isEqual(9997L)), - - /** - * A registrar used for when the registry acts as a registrar. Must have either IANA ID 9998 - * (for billable transactions) or 9999 (for non-billable transactions). - */ - // TODO(b/13786188): determine what billing account ID for this should be, if any. - INTERNAL(n -> ImmutableSet.of(9998L, 9999L).contains(n)), - - /** A registrar used for internal monitoring. Should have null IANA/billing account IDs. */ - MONITORING(Objects::isNull), - - /** A registrar used for internal testing. Should have null IANA/billing account IDs. */ - TEST(Objects::isNull); - - /** - * Predicate for validating IANA IDs for this type of registrar. - * - * @see Registrar - * IDs - */ - @SuppressWarnings("ImmutableEnumChecker") - private final Predicate ianaIdValidator; - - Type(Predicate ianaIdValidator) { - this.ianaIdValidator = ianaIdValidator; - } - - /** Returns true if the given IANA identifier is valid for this registrar type. */ - public boolean isValidIanaId(Long ianaId) { - return ianaIdValidator.test(ianaId); - } - } - - /** Represents the state of a persisted registrar entity. */ - public enum State { - - /** This registrar is provisioned but not yet active, and cannot log in. */ - PENDING, - - /** This is an active registrar account which is allowed to provision and modify domains. */ - ACTIVE, - - /** - * This is a suspended account which is disallowed from provisioning new domains, but can - * otherwise still perform other operations to continue operations. - */ - SUSPENDED, - - /** - * This registrar is completely disabled and cannot perform any EPP actions whatsoever, nor log - * in to the registrar console. - */ - DISABLED - } - - /** Regex for E.164 phone number format specified by {@code contact.xsd}. */ - private static final Pattern E164_PATTERN = Pattern.compile("\\+[0-9]{1,3}\\.[0-9]{1,14}"); - - /** Regex for telephone support passcode (5 digit string). */ - public static final Pattern PHONE_PASSCODE_PATTERN = Pattern.compile("\\d{5}"); - - /** The states in which a {@link Registrar} is considered {@link #isLive live}. */ - private static final ImmutableSet LIVE_STATES = - immutableEnumSet(State.ACTIVE, State.SUSPENDED); - - /** - * The types for which a {@link Registrar} should be included in WHOIS and RDAP output. We exclude - * registrars of type TEST. We considered excluding INTERNAL as well, but decided that - * troubleshooting would be easier with INTERNAL registrars visible. Before removing other types - * from view, carefully consider the effect on things like prober monitoring and OT&E. - */ - private static final ImmutableSet PUBLICLY_VISIBLE_TYPES = - immutableEnumSet( - Type.REAL, Type.PDT, Type.OTE, Type.EXTERNAL_MONITORING, Type.MONITORING, Type.INTERNAL); - - /** Compare two instances of {@link RegistrarPoc} by their email addresses lexicographically. */ - private static final Comparator CONTACT_EMAIL_COMPARATOR = - comparing(RegistrarPoc::getEmailAddress, String::compareTo); - - /** A caching {@link Supplier} of a registrarId to {@link Registrar} map. */ - private static final Supplier> CACHE_BY_REGISTRAR_ID = - memoizeWithShortExpiration( - () -> - Maps.uniqueIndex( - tm().reTransact(() -> tm().loadAllOf(Registrar.class)), - Registrar::getRegistrarId)); - - /** - * Unique registrar client id. Must conform to "clIDType" as defined in RFC5730. - * - * @see Shared Structure Schema - */ - @Id - @Column(nullable = false) - @Expose - String registrarId; - - /** - * Registrar name. This is a distinct from the client identifier since there are no restrictions - * on its length. - * - *

NB: We are assuming that this field is unique across all registrar entities. This is not - * formally enforced in the database, but should be enforced by ICANN in that no two registrars - * will be accredited with the same name. - * - * @see ICANN-Accredited - * Registrars - */ - @Expose - @Column(nullable = false) - String registrarName; - - /** The type of this registrar. */ - @Column(nullable = false) - @Enumerated(EnumType.STRING) - Type type; - - /** The state of this registrar. */ - @Enumerated(EnumType.STRING) - State state; - - /** The set of TLDs which this registrar is allowed to access. */ - @Expose Set allowedTlds; - - /** Host name of WHOIS server. */ - @Expose String whoisServer; - - /** Base URLs for the registrar's RDAP servers. */ - Set rdapBaseUrls; - - /** - * Whether registration of premium names should be blocked over EPP. If this is set to true, then - * the only way to register premium names is with the superuser flag. - */ - boolean blockPremiumNames; - - // Authentication. - - /** X.509 PEM client certificate(s) used to authenticate registrar to EPP service. */ - @Expose String clientCertificate; - - /** Base64 encoded SHA256 hash of {@link #clientCertificate}. */ - String clientCertificateHash; - - /** - * Optional secondary X.509 PEM certificate to try if {@link #clientCertificate} does not work. - * - *

This allows registrars to migrate certificates without downtime. - */ - @Expose String failoverClientCertificate; - - /** Base64 encoded SHA256 hash of {@link #failoverClientCertificate}. */ - String failoverClientCertificateHash; - - /** An allow list of netmasks (in CIDR notation) which the client is allowed to connect from. */ - @Expose List ipAddressAllowList; - - /** A hashed password for EPP access. The hash is a base64 encoded SHA256 string. */ - String passwordHash; - - /** Randomly generated hash salt. */ - @Column(name = "password_salt") - String salt; - - // The following fields may appear redundant to the above, but are - // implied by RFC examples and should be interpreted as "for the - // Registrar as a whole". - /** - * Localized {@link RegistrarAddress} for this registrar. Contents can be represented in - * unrestricted UTF-8. - */ - @Embedded - @Expose - @AttributeOverrides({ - @AttributeOverride( - name = "streetLine1", - column = @Column(name = "localized_address_street_line1")), - @AttributeOverride( - name = "streetLine2", - column = @Column(name = "localized_address_street_line2")), - @AttributeOverride( - name = "streetLine3", - column = @Column(name = "localized_address_street_line3")), - @AttributeOverride(name = "city", column = @Column(name = "localized_address_city")), - @AttributeOverride(name = "state", column = @Column(name = "localized_address_state")), - @AttributeOverride(name = "zip", column = @Column(name = "localized_address_zip")), - @AttributeOverride( - name = "countryCode", - column = @Column(name = "localized_address_country_code")) - }) - RegistrarAddress localizedAddress; - - /** - * Internationalized {@link RegistrarAddress} for this registrar. All contained values must be - * representable in the 7-bit US-ASCII character set. - */ - @Embedded - @AttributeOverrides({ - @AttributeOverride(name = "streetLine1", column = @Column(name = "i18n_address_street_line1")), - @AttributeOverride(name = "streetLine2", column = @Column(name = "i18n_address_street_line2")), - @AttributeOverride(name = "streetLine3", column = @Column(name = "i18n_address_street_line3")), - @AttributeOverride(name = "city", column = @Column(name = "i18n_address_city")), - @AttributeOverride(name = "state", column = @Column(name = "i18n_address_state")), - @AttributeOverride(name = "zip", column = @Column(name = "i18n_address_zip")), - @AttributeOverride(name = "countryCode", column = @Column(name = "i18n_address_country_code")) - }) - RegistrarAddress internationalizedAddress; - - /** Voice number. */ - @Expose String phoneNumber; - - /** Fax number. */ - @Expose String faxNumber; - - /** Email address. */ - @Expose String emailAddress; - - // External IDs. - - /** - * Registrar identifier used for reporting to ICANN. - * - *

    - *
  • 8 is used for Testing Registrar. - *
  • 9997 is used by ICANN for SLA monitoring. - *
  • 9999 is used for cases when the registry operator acts as registrar. - *
- * - * @see Registrar - * IDs - */ - @Expose @Nullable Long ianaIdentifier; - - /** Purchase Order number used for invoices in external billing system, if applicable. */ - @Nullable String poNumber; - - /** - * Map of currency-to-billing account for the registrar. - * - *

A registrar can have different billing accounts that are denoted in different currencies. - * This provides flexibility for billing systems that require such distinction. When this field is - * accessed by {@link #getBillingAccountMap}, a sorted map is returned to guarantee deterministic - * behavior when serializing the map, for display purpose for instance. - */ - @Expose @Nullable Map billingAccountMap; - - /** URL of registrar's website. */ - @Expose String url; - - /** - * ICANN referral email address. - * - *

This value is specified in the initial registrar contact. It can't be edited in the web GUI, - * and it must be specified when the registrar account is created. - */ - @Expose String icannReferralEmail; - - /** ID of the folder in drive used to publish information for this registrar. */ - @Expose String driveFolderId; - - // Metadata. - - /** The time when this registrar was created. */ - CreateAutoTimestamp creationTime = CreateAutoTimestamp.create(null); - - /** The time that the certificate was last updated. */ - DateTime lastCertificateUpdateTime; - - /** The time that an expiring certificate notification email was sent to the registrar. */ - DateTime lastExpiringCertNotificationSentDate = START_OF_TIME; - - /** - * The time that an expiring failover certificate notification email was sent to the registrar. - */ - DateTime lastExpiringFailoverCertNotificationSentDate = START_OF_TIME; - - /** Telephone support passcode (5-digit numeric) */ - String phonePasscode; - - /** - * A dirty bit for whether RegistrarContact changes have been made that haven't been synced to - * Google Groups yet. When creating a new instance, contacts require syncing by default. - */ - boolean contactsRequireSyncing = true; - - /** Whether or not registry lock is allowed for this registrar. */ - @Expose boolean registryLockAllowed = false; - - public String getRegistrarId() { - return registrarId; - } - - public DateTime getCreationTime() { - return creationTime.getTimestamp(); - } - - @Nullable - public Long getIanaIdentifier() { - return ianaIdentifier; - } - - public Optional getPoNumber() { - return Optional.ofNullable(poNumber); - } - - public ImmutableSortedMap getBillingAccountMap() { - return billingAccountMap == null - ? ImmutableSortedMap.of() - : ImmutableSortedMap.copyOf(billingAccountMap); - } - - public DateTime getLastUpdateTime() { - return getUpdateTimestamp().getTimestamp(); - } - - public DateTime getLastCertificateUpdateTime() { - return lastCertificateUpdateTime; - } - - public DateTime getLastExpiringCertNotificationSentDate() { - return lastExpiringCertNotificationSentDate; - } - - public DateTime getLastExpiringFailoverCertNotificationSentDate() { - return lastExpiringFailoverCertNotificationSentDate; - } - - public String getRegistrarName() { - return registrarName; - } - - public Type getType() { - return type; - } - - public State getState() { - return state; - } - - public ImmutableSortedSet getAllowedTlds() { - return nullToEmptyImmutableSortedCopy(allowedTlds); - } - - /** - * Returns {@code true} if the registrar is live. - * - *

A live registrar is one that can have live domains/contacts/hosts in the registry, meaning - * that it is either currently active or used to be active (i.e. suspended). - */ - public boolean isLive() { - return LIVE_STATES.contains(state); - } - - /** Returns {@code true} if registrar should be visible in WHOIS results. */ - public boolean isLiveAndPubliclyVisible() { - return LIVE_STATES.contains(state) && PUBLICLY_VISIBLE_TYPES.contains(type); - } - - /** Returns the client certificate string if it has been set, or empty otherwise. */ - public Optional getClientCertificate() { - return Optional.ofNullable(clientCertificate); - } - - /** Returns the client certificate hash if it has been set, or empty otherwise. */ - public Optional getClientCertificateHash() { - return Optional.ofNullable(clientCertificateHash); - } - - /** Returns the failover client certificate string if it has been set, or empty otherwise. */ - public Optional getFailoverClientCertificate() { - return Optional.ofNullable(failoverClientCertificate); - } - - /** Returns the failover client certificate hash if it has been set, or empty otherwise. */ - public Optional getFailoverClientCertificateHash() { - return Optional.ofNullable(failoverClientCertificateHash); - } - - public ImmutableList getIpAddressAllowList() { - return nullToEmptyImmutableCopy(ipAddressAllowList); - } - - public RegistrarAddress getLocalizedAddress() { - return localizedAddress; - } - - public RegistrarAddress getInternationalizedAddress() { - return internationalizedAddress; - } - - public String getPhoneNumber() { - return phoneNumber; - } - - public String getFaxNumber() { - return faxNumber; - } - - public String getEmailAddress() { - return emailAddress; - } - - public String getWhoisServer() { - return firstNonNull(whoisServer, getDefaultRegistrarWhoisServer()); - } - - public ImmutableSet getRdapBaseUrls() { - return nullToEmptyImmutableSortedCopy(rdapBaseUrls); - } - - public boolean getBlockPremiumNames() { - return blockPremiumNames; - } - - public boolean getContactsRequireSyncing() { - return contactsRequireSyncing; - } - - public boolean isRegistryLockAllowed() { - return registryLockAllowed; - } - - public String getUrl() { - return url; - } - - public String getIcannReferralEmail() { - return nullToEmpty(icannReferralEmail); - } - - public String getDriveFolderId() { - return driveFolderId; - } - - /** - * Returns a list of all {@link RegistrarPoc} objects for this registrar sorted by their email - * address. - */ - public ImmutableSortedSet getContacts() { - return getContactPocs().stream() - .filter(Objects::nonNull) - .collect(toImmutableSortedSet(CONTACT_EMAIL_COMPARATOR)); - } - - /** - * Returns a list of {@link RegistrarPoc} objects of a given type for this registrar sorted by - * their email address. - */ - public ImmutableSortedSet getContactsOfType(final RegistrarPoc.Type type) { - return getContactPocs().stream() - .filter(Objects::nonNull) - .filter((@Nullable RegistrarPoc contact) -> contact.getTypes().contains(type)) - .collect(toImmutableSortedSet(CONTACT_EMAIL_COMPARATOR)); - } - - /** - * Returns the {@link RegistrarPoc} that is the WHOIS abuse contact for this registrar, or empty - * if one does not exist. - */ - public Optional getWhoisAbuseContact() { - return getContacts().stream().filter(RegistrarPoc::getVisibleInDomainWhoisAsAbuse).findFirst(); - } - - private ImmutableSet getContactPocs() { - return tm().transact( - () -> - tm().query("FROM RegistrarPoc WHERE registrarId = :registrarId", RegistrarPoc.class) - .setParameter("registrarId", registrarId) - .getResultStream() - .collect(toImmutableSet())); - } +public class Registrar extends RegistrarBase { @Override - public Map toJsonMap() { - return new JsonMapBuilder() - .put("registrarId", registrarId) - .put("ianaIdentifier", ianaIdentifier) - .putString("creationTime", creationTime.getTimestamp()) - .putString("lastUpdateTime", getUpdateTimestamp().getTimestamp()) - .putString("lastCertificateUpdateTime", lastCertificateUpdateTime) - .putString("lastExpiringCertNotificationSentDate", lastExpiringCertNotificationSentDate) - .putString( - "lastExpiringFailoverCertNotificationSentDate", - lastExpiringFailoverCertNotificationSentDate) - .put("registrarName", registrarName) - .put("type", type) - .put("state", state) - .put("clientCertificate", clientCertificate) - .put("clientCertificateHash", clientCertificateHash) - .put("failoverClientCertificate", failoverClientCertificate) - .put("failoverClientCertificateHash", failoverClientCertificateHash) - .put("localizedAddress", localizedAddress) - .put("internationalizedAddress", internationalizedAddress) - .put("phoneNumber", phoneNumber) - .put("faxNumber", faxNumber) - .put("emailAddress", emailAddress) - .put("whoisServer", getWhoisServer()) - .putListOfStrings("rdapBaseUrls", getRdapBaseUrls()) - .put("blockPremiumNames", blockPremiumNames) - .put("url", url) - .put("icannReferralEmail", getIcannReferralEmail()) - .put("driveFolderId", driveFolderId) - .put("phoneNumber", phoneNumber) - .put("phonePasscode", phonePasscode) - .putListOfStrings("allowedTlds", getAllowedTlds()) - .putListOfStrings("ipAddressAllowList", getIpAddressAllowList()) - .putListOfJsonObjects("contacts", getContacts()) - .put("registryLockAllowed", registryLockAllowed) - .build(); - } - - private static String checkValidPhoneNumber(String phoneNumber) { - checkArgument( - E164_PATTERN.matcher(phoneNumber).matches(), - "Not a valid E.164 phone number: %s", - phoneNumber); - return phoneNumber; - } - - public boolean verifyPassword(String password) { - return PasswordUtils.verifyPassword(password, passwordHash, salt); - } - - public String getPhonePasscode() { - return phonePasscode; + @Id + @Access(AccessType.PROPERTY) + public String getRegistrarId() { + return super.getRegistrarId(); } @Override @@ -653,363 +48,14 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J return new Builder(clone(this)); } - /** Creates a {@link VKey} for this instance. */ - @Override - public VKey createVKey() { - return createVKey(registrarId); - } + /** Builder for constructing immutable {@link Registrar} objects. */ + public static class Builder extends RegistrarBase.Builder { - /** Creates a {@link VKey} for the given {@code registrarId}. */ - public static VKey createVKey(String registrarId) { - checkArgumentNotNull(registrarId, "registrarId must be specified"); - return VKey.create(Registrar.class, registrarId); - } - - /** A builder for constructing {@link Registrar}, since it is immutable. */ - public static class Builder extends Buildable.Builder { public Builder() {} - private Builder(Registrar instance) { - super(instance); + public Builder(Registrar registrar) { + super(registrar); } - public Builder setRegistrarId(String registrarId) { - // Registrar id must be [3,16] chars long. See "clIDType" in the base EPP schema of RFC 5730. - // (Need to validate this here as there's no matching EPP XSD for validation.) - checkArgument( - Range.closed(3, 16).contains(registrarId.length()), - "Registrar ID must be 3-16 characters long."); - getInstance().registrarId = registrarId; - return this; - } - - public Builder setIanaIdentifier(@Nullable Long ianaIdentifier) { - checkArgument( - ianaIdentifier == null || ianaIdentifier > 0, "IANA ID must be a positive number"); - getInstance().ianaIdentifier = ianaIdentifier; - return this; - } - - public Builder setPoNumber(Optional poNumber) { - getInstance().poNumber = poNumber.orElse(null); - return this; - } - - public Builder setBillingAccountMap(@Nullable Map billingAccountMap) { - getInstance().billingAccountMap = nullToEmptyImmutableCopy(billingAccountMap); - return this; - } - - public Builder setRegistrarName(String registrarName) { - getInstance().registrarName = registrarName; - return this; - } - - public Builder setType(Type type) { - getInstance().type = type; - return this; - } - - public Builder setState(State state) { - getInstance().state = state; - return this; - } - - public Builder setAllowedTlds(Set allowedTlds) { - getInstance().allowedTlds = ImmutableSortedSet.copyOf(assertTldsExist(allowedTlds)); - return this; - } - - /** - * Same as {@link #setAllowedTlds}, but doesn't use the cache to check if the TLDs exist. - * - *

This should be used if the TLD we want to set is persisted in the same transaction - - * meaning its existence can't be cached before we need to save the Registrar. - * - *

We can still only set the allowedTld AFTER we saved the Registry entity. Make sure to call - * {@code .now()} when saving the Registry entity to make sure it's actually saved before trying - * to set the allowed TLDs. - */ - public Builder setAllowedTldsUncached(Set allowedTlds) { - ImmutableSet> newTldKeys = - Sets.difference(allowedTlds, getInstance().getAllowedTlds()).stream() - .map(Tld::createVKey) - .collect(toImmutableSet()); - Set> missingTldKeys = - Sets.difference(newTldKeys, tm().loadByKeysIfPresent(newTldKeys).keySet()); - checkArgument(missingTldKeys.isEmpty(), "Trying to set nonexistent TLDs: %s", missingTldKeys); - getInstance().allowedTlds = ImmutableSortedSet.copyOf(allowedTlds); - return this; - } - - public Builder setClientCertificate(String clientCertificate, DateTime now) { - clientCertificate = emptyToNull(clientCertificate); - String clientCertificateHash = calculateHash(clientCertificate); - if (!Objects.equals(clientCertificate, getInstance().clientCertificate) - || !Objects.equals(clientCertificateHash, getInstance().clientCertificateHash)) { - getInstance().clientCertificate = clientCertificate; - getInstance().clientCertificateHash = clientCertificateHash; - getInstance().lastCertificateUpdateTime = now; - } - return this; - } - - public Builder setLastExpiringCertNotificationSentDate(DateTime now) { - checkArgumentNotNull(now, "Registrar lastExpiringCertNotificationSentDate cannot be null"); - getInstance().lastExpiringCertNotificationSentDate = now; - return this; - } - - public Builder setLastExpiringFailoverCertNotificationSentDate(DateTime now) { - checkArgumentNotNull( - now, "Registrar lastExpiringFailoverCertNotificationSentDate cannot be null"); - getInstance().lastExpiringFailoverCertNotificationSentDate = now; - return this; - } - - public Builder setFailoverClientCertificate(String clientCertificate, DateTime now) { - clientCertificate = emptyToNull(clientCertificate); - String clientCertificateHash = calculateHash(clientCertificate); - if (!Objects.equals(clientCertificate, getInstance().failoverClientCertificate) - || !Objects.equals(clientCertificateHash, getInstance().failoverClientCertificateHash)) { - getInstance().failoverClientCertificate = clientCertificate; - getInstance().failoverClientCertificateHash = clientCertificateHash; - getInstance().lastCertificateUpdateTime = now; - } - return this; - } - - private static String calculateHash(String clientCertificate) { - if (clientCertificate == null) { - return null; - } - try { - return getCertificateHash(loadCertificate(clientCertificate)); - } catch (CertificateParsingException e) { - throw new IllegalArgumentException(e); - } - } - - // Making sure there's no registrar with the same ianaId already in the system - private static boolean isNotADuplicateIanaId( - Iterable registrars, Registrar newInstance) { - // Return early if newly build registrar is not type REAL or ianaId is - // reserved by ICANN - https://www.iana.org/assignments/registrar-ids/registrar-ids.xhtml - if (!Type.REAL.equals(newInstance.type) - || ImmutableSet.of(1L, 8L).contains(newInstance.ianaIdentifier)) { - return true; - } - - return stream(registrars) - .filter(registrar -> Type.REAL.equals(registrar.getType())) - .filter(registrar -> !Objects.equals(newInstance.registrarId, registrar.getRegistrarId())) - .noneMatch( - registrar -> - Objects.equals(newInstance.ianaIdentifier, registrar.getIanaIdentifier())); - } - - public Builder setContactsRequireSyncing(boolean contactsRequireSyncing) { - getInstance().contactsRequireSyncing = contactsRequireSyncing; - return this; - } - - public Builder setIpAddressAllowList(Iterable ipAddressAllowList) { - getInstance().ipAddressAllowList = ImmutableList.copyOf(ipAddressAllowList); - return this; - } - - public Builder setLocalizedAddress(RegistrarAddress localizedAddress) { - getInstance().localizedAddress = localizedAddress; - return this; - } - - public Builder setInternationalizedAddress(RegistrarAddress internationalizedAddress) { - getInstance().internationalizedAddress = internationalizedAddress; - return this; - } - - public Builder setPhoneNumber(String phoneNumber) { - getInstance().phoneNumber = (phoneNumber == null) ? null : checkValidPhoneNumber(phoneNumber); - return this; - } - - public Builder setFaxNumber(String faxNumber) { - getInstance().faxNumber = (faxNumber == null) ? null : checkValidPhoneNumber(faxNumber); - return this; - } - - public Builder setEmailAddress(String emailAddress) { - getInstance().emailAddress = checkValidEmail(emailAddress); - return this; - } - - public Builder setWhoisServer(String whoisServer) { - getInstance().whoisServer = whoisServer; - return this; - } - - public Builder setRdapBaseUrls(Set rdapBaseUrls) { - getInstance().rdapBaseUrls = ImmutableSet.copyOf(rdapBaseUrls); - return this; - } - - public Builder setBlockPremiumNames(boolean blockPremiumNames) { - getInstance().blockPremiumNames = blockPremiumNames; - return this; - } - - public Builder setUrl(String url) { - getInstance().url = url; - return this; - } - - public Builder setIcannReferralEmail(String icannReferralEmail) { - getInstance().icannReferralEmail = checkValidEmail(icannReferralEmail); - return this; - } - - public Builder setDriveFolderId(@Nullable String driveFolderId) { - checkArgument( - driveFolderId == null || !driveFolderId.contains("/"), - "Drive folder ID must not be a full URL"); - getInstance().driveFolderId = driveFolderId; - return this; - } - - public Builder setPassword(String password) { - // Passwords must be [6,16] chars long. See "pwType" in the base EPP schema of RFC 5730. - checkArgument( - Range.closed(6, 16).contains(nullToEmpty(password).length()), - "Password must be 6-16 characters long."); - byte[] salt = SALT_SUPPLIER.get(); - getInstance().salt = base64().encode(salt); - getInstance().passwordHash = hashPassword(password, salt); - return this; - } - - /** - * Set the phone passcode. - * - * @throws IllegalArgumentException if provided passcode is not 5-digit numeric - */ - public Builder setPhonePasscode(String phonePasscode) { - checkArgument( - phonePasscode == null || PHONE_PASSCODE_PATTERN.matcher(phonePasscode).matches(), - "Not a valid telephone passcode (must be 5 digits long): %s", - phonePasscode); - getInstance().phonePasscode = phonePasscode; - return this; - } - - public Builder setRegistryLockAllowed(boolean registryLockAllowed) { - getInstance().registryLockAllowed = registryLockAllowed; - return this; - } - - /** - * This lets tests set the update timestamp in cases where setting fields resets the timestamp - * and breaks the verification that an object has not been updated since it was copied. - */ - @VisibleForTesting - public Builder setLastUpdateTime(DateTime timestamp) { - getInstance().setUpdateTimestamp(UpdateAutoTimestamp.create(timestamp)); - return this; - } - - /** Build the registrar, nullifying empty fields. */ - @Override - public Registrar build() { - checkArgumentNotNull(getInstance().type, "Registrar type cannot be null"); - checkArgumentNotNull(getInstance().registrarName, "Registrar name cannot be null"); - checkArgument( - getInstance().localizedAddress != null || getInstance().internationalizedAddress != null, - "Must specify at least one of localized or internationalized address"); - checkArgument( - getInstance().type.isValidIanaId(getInstance().ianaIdentifier), - String.format( - "Supplied IANA ID is not valid for %s registrar type: %s", - getInstance().type, getInstance().ianaIdentifier)); - - // We do not allow creating Real registrars with IANA ID that's already in the system - // b/315007360 - for more details - checkArgument( - isNotADuplicateIanaId(loadAllCached(), getInstance()), - String.format( - "Rejected attempt to create a registrar with ianaId that's already in the system -" - + " %s", - getInstance().ianaIdentifier)); - - // In order to grant access to real TLDs, the registrar must have a corresponding billing - // account ID for that TLD's billing currency. - ImmutableSet nonBillableTlds = - Tld.get(getInstance().getAllowedTlds()).stream() - .filter(r -> r.getTldType() == TldType.REAL) - .filter(r -> !getInstance().getBillingAccountMap().containsKey(r.getCurrency())) - .map(Tld::getTldStr) - .collect(toImmutableSet()); - checkArgument( - nonBillableTlds.isEmpty(), - "Cannot set these allowed, real TLDs because their currency is missing " - + "from the billing account map: %s", - nonBillableTlds); - return cloneEmptyToNull(super.build()); - } - } - - /** Verifies that the email address in question is not null and has a valid format. */ - public static String checkValidEmail(String email) { - checkNotNull(email, "Provided email was null"); - try { - InternetAddress internetAddress = new InternetAddress(email, true); - internetAddress.validate(); - } catch (AddressException e) { - throw new IllegalArgumentException( - String.format("Provided email %s is not a valid email address", email)); - } - return email; - } - - /** Loads all registrar entities directly from the database. */ - public static Iterable loadAll() { - return tm().transact(() -> tm().loadAllOf(Registrar.class)); - } - - /** Loads all registrar entities using an in-memory cache. */ - public static Iterable loadAllCached() { - return CACHE_BY_REGISTRAR_ID.get().values(); - } - - /** Loads all registrar keys using an in-memory cache. */ - public static ImmutableSet> loadAllKeysCached() { - return CACHE_BY_REGISTRAR_ID.get().keySet().stream() - .map(Registrar::createVKey) - .collect(toImmutableSet()); - } - - /** Loads and returns a registrar entity by its id directly from the database. */ - public static Optional loadByRegistrarId(String registrarId) { - checkArgument(!Strings.isNullOrEmpty(registrarId), "registrarId must be specified"); - return tm().transact(() -> tm().loadByKeyIfPresent(createVKey(registrarId))); - } - - /** - * Loads and returns a registrar entity by its id using an in-memory cache. - * - *

Returns empty if the registrar isn't found. - */ - public static Optional loadByRegistrarIdCached(String registrarId) { - checkArgument(!Strings.isNullOrEmpty(registrarId), "registrarId must be specified"); - return Optional.ofNullable(CACHE_BY_REGISTRAR_ID.get().get(registrarId)); - } - - /** - * Loads and returns a registrar entity by its id using an in-memory cache. - * - *

Throws if the registrar isn't found. - */ - public static Registrar loadRequiredRegistrarCached(String registrarId) { - Optional registrar = loadByRegistrarIdCached(registrarId); - checkArgument(registrar.isPresent(), "couldn't find registrar '%s'", registrarId); - return registrar.get(); } } diff --git a/core/src/main/java/google/registry/model/registrar/RegistrarBase.java b/core/src/main/java/google/registry/model/registrar/RegistrarBase.java new file mode 100644 index 000000000..db1cbe3b3 --- /dev/null +++ b/core/src/main/java/google/registry/model/registrar/RegistrarBase.java @@ -0,0 +1,1028 @@ +// 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.model.registrar; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.emptyToNull; +import static com.google.common.base.Strings.nullToEmpty; +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet; +import static com.google.common.collect.Sets.immutableEnumSet; +import static com.google.common.collect.Streams.stream; +import static com.google.common.io.BaseEncoding.base64; +import static google.registry.config.RegistryConfig.getDefaultRegistrarWhoisServer; +import static google.registry.model.CacheUtils.memoizeWithShortExpiration; +import static google.registry.model.tld.Tlds.assertTldsExist; +import static google.registry.persistence.transaction.TransactionManagerFactory.tm; +import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy; +import static google.registry.util.CollectionUtils.nullToEmptyImmutableSortedCopy; +import static google.registry.util.DateTimeUtils.START_OF_TIME; +import static google.registry.util.PasswordUtils.SALT_SUPPLIER; +import static google.registry.util.PasswordUtils.hashPassword; +import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; +import static google.registry.util.X509Utils.getCertificateHash; +import static google.registry.util.X509Utils.loadCertificate; +import static java.util.Comparator.comparing; +import static java.util.function.Predicate.isEqual; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSortedMap; +import com.google.common.collect.ImmutableSortedSet; +import com.google.common.collect.Maps; +import com.google.common.collect.Range; +import com.google.common.collect.Sets; +import com.google.gson.annotations.Expose; +import com.google.re2j.Pattern; +import google.registry.model.Buildable; +import google.registry.model.CreateAutoTimestamp; +import google.registry.model.JsonMapBuilder; +import google.registry.model.Jsonifiable; +import google.registry.model.UpdateAutoTimestamp; +import google.registry.model.UpdateAutoTimestampEntity; +import google.registry.model.tld.Tld; +import google.registry.model.tld.Tld.TldType; +import google.registry.persistence.VKey; +import google.registry.util.CidrAddressBlock; +import google.registry.util.PasswordUtils; +import java.security.cert.CertificateParsingException; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.function.Supplier; +import javax.annotation.Nullable; +import javax.mail.internet.AddressException; +import javax.mail.internet.InternetAddress; +import javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.AttributeOverride; +import javax.persistence.AttributeOverrides; +import javax.persistence.Column; +import javax.persistence.Embeddable; +import javax.persistence.Embedded; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Id; +import javax.persistence.MappedSuperclass; +import javax.persistence.Transient; +import org.joda.money.CurrencyUnit; +import org.joda.time.DateTime; + +/** + * Information about a registrar. + * + *

This class deliberately does not include an {@link Id} so that any foreign-keyed fields can + * refer to the proper parent entity's ID, whether we're storing this in the DB itself or as part of + * another entity. + */ +@Access(AccessType.FIELD) +@Embeddable +@MappedSuperclass +public class RegistrarBase extends UpdateAutoTimestampEntity implements Buildable, Jsonifiable { + + /** Represents the type of registrar entity. */ + public enum Type { + /** A real-world, third-party registrar. Should have non-null IANA and billing account IDs. */ + REAL(Objects::nonNull), + + /** + * A registrar account used by a real third-party registrar undergoing operational testing and + * evaluation. Should only be created in sandbox, and should have null IANA/billing account IDs. + */ + OTE(Objects::isNull), + + /** + * A registrar used for predelegation testing. Should have a null billing account ID. The IANA + * ID should be either 9995 or 9996, which are reserved for predelegation testing. + */ + PDT(n -> ImmutableSet.of(9995L, 9996L).contains(n)), + + /** + * A registrar used for external monitoring by ICANN. Should have IANA ID 9997 and a null + * billing account ID. + */ + EXTERNAL_MONITORING(isEqual(9997L)), + + /** + * A registrar used for when the registry acts as a registrar. Must have either IANA ID 9998 + * (for billable transactions) or 9999 (for non-billable transactions). + */ + // TODO(b/13786188): determine what billing account ID for this should be, if any. + INTERNAL(n -> ImmutableSet.of(9998L, 9999L).contains(n)), + + /** A registrar used for internal monitoring. Should have null IANA/billing account IDs. */ + MONITORING(Objects::isNull), + + /** A registrar used for internal testing. Should have null IANA/billing account IDs. */ + TEST(Objects::isNull); + + /** + * Predicate for validating IANA IDs for this type of registrar. + * + * @see Registrar + * IDs + */ + @SuppressWarnings("ImmutableEnumChecker") + private final Predicate ianaIdValidator; + + Type(Predicate ianaIdValidator) { + this.ianaIdValidator = ianaIdValidator; + } + + /** Returns true if the given IANA identifier is valid for this registrar type. */ + public boolean isValidIanaId(Long ianaId) { + return ianaIdValidator.test(ianaId); + } + } + + /** Represents the state of a persisted registrar entity. */ + public enum State { + + /** This registrar is provisioned but not yet active, and cannot log in. */ + PENDING, + + /** This is an active registrar account which is allowed to provision and modify domains. */ + ACTIVE, + + /** + * This is a suspended account which is disallowed from provisioning new domains, but can + * otherwise still perform other operations to continue operations. + */ + SUSPENDED, + + /** + * This registrar is completely disabled and cannot perform any EPP actions whatsoever, nor log + * in to the registrar console. + */ + DISABLED + } + + /** Regex for E.164 phone number format specified by {@code contact.xsd}. */ + private static final Pattern E164_PATTERN = Pattern.compile("\\+[0-9]{1,3}\\.[0-9]{1,14}"); + + /** Regex for telephone support passcode (5 digit string). */ + public static final Pattern PHONE_PASSCODE_PATTERN = Pattern.compile("\\d{5}"); + + /** The states in which a {@link Registrar} is considered {@link #isLive live}. */ + private static final ImmutableSet LIVE_STATES = + immutableEnumSet(State.ACTIVE, State.SUSPENDED); + + /** + * The types for which a {@link Registrar} should be included in WHOIS and RDAP output. We exclude + * registrars of type TEST. We considered excluding INTERNAL as well, but decided that + * troubleshooting would be easier with INTERNAL registrars visible. Before removing other types + * from view, carefully consider the effect on things like prober monitoring and OT&E. + */ + private static final ImmutableSet PUBLICLY_VISIBLE_TYPES = + immutableEnumSet( + Type.REAL, Type.PDT, Type.OTE, Type.EXTERNAL_MONITORING, Type.MONITORING, Type.INTERNAL); + + /** Compare two instances of {@link RegistrarPoc} by their email addresses lexicographically. */ + private static final Comparator CONTACT_EMAIL_COMPARATOR = + comparing(RegistrarPoc::getEmailAddress, String::compareTo); + + /** A caching {@link Supplier} of a registrarId to {@link Registrar} map. */ + private static final Supplier> CACHE_BY_REGISTRAR_ID = + memoizeWithShortExpiration( + () -> + Maps.uniqueIndex( + tm().reTransact(() -> tm().loadAllOf(Registrar.class)), + Registrar::getRegistrarId)); + + /** + * Unique registrar client id. Must conform to "clIDType" as defined in RFC5730. + * + * @see Shared Structure Schema + */ + @Expose @Transient String registrarId; + + /** + * Registrar name. This is a distinct from the client identifier since there are no restrictions + * on its length. + * + *

NB: We are assuming that this field is unique across all registrar entities. This is not + * formally enforced in the database, but should be enforced by ICANN in that no two registrars + * will be accredited with the same name. + * + * @see ICANN-Accredited + * Registrars + */ + @Expose + @Column(nullable = false) + String registrarName; + + /** The type of this registrar. */ + @Column(nullable = false) + @Enumerated(EnumType.STRING) + Type type; + + /** The state of this registrar. */ + @Enumerated(EnumType.STRING) + State state; + + /** The set of TLDs which this registrar is allowed to access. */ + @Expose Set allowedTlds; + + /** Host name of WHOIS server. */ + @Expose String whoisServer; + + /** Base URLs for the registrar's RDAP servers. */ + Set rdapBaseUrls; + + /** + * Whether registration of premium names should be blocked over EPP. If this is set to true, then + * the only way to register premium names is with the superuser flag. + */ + boolean blockPremiumNames; + + // Authentication. + + /** X.509 PEM client certificate(s) used to authenticate registrar to EPP service. */ + @Expose String clientCertificate; + + /** Base64 encoded SHA256 hash of {@link #clientCertificate}. */ + String clientCertificateHash; + + /** + * Optional secondary X.509 PEM certificate to try if {@link #clientCertificate} does not work. + * + *

This allows registrars to migrate certificates without downtime. + */ + @Expose String failoverClientCertificate; + + /** Base64 encoded SHA256 hash of {@link #failoverClientCertificate}. */ + String failoverClientCertificateHash; + + /** An allow list of netmasks (in CIDR notation) which the client is allowed to connect from. */ + @Expose List ipAddressAllowList; + + /** A hashed password for EPP access. The hash is a base64 encoded SHA256 string. */ + String passwordHash; + + /** Randomly generated hash salt. */ + @Column(name = "password_salt") + String salt; + + // The following fields may appear redundant to the above, but are + // implied by RFC examples and should be interpreted as "for the + // Registrar as a whole". + /** + * Localized {@link RegistrarAddress} for this registrar. Contents can be represented in + * unrestricted UTF-8. + */ + @Embedded + @Expose + @AttributeOverrides({ + @AttributeOverride( + name = "streetLine1", + column = @Column(name = "localized_address_street_line1")), + @AttributeOverride( + name = "streetLine2", + column = @Column(name = "localized_address_street_line2")), + @AttributeOverride( + name = "streetLine3", + column = @Column(name = "localized_address_street_line3")), + @AttributeOverride(name = "city", column = @Column(name = "localized_address_city")), + @AttributeOverride(name = "state", column = @Column(name = "localized_address_state")), + @AttributeOverride(name = "zip", column = @Column(name = "localized_address_zip")), + @AttributeOverride( + name = "countryCode", + column = @Column(name = "localized_address_country_code")) + }) + RegistrarAddress localizedAddress; + + /** + * Internationalized {@link RegistrarAddress} for this registrar. All contained values must be + * representable in the 7-bit US-ASCII character set. + */ + @Embedded + @AttributeOverrides({ + @AttributeOverride(name = "streetLine1", column = @Column(name = "i18n_address_street_line1")), + @AttributeOverride(name = "streetLine2", column = @Column(name = "i18n_address_street_line2")), + @AttributeOverride(name = "streetLine3", column = @Column(name = "i18n_address_street_line3")), + @AttributeOverride(name = "city", column = @Column(name = "i18n_address_city")), + @AttributeOverride(name = "state", column = @Column(name = "i18n_address_state")), + @AttributeOverride(name = "zip", column = @Column(name = "i18n_address_zip")), + @AttributeOverride(name = "countryCode", column = @Column(name = "i18n_address_country_code")) + }) + RegistrarAddress internationalizedAddress; + + /** Voice number. */ + @Expose String phoneNumber; + + /** Fax number. */ + @Expose String faxNumber; + + /** Email address. */ + @Expose String emailAddress; + + // External IDs. + + /** + * Registrar identifier used for reporting to ICANN. + * + *

    + *
  • 8 is used for Testing Registrar. + *
  • 9997 is used by ICANN for SLA monitoring. + *
  • 9999 is used for cases when the registry operator acts as registrar. + *
+ * + * @see Registrar + * IDs + */ + @Expose @Nullable Long ianaIdentifier; + + /** Purchase Order number used for invoices in external billing system, if applicable. */ + @Nullable String poNumber; + + /** + * Map of currency-to-billing account for the registrar. + * + *

A registrar can have different billing accounts that are denoted in different currencies. + * This provides flexibility for billing systems that require such distinction. When this field is + * accessed by {@link #getBillingAccountMap}, a sorted map is returned to guarantee deterministic + * behavior when serializing the map, for display purpose for instance. + */ + @Expose @Nullable Map billingAccountMap; + + /** URL of registrar's website. */ + @Expose String url; + + /** + * ICANN referral email address. + * + *

This value is specified in the initial registrar contact. It can't be edited in the web GUI, + * and it must be specified when the registrar account is created. + */ + @Expose String icannReferralEmail; + + /** ID of the folder in drive used to publish information for this registrar. */ + @Expose String driveFolderId; + + // Metadata. + + /** The time when this registrar was created. */ + CreateAutoTimestamp creationTime = CreateAutoTimestamp.create(null); + + /** The time that the certificate was last updated. */ + DateTime lastCertificateUpdateTime; + + /** The time that an expiring certificate notification email was sent to the registrar. */ + DateTime lastExpiringCertNotificationSentDate = START_OF_TIME; + + /** + * The time that an expiring failover certificate notification email was sent to the registrar. + */ + DateTime lastExpiringFailoverCertNotificationSentDate = START_OF_TIME; + + /** Telephone support passcode (5-digit numeric) */ + String phonePasscode; + + /** + * A dirty bit for whether RegistrarContact changes have been made that haven't been synced to + * Google Groups yet. When creating a new instance, contacts require syncing by default. + */ + boolean contactsRequireSyncing = true; + + /** Whether or not registry lock is allowed for this registrar. */ + @Expose boolean registryLockAllowed = false; + + public String getRegistrarId() { + return registrarId; + } + + public DateTime getCreationTime() { + return creationTime.getTimestamp(); + } + + @Nullable + public Long getIanaIdentifier() { + return ianaIdentifier; + } + + public Optional getPoNumber() { + return Optional.ofNullable(poNumber); + } + + public ImmutableSortedMap getBillingAccountMap() { + return billingAccountMap == null + ? ImmutableSortedMap.of() + : ImmutableSortedMap.copyOf(billingAccountMap); + } + + public DateTime getLastUpdateTime() { + return getUpdateTimestamp().getTimestamp(); + } + + public DateTime getLastCertificateUpdateTime() { + return lastCertificateUpdateTime; + } + + public DateTime getLastExpiringCertNotificationSentDate() { + return lastExpiringCertNotificationSentDate; + } + + public DateTime getLastExpiringFailoverCertNotificationSentDate() { + return lastExpiringFailoverCertNotificationSentDate; + } + + public String getRegistrarName() { + return registrarName; + } + + public Type getType() { + return type; + } + + public State getState() { + return state; + } + + public ImmutableSortedSet getAllowedTlds() { + return nullToEmptyImmutableSortedCopy(allowedTlds); + } + + /** + * Returns {@code true} if the registrar is live. + * + *

A live registrar is one that can have live domains/contacts/hosts in the registry, meaning + * that it is either currently active or used to be active (i.e. suspended). + */ + public boolean isLive() { + return LIVE_STATES.contains(state); + } + + /** Returns {@code true} if registrar should be visible in WHOIS results. */ + public boolean isLiveAndPubliclyVisible() { + return LIVE_STATES.contains(state) && PUBLICLY_VISIBLE_TYPES.contains(type); + } + + /** Returns the client certificate string if it has been set, or empty otherwise. */ + public Optional getClientCertificate() { + return Optional.ofNullable(clientCertificate); + } + + /** Returns the client certificate hash if it has been set, or empty otherwise. */ + public Optional getClientCertificateHash() { + return Optional.ofNullable(clientCertificateHash); + } + + /** Returns the failover client certificate string if it has been set, or empty otherwise. */ + public Optional getFailoverClientCertificate() { + return Optional.ofNullable(failoverClientCertificate); + } + + /** Returns the failover client certificate hash if it has been set, or empty otherwise. */ + public Optional getFailoverClientCertificateHash() { + return Optional.ofNullable(failoverClientCertificateHash); + } + + public ImmutableList getIpAddressAllowList() { + return nullToEmptyImmutableCopy(ipAddressAllowList); + } + + public RegistrarAddress getLocalizedAddress() { + return localizedAddress; + } + + public RegistrarAddress getInternationalizedAddress() { + return internationalizedAddress; + } + + public String getPhoneNumber() { + return phoneNumber; + } + + public String getFaxNumber() { + return faxNumber; + } + + public String getEmailAddress() { + return emailAddress; + } + + public String getWhoisServer() { + return firstNonNull(whoisServer, getDefaultRegistrarWhoisServer()); + } + + public ImmutableSet getRdapBaseUrls() { + return nullToEmptyImmutableSortedCopy(rdapBaseUrls); + } + + public boolean getBlockPremiumNames() { + return blockPremiumNames; + } + + public boolean getContactsRequireSyncing() { + return contactsRequireSyncing; + } + + public boolean isRegistryLockAllowed() { + return registryLockAllowed; + } + + public String getUrl() { + return url; + } + + public String getIcannReferralEmail() { + return nullToEmpty(icannReferralEmail); + } + + public String getDriveFolderId() { + return driveFolderId; + } + + /** + * Returns a list of all {@link RegistrarPoc} objects for this registrar sorted by their email + * address. + */ + public ImmutableSortedSet getContacts() { + return getContactPocs().stream() + .filter(Objects::nonNull) + .collect(toImmutableSortedSet(CONTACT_EMAIL_COMPARATOR)); + } + + /** + * Returns a list of {@link RegistrarPoc} objects of a given type for this registrar sorted by + * their email address. + */ + public ImmutableSortedSet getContactsOfType(final RegistrarPocBase.Type type) { + return getContactPocs().stream() + .filter(Objects::nonNull) + .filter((@Nullable RegistrarPoc contact) -> contact.getTypes().contains(type)) + .collect(toImmutableSortedSet(CONTACT_EMAIL_COMPARATOR)); + } + + /** + * Returns the {@link RegistrarPoc} that is the WHOIS abuse contact for this registrar, or empty + * if one does not exist. + */ + public Optional getWhoisAbuseContact() { + return getContacts().stream().filter(RegistrarPoc::getVisibleInDomainWhoisAsAbuse).findFirst(); + } + + private ImmutableSet getContactPocs() { + return tm().transact( + () -> + tm().query("FROM RegistrarPoc WHERE registrarId = :registrarId", RegistrarPoc.class) + .setParameter("registrarId", registrarId) + .getResultStream() + .collect(toImmutableSet())); + } + + @Override + public Map toJsonMap() { + return new JsonMapBuilder() + .put("registrarId", registrarId) + .put("ianaIdentifier", ianaIdentifier) + .putString("creationTime", creationTime.getTimestamp()) + .putString("lastUpdateTime", getUpdateTimestamp().getTimestamp()) + .putString("lastCertificateUpdateTime", lastCertificateUpdateTime) + .putString("lastExpiringCertNotificationSentDate", lastExpiringCertNotificationSentDate) + .putString( + "lastExpiringFailoverCertNotificationSentDate", + lastExpiringFailoverCertNotificationSentDate) + .put("registrarName", registrarName) + .put("type", type) + .put("state", state) + .put("clientCertificate", clientCertificate) + .put("clientCertificateHash", clientCertificateHash) + .put("failoverClientCertificate", failoverClientCertificate) + .put("failoverClientCertificateHash", failoverClientCertificateHash) + .put("localizedAddress", localizedAddress) + .put("internationalizedAddress", internationalizedAddress) + .put("phoneNumber", phoneNumber) + .put("faxNumber", faxNumber) + .put("emailAddress", emailAddress) + .put("whoisServer", getWhoisServer()) + .putListOfStrings("rdapBaseUrls", getRdapBaseUrls()) + .put("blockPremiumNames", blockPremiumNames) + .put("url", url) + .put("icannReferralEmail", getIcannReferralEmail()) + .put("driveFolderId", driveFolderId) + .put("phoneNumber", phoneNumber) + .put("phonePasscode", phonePasscode) + .putListOfStrings("allowedTlds", getAllowedTlds()) + .putListOfStrings("ipAddressAllowList", getIpAddressAllowList()) + .putListOfJsonObjects("contacts", getContacts()) + .put("registryLockAllowed", registryLockAllowed) + .build(); + } + + private static String checkValidPhoneNumber(String phoneNumber) { + checkArgument( + E164_PATTERN.matcher(phoneNumber).matches(), + "Not a valid E.164 phone number: %s", + phoneNumber); + return phoneNumber; + } + + public boolean verifyPassword(String password) { + return PasswordUtils.verifyPassword(password, passwordHash, salt); + } + + public String getPhonePasscode() { + return phonePasscode; + } + + /** + * Sets the registrar ID. + * + *

This should only be used for restoring the registrar ID of an object being loaded in a + * PostLoad method (effectively, when it is still under construction by Hibernate). In all other + * cases, the object should be regarded as immutable and changes should go through a Builder. + * + *

In addition to this special case use, this method must exist to satisfy Hibernate. + */ + @SuppressWarnings("unused") + public void setRegistrarId(String registrarId) { + this.registrarId = registrarId; + } + + @Override + public Builder asBuilder() { + return new Builder<>(clone(this)); + } + + @Override + public VKey createVKey() { + return createVKey(registrarId); + } + + /** Creates a {@link VKey} for the given {@code registrarId}. */ + public static VKey createVKey(String registrarId) { + checkArgumentNotNull(registrarId, "registrarId must be specified"); + return VKey.create(Registrar.class, registrarId); + } + + /** A builder for constructing {@link Registrar}, since it is immutable. */ + public static class Builder> + extends GenericBuilder { + public Builder() {} + + public Builder(T instance) { + super(instance); + } + + public B setRegistrarId(String registrarId) { + // Registrar id must be [3,16] chars long. See "clIDType" in the base EPP schema of RFC 5730. + // (Need to validate this here as there's no matching EPP XSD for validation.) + checkArgument( + Range.closed(3, 16).contains(registrarId.length()), + "Registrar ID must be 3-16 characters long."); + getInstance().registrarId = registrarId; + return thisCastToDerived(); + } + + public B setIanaIdentifier(@Nullable Long ianaIdentifier) { + checkArgument( + ianaIdentifier == null || ianaIdentifier > 0, "IANA ID must be a positive number"); + getInstance().ianaIdentifier = ianaIdentifier; + return thisCastToDerived(); + } + + public B setPoNumber(Optional poNumber) { + getInstance().poNumber = poNumber.orElse(null); + return thisCastToDerived(); + } + + public B setBillingAccountMap(@Nullable Map billingAccountMap) { + getInstance().billingAccountMap = nullToEmptyImmutableCopy(billingAccountMap); + return thisCastToDerived(); + } + + public B setRegistrarName(String registrarName) { + getInstance().registrarName = registrarName; + return thisCastToDerived(); + } + + public B setType(Type type) { + getInstance().type = type; + return thisCastToDerived(); + } + + public B setState(State state) { + getInstance().state = state; + return thisCastToDerived(); + } + + public B setAllowedTlds(Set allowedTlds) { + getInstance().allowedTlds = ImmutableSortedSet.copyOf(assertTldsExist(allowedTlds)); + return thisCastToDerived(); + } + + /** + * Same as {@link #setAllowedTlds}, but doesn't use the cache to check if the TLDs exist. + * + *

This should be used if the TLD we want to set is persisted in the same transaction - + * meaning its existence can't be cached before we need to save the Registrar. + * + *

We can still only set the allowedTld AFTER we saved the Registry entity. Make sure to call + * {@code .now()} when saving the Registry entity to make sure it's actually saved before trying + * to set the allowed TLDs. + */ + public B setAllowedTldsUncached(Set allowedTlds) { + ImmutableSet> newTldKeys = + Sets.difference(allowedTlds, getInstance().getAllowedTlds()).stream() + .map(Tld::createVKey) + .collect(toImmutableSet()); + Set> missingTldKeys = + Sets.difference(newTldKeys, tm().loadByKeysIfPresent(newTldKeys).keySet()); + checkArgument(missingTldKeys.isEmpty(), "Trying to set nonexistent TLDs: %s", missingTldKeys); + getInstance().allowedTlds = ImmutableSortedSet.copyOf(allowedTlds); + return thisCastToDerived(); + } + + public B setClientCertificate(String clientCertificate, DateTime now) { + clientCertificate = emptyToNull(clientCertificate); + String clientCertificateHash = calculateHash(clientCertificate); + if (!Objects.equals(clientCertificate, getInstance().clientCertificate) + || !Objects.equals(clientCertificateHash, getInstance().clientCertificateHash)) { + getInstance().clientCertificate = clientCertificate; + getInstance().clientCertificateHash = clientCertificateHash; + getInstance().lastCertificateUpdateTime = now; + } + return thisCastToDerived(); + } + + public B setLastExpiringCertNotificationSentDate(DateTime now) { + checkArgumentNotNull(now, "Registrar lastExpiringCertNotificationSentDate cannot be null"); + getInstance().lastExpiringCertNotificationSentDate = now; + return thisCastToDerived(); + } + + public B setLastExpiringFailoverCertNotificationSentDate(DateTime now) { + checkArgumentNotNull( + now, "Registrar lastExpiringFailoverCertNotificationSentDate cannot be null"); + getInstance().lastExpiringFailoverCertNotificationSentDate = now; + return thisCastToDerived(); + } + + public B setFailoverClientCertificate(String clientCertificate, DateTime now) { + clientCertificate = emptyToNull(clientCertificate); + String clientCertificateHash = calculateHash(clientCertificate); + if (!Objects.equals(clientCertificate, getInstance().failoverClientCertificate) + || !Objects.equals(clientCertificateHash, getInstance().failoverClientCertificateHash)) { + getInstance().failoverClientCertificate = clientCertificate; + getInstance().failoverClientCertificateHash = clientCertificateHash; + getInstance().lastCertificateUpdateTime = now; + } + return thisCastToDerived(); + } + + private static String calculateHash(String clientCertificate) { + if (clientCertificate == null) { + return null; + } + try { + return getCertificateHash(loadCertificate(clientCertificate)); + } catch (CertificateParsingException e) { + throw new IllegalArgumentException(e); + } + } + + // Making sure there's no registrar with the same ianaId already in the system + private static boolean isNotADuplicateIanaId( + Iterable registrars, RegistrarBase newInstance) { + // Return early if newly build registrar is not type REAL or ianaId is + // reserved by ICANN - https://www.iana.org/assignments/registrar-ids/registrar-ids.xhtml + if (!Type.REAL.equals(newInstance.type) + || ImmutableSet.of(1L, 8L).contains(newInstance.ianaIdentifier)) { + return true; + } + + return stream(registrars) + .filter(registrar -> Type.REAL.equals(registrar.getType())) + .filter(registrar -> !Objects.equals(newInstance.registrarId, registrar.getRegistrarId())) + .noneMatch( + registrar -> + Objects.equals(newInstance.ianaIdentifier, registrar.getIanaIdentifier())); + } + + public B setContactsRequireSyncing(boolean contactsRequireSyncing) { + getInstance().contactsRequireSyncing = contactsRequireSyncing; + return thisCastToDerived(); + } + + public B setIpAddressAllowList(Iterable ipAddressAllowList) { + getInstance().ipAddressAllowList = ImmutableList.copyOf(ipAddressAllowList); + return thisCastToDerived(); + } + + public B setLocalizedAddress(RegistrarAddress localizedAddress) { + getInstance().localizedAddress = localizedAddress; + return thisCastToDerived(); + } + + public B setInternationalizedAddress(RegistrarAddress internationalizedAddress) { + getInstance().internationalizedAddress = internationalizedAddress; + return thisCastToDerived(); + } + + public B setPhoneNumber(String phoneNumber) { + getInstance().phoneNumber = (phoneNumber == null) ? null : checkValidPhoneNumber(phoneNumber); + return thisCastToDerived(); + } + + public B setFaxNumber(String faxNumber) { + getInstance().faxNumber = (faxNumber == null) ? null : checkValidPhoneNumber(faxNumber); + return thisCastToDerived(); + } + + public B setEmailAddress(String emailAddress) { + getInstance().emailAddress = checkValidEmail(emailAddress); + return thisCastToDerived(); + } + + public B setWhoisServer(String whoisServer) { + getInstance().whoisServer = whoisServer; + return thisCastToDerived(); + } + + public B setRdapBaseUrls(Set rdapBaseUrls) { + getInstance().rdapBaseUrls = ImmutableSet.copyOf(rdapBaseUrls); + return thisCastToDerived(); + } + + public B setBlockPremiumNames(boolean blockPremiumNames) { + getInstance().blockPremiumNames = blockPremiumNames; + return thisCastToDerived(); + } + + public B setUrl(String url) { + getInstance().url = url; + return thisCastToDerived(); + } + + public B setIcannReferralEmail(String icannReferralEmail) { + getInstance().icannReferralEmail = checkValidEmail(icannReferralEmail); + return thisCastToDerived(); + } + + public B setDriveFolderId(@Nullable String driveFolderId) { + checkArgument( + driveFolderId == null || !driveFolderId.contains("/"), + "Drive folder ID must not be a full URL"); + getInstance().driveFolderId = driveFolderId; + return thisCastToDerived(); + } + + public B setPassword(String password) { + // Passwords must be [6,16] chars long. See "pwType" in the base EPP schema of RFC 5730. + checkArgument( + Range.closed(6, 16).contains(nullToEmpty(password).length()), + "Password must be 6-16 characters long."); + byte[] salt = SALT_SUPPLIER.get(); + getInstance().salt = base64().encode(salt); + getInstance().passwordHash = hashPassword(password, salt); + return thisCastToDerived(); + } + + /** + * Set the phone passcode. + * + * @throws IllegalArgumentException if provided passcode is not 5-digit numeric + */ + public B setPhonePasscode(String phonePasscode) { + checkArgument( + phonePasscode == null || PHONE_PASSCODE_PATTERN.matcher(phonePasscode).matches(), + "Not a valid telephone passcode (must be 5 digits long): %s", + phonePasscode); + getInstance().phonePasscode = phonePasscode; + return thisCastToDerived(); + } + + public B setRegistryLockAllowed(boolean registryLockAllowed) { + getInstance().registryLockAllowed = registryLockAllowed; + return thisCastToDerived(); + } + + /** + * This lets tests set the update timestamp in cases where setting fields resets the timestamp + * and breaks the verification that an object has not been updated since it was copied. + */ + @VisibleForTesting + public B setLastUpdateTime(DateTime timestamp) { + getInstance().setUpdateTimestamp(UpdateAutoTimestamp.create(timestamp)); + return thisCastToDerived(); + } + + /** Build the registrar, nullifying empty fields. */ + @Override + public T build() { + checkArgumentNotNull(getInstance().type, "Registrar type cannot be null"); + checkArgumentNotNull(getInstance().registrarName, "Registrar name cannot be null"); + checkArgument( + getInstance().localizedAddress != null || getInstance().internationalizedAddress != null, + "Must specify at least one of localized or internationalized address"); + checkArgument( + getInstance().type.isValidIanaId(getInstance().ianaIdentifier), + String.format( + "Supplied IANA ID is not valid for %s registrar type: %s", + getInstance().type, getInstance().ianaIdentifier)); + + // We do not allow creating Real registrars with IANA ID that's already in the system + // b/315007360 - for more details + checkArgument( + isNotADuplicateIanaId(loadAllCached(), getInstance()), + String.format( + "Rejected attempt to create a registrar with ianaId that's already in the system -" + + " %s", + getInstance().ianaIdentifier)); + + // In order to grant access to real TLDs, the registrar must have a corresponding billing + // account ID for that TLD's billing currency. + ImmutableSet nonBillableTlds = + Tld.get(getInstance().getAllowedTlds()).stream() + .filter(r -> r.getTldType() == TldType.REAL) + .filter(r -> !getInstance().getBillingAccountMap().containsKey(r.getCurrency())) + .map(Tld::getTldStr) + .collect(toImmutableSet()); + checkArgument( + nonBillableTlds.isEmpty(), + "Cannot set these allowed, real TLDs because their currency is missing " + + "from the billing account map: %s", + nonBillableTlds); + return cloneEmptyToNull(super.build()); + } + } + + /** Verifies that the email address in question is not null and has a valid format. */ + public static String checkValidEmail(String email) { + checkNotNull(email, "Provided email was null"); + try { + InternetAddress internetAddress = new InternetAddress(email, true); + internetAddress.validate(); + } catch (AddressException e) { + throw new IllegalArgumentException( + String.format("Provided email %s is not a valid email address", email)); + } + return email; + } + + /** Loads all registrar entities directly from the database. */ + public static Iterable loadAll() { + return tm().transact(() -> tm().loadAllOf(Registrar.class)); + } + + /** Loads all registrar entities using an in-memory cache. */ + public static Iterable loadAllCached() { + return CACHE_BY_REGISTRAR_ID.get().values(); + } + + /** Loads all registrar keys using an in-memory cache. */ + public static ImmutableSet> loadAllKeysCached() { + return CACHE_BY_REGISTRAR_ID.get().keySet().stream() + .map(Registrar::createVKey) + .collect(toImmutableSet()); + } + + /** Loads and returns a registrar entity by its id directly from the database. */ + public static Optional loadByRegistrarId(String registrarId) { + checkArgument(!Strings.isNullOrEmpty(registrarId), "registrarId must be specified"); + return tm().transact(() -> tm().loadByKeyIfPresent(createVKey(registrarId))); + } + + /** + * Loads and returns a registrar entity by its id using an in-memory cache. + * + *

Returns empty if the registrar isn't found. + */ + public static Optional loadByRegistrarIdCached(String registrarId) { + checkArgument(!Strings.isNullOrEmpty(registrarId), "registrarId must be specified"); + return Optional.ofNullable(CACHE_BY_REGISTRAR_ID.get().get(registrarId)); + } + + /** + * Loads and returns a registrar entity by its id using an in-memory cache. + * + *

Throws if the registrar isn't found. + */ + public static Registrar loadRequiredRegistrarCached(String registrarId) { + Optional registrar = loadByRegistrarIdCached(registrarId); + checkArgument(registrar.isPresent(), "couldn't find registrar '%s'", registrarId); + return registrar.get(); + } +} diff --git a/core/src/main/java/google/registry/model/registrar/RegistrarPoc.java b/core/src/main/java/google/registry/model/registrar/RegistrarPoc.java index f67377283..9a3a56569 100644 --- a/core/src/main/java/google/registry/model/registrar/RegistrarPoc.java +++ b/core/src/main/java/google/registry/model/registrar/RegistrarPoc.java @@ -14,35 +14,14 @@ package google.registry.model.registrar; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Strings.isNullOrEmpty; -import static com.google.common.collect.ImmutableSet.toImmutableSet; -import static com.google.common.io.BaseEncoding.base64; -import static google.registry.model.registrar.Registrar.checkValidEmail; -import static google.registry.persistence.transaction.TransactionManagerFactory.tm; -import static google.registry.util.CollectionUtils.nullToEmptyImmutableSortedCopy; -import static google.registry.util.PasswordUtils.SALT_SUPPLIER; -import static google.registry.util.PasswordUtils.hashPassword; -import static java.util.stream.Collectors.joining; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.ImmutableSortedSet; -import com.google.gson.annotations.Expose; -import google.registry.model.Buildable; import google.registry.model.ImmutableObject; -import google.registry.model.JsonMapBuilder; -import google.registry.model.Jsonifiable; -import google.registry.model.UnsafeSerializable; import google.registry.model.registrar.RegistrarPoc.RegistrarPocId; import google.registry.persistence.VKey; -import google.registry.util.PasswordUtils; import java.io.Serializable; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import javax.annotation.Nullable; +import javax.persistence.Access; +import javax.persistence.AccessType; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.IdClass; @@ -60,258 +39,20 @@ import javax.persistence.Table; @Entity @Table(indexes = @Index(columnList = "loginEmailAddress", name = "registrarpoc_login_email_idx")) @IdClass(RegistrarPocId.class) -public class RegistrarPoc extends ImmutableObject implements Jsonifiable, UnsafeSerializable { - - /** - * Registrar contacts types for partner communication tracking. - * - *

Note: These types only matter to the registry. They are not meant to be used for - * WHOIS or RDAP results. - */ - public enum Type { - ABUSE("abuse", true), - ADMIN("primary", true), - BILLING("billing", true), - LEGAL("legal", true), - MARKETING("marketing", false), - TECH("technical", true), - WHOIS("whois-inquiry", true); - - private final String displayName; - - private final boolean required; - - public String getDisplayName() { - return displayName; - } - - public boolean isRequired() { - return required; - } - - Type(String display, boolean required) { - displayName = display; - this.required = required; - } - } - - /** The name of the contact. */ - @Expose String name; - - /** - * The contact email address of the contact. - * - *

This is different from the login email which is assgined to the regstrar and cannot be - * changed. - */ - @Expose @Id String emailAddress; - - @Expose @Id public String registrarId; - - /** External email address of this contact used for registry lock confirmations. */ - String registryLockEmailAddress; - - /** The voice number of the contact. */ - @Expose String phoneNumber; - - /** The fax number of the contact. */ - @Expose String faxNumber; - - /** - * Multiple types are used to associate the registrar contact with various mailing groups. This - * data is internal to the registry. - */ - @Expose Set types; - - /** A GAIA email address that was assigned to the registrar for console login purpose. */ - String loginEmailAddress; - - /** - * Whether this contact is publicly visible in WHOIS registrar query results as an Admin contact. - */ - @Expose boolean visibleInWhoisAsAdmin = false; - - /** - * Whether this contact is publicly visible in WHOIS registrar query results as a Technical - * contact. - */ - @Expose boolean visibleInWhoisAsTech = false; - - /** - * Whether this contact's phone number and email address is publicly visible in WHOIS domain query - * results as registrar abuse contact info. - */ - @Expose boolean visibleInDomainWhoisAsAbuse = false; - - /** - * Whether the contact is allowed to set their registry lock password through the registrar - * console. This will be set to false on contact creation and when the user sets a password. - */ - boolean allowedToSetRegistryLockPassword = false; - - /** - * A hashed password that exists iff this contact is registry-lock-enabled. The hash is a base64 - * encoded SHA256 string. - */ - String registryLockPasswordHash; - - /** Randomly generated hash salt. */ - String registryLockPasswordSalt; - - /** - * Helper to update the contacts associated with a Registrar. This requires querying for the - * existing contacts, deleting existing contacts that are not part of the given {@code contacts} - * set, and then saving the given {@code contacts}. - * - *

IMPORTANT NOTE: If you call this method then it is your responsibility to also persist the - * relevant Registrar entity with the {@link Registrar#contactsRequireSyncing} field set to true. - */ - public static void updateContacts( - final Registrar registrar, final ImmutableSet contacts) { - tm().transact( - () -> { - ImmutableSet emailAddressesToKeep = - contacts.stream().map(RegistrarPoc::getEmailAddress).collect(toImmutableSet()); - tm().query( - "DELETE FROM RegistrarPoc WHERE registrarId = :registrarId AND " - + "emailAddress NOT IN :emailAddressesToKeep") - .setParameter("registrarId", registrar.getRegistrarId()) - .setParameter("emailAddressesToKeep", emailAddressesToKeep) - .executeUpdate(); - - tm().putAll(contacts); - }); - } - - public String getName() { - return name; - } +@Access(AccessType.FIELD) +public class RegistrarPoc extends RegistrarPocBase { + @Id + @Access(AccessType.PROPERTY) + @Override public String getEmailAddress() { return emailAddress; } - public Optional getRegistryLockEmailAddress() { - return Optional.ofNullable(registryLockEmailAddress); - } - - public String getPhoneNumber() { - return phoneNumber; - } - - public String getFaxNumber() { - return faxNumber; - } - - public ImmutableSortedSet getTypes() { - return nullToEmptyImmutableSortedCopy(types); - } - - public boolean getVisibleInWhoisAsAdmin() { - return visibleInWhoisAsAdmin; - } - - public boolean getVisibleInWhoisAsTech() { - return visibleInWhoisAsTech; - } - - public boolean getVisibleInDomainWhoisAsAbuse() { - return visibleInDomainWhoisAsAbuse; - } - - public String getLoginEmailAddress() { - return loginEmailAddress; - } - - public Builder asBuilder() { - return new Builder(clone(this)); - } - - public boolean isAllowedToSetRegistryLockPassword() { - return allowedToSetRegistryLockPassword; - } - - public boolean isRegistryLockAllowed() { - return !isNullOrEmpty(registryLockPasswordHash) && !isNullOrEmpty(registryLockPasswordSalt); - } - - public boolean verifyRegistryLockPassword(String registryLockPassword) { - if (isNullOrEmpty(registryLockPassword) - || isNullOrEmpty(registryLockPasswordSalt) - || isNullOrEmpty(registryLockPasswordHash)) { - return false; - } - return PasswordUtils.verifyPassword( - registryLockPassword, registryLockPasswordHash, registryLockPasswordSalt); - } - - /** - * Returns a string representation that's human friendly. - * - *

The output will look something like this: - * - *

{@code
-   * Some Person
-   * person@example.com
-   * Tel: +1.2125650666
-   * Types: [ADMIN, WHOIS]
-   * Visible in WHOIS as Admin contact: Yes
-   * Visible in WHOIS as Technical contact: No
-   * Registrar-Console access: Yes
-   * Login Email Address: person@registry.example
-   * }
- */ - public String toStringMultilinePlainText() { - StringBuilder result = new StringBuilder(256); - result.append(getName()).append('\n'); - result.append(getEmailAddress()).append('\n'); - if (phoneNumber != null) { - result.append("Tel: ").append(getPhoneNumber()).append('\n'); - } - if (faxNumber != null) { - result.append("Fax: ").append(getFaxNumber()).append('\n'); - } - result.append("Types: ").append(getTypes()).append('\n'); - result - .append("Visible in registrar WHOIS query as Admin contact: ") - .append(getVisibleInWhoisAsAdmin() ? "Yes" : "No") - .append('\n'); - result - .append("Visible in registrar WHOIS query as Technical contact: ") - .append(getVisibleInWhoisAsTech() ? "Yes" : "No") - .append('\n'); - result - .append( - "Phone number and email visible in domain WHOIS query as " - + "Registrar Abuse contact info: ") - .append(getVisibleInDomainWhoisAsAbuse() ? "Yes" : "No") - .append('\n'); - result - .append("Registrar-Console access: ") - .append(getLoginEmailAddress() != null ? "Yes" : "No") - .append('\n'); - if (getLoginEmailAddress() != null) { - result.append("Login Email Address: ").append(getLoginEmailAddress()).append('\n'); - } - return result.toString(); - } - - @Override - public Map toJsonMap() { - return new JsonMapBuilder() - .put("name", name) - .put("emailAddress", emailAddress) - .put("registryLockEmailAddress", registryLockEmailAddress) - .put("phoneNumber", phoneNumber) - .put("faxNumber", faxNumber) - .put("types", getTypes().stream().map(Object::toString).collect(joining(","))) - .put("visibleInWhoisAsAdmin", visibleInWhoisAsAdmin) - .put("visibleInWhoisAsTech", visibleInWhoisAsTech) - .put("visibleInDomainWhoisAsAbuse", visibleInDomainWhoisAsAbuse) - .put("allowedToSetRegistryLockPassword", allowedToSetRegistryLockPassword) - .put("registryLockAllowed", isRegistryLockAllowed()) - .put("loginEmailAddress", loginEmailAddress) - .build(); + @Id + @Access(AccessType.PROPERTY) + public String getRegistrarId() { + return registrarId; } @Override @@ -319,6 +60,11 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe return VKey.create(RegistrarPoc.class, new RegistrarPocId(emailAddress, registrarId)); } + @Override + public Builder asBuilder() { + return new Builder(clone(this)); + } + /** Class to represent the composite primary key for {@link RegistrarPoc} entity. */ @VisibleForTesting public static class RegistrarPocId extends ImmutableObject implements Serializable { @@ -336,112 +82,24 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe this.emailAddress = emailAddress; this.registrarId = registrarId; } + + @Id + public String getEmailAddress() { + return emailAddress; + } + + @Id + public String getRegistrarId() { + return registrarId; + } } - /** A builder for constructing a {@link RegistrarPoc}, since it is immutable. */ - public static class Builder extends Buildable.Builder { + public static class Builder extends RegistrarPocBase.Builder { + public Builder() {} - private Builder(RegistrarPoc instance) { - super(instance); - } - - /** Build the registrar, nullifying empty fields. */ - @Override - public RegistrarPoc build() { - checkNotNull(getInstance().registrarId, "Registrar ID cannot be null"); - checkValidEmail(getInstance().emailAddress); - // Check allowedToSetRegistryLockPassword here because if we want to allow the user to set - // a registry lock password, we must also set up the correct registry lock email concurrently - // or beforehand. - if (getInstance().allowedToSetRegistryLockPassword) { - checkArgument( - !isNullOrEmpty(getInstance().registryLockEmailAddress), - "Registry lock email must not be null if allowing registry lock access"); - } - return cloneEmptyToNull(super.build()); - } - - public Builder setName(String name) { - getInstance().name = name; - return this; - } - - public Builder setEmailAddress(String emailAddress) { - getInstance().emailAddress = emailAddress; - return this; - } - - public Builder setRegistryLockEmailAddress(@Nullable String registryLockEmailAddress) { - getInstance().registryLockEmailAddress = registryLockEmailAddress; - return this; - } - - public Builder setPhoneNumber(String phoneNumber) { - getInstance().phoneNumber = phoneNumber; - return this; - } - - public Builder setRegistrarId(String registrarId) { - getInstance().registrarId = registrarId; - return this; - } - - public Builder setRegistrar(Registrar registrar) { - getInstance().registrarId = registrar.getRegistrarId(); - return this; - } - - public Builder setFaxNumber(String faxNumber) { - getInstance().faxNumber = faxNumber; - return this; - } - - public Builder setTypes(Iterable types) { - getInstance().types = ImmutableSet.copyOf(types); - return this; - } - - public Builder setVisibleInWhoisAsAdmin(boolean visible) { - getInstance().visibleInWhoisAsAdmin = visible; - return this; - } - - public Builder setVisibleInWhoisAsTech(boolean visible) { - getInstance().visibleInWhoisAsTech = visible; - return this; - } - - public Builder setVisibleInDomainWhoisAsAbuse(boolean visible) { - getInstance().visibleInDomainWhoisAsAbuse = visible; - return this; - } - - public Builder setLoginEmailAddress(String loginEmailAddress) { - getInstance().loginEmailAddress = loginEmailAddress; - return this; - } - - public Builder setAllowedToSetRegistryLockPassword(boolean allowedToSetRegistryLockPassword) { - if (allowedToSetRegistryLockPassword) { - getInstance().registryLockPasswordSalt = null; - getInstance().registryLockPasswordHash = null; - } - getInstance().allowedToSetRegistryLockPassword = allowedToSetRegistryLockPassword; - return this; - } - - public Builder setRegistryLockPassword(String registryLockPassword) { - checkArgument( - getInstance().allowedToSetRegistryLockPassword, - "Not allowed to set registry lock password for this contact"); - checkArgument( - !isNullOrEmpty(registryLockPassword), "Registry lock password was null or empty"); - byte[] salt = SALT_SUPPLIER.get(); - getInstance().registryLockPasswordSalt = base64().encode(salt); - getInstance().registryLockPasswordHash = hashPassword(registryLockPassword, salt); - getInstance().allowedToSetRegistryLockPassword = false; - return this; + public Builder(RegistrarPoc registrarPoc) { + super(registrarPoc); } } } diff --git a/core/src/main/java/google/registry/model/registrar/RegistrarPocBase.java b/core/src/main/java/google/registry/model/registrar/RegistrarPocBase.java new file mode 100644 index 000000000..ad43446de --- /dev/null +++ b/core/src/main/java/google/registry/model/registrar/RegistrarPocBase.java @@ -0,0 +1,441 @@ +// 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.model.registrar; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.isNullOrEmpty; +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.google.common.io.BaseEncoding.base64; +import static google.registry.model.registrar.RegistrarBase.checkValidEmail; +import static google.registry.persistence.transaction.TransactionManagerFactory.tm; +import static google.registry.util.CollectionUtils.nullToEmptyImmutableSortedCopy; +import static google.registry.util.PasswordUtils.SALT_SUPPLIER; +import static google.registry.util.PasswordUtils.hashPassword; +import static java.util.stream.Collectors.joining; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSortedSet; +import com.google.gson.annotations.Expose; +import google.registry.model.Buildable.GenericBuilder; +import google.registry.model.ImmutableObject; +import google.registry.model.JsonMapBuilder; +import google.registry.model.Jsonifiable; +import google.registry.model.UnsafeSerializable; +import google.registry.util.PasswordUtils; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import javax.annotation.Nullable; +import javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.Embeddable; +import javax.persistence.Id; +import javax.persistence.MappedSuperclass; +import javax.persistence.Transient; + +/** + * A contact for a Registrar. Note, equality, hashCode and comparable have been overridden to only + * enable key equality. + * + *

IMPORTANT NOTE: Any time that you change, update, or delete RegistrarContact entities, you + * *MUST* also modify the persisted Registrar entity with {@link Registrar#contactsRequireSyncing} + * set to true. + * + *

This class deliberately does not include an {@link Id} so that any foreign-keyed fields can + * refer to the proper parent entity's ID, whether we're storing this in the DB itself or as part of + * another entity. + */ +@Access(AccessType.FIELD) +@Embeddable +@MappedSuperclass +public class RegistrarPocBase extends ImmutableObject implements Jsonifiable, UnsafeSerializable { + /** + * Registrar contacts types for partner communication tracking. + * + *

Note: These types only matter to the registry. They are not meant to be used for + * WHOIS or RDAP results. + */ + public enum Type { + ABUSE("abuse", true), + ADMIN("primary", true), + BILLING("billing", true), + LEGAL("legal", true), + MARKETING("marketing", false), + TECH("technical", true), + WHOIS("whois-inquiry", true); + + private final String displayName; + + private final boolean required; + + public String getDisplayName() { + return displayName; + } + + public boolean isRequired() { + return required; + } + + Type(String display, boolean required) { + displayName = display; + this.required = required; + } + } + + /** The name of the contact. */ + @Expose String name; + + /** + * The contact email address of the contact. + * + *

This is different from the login email which is assgined to the regstrar and cannot be + * changed. + */ + @Expose @Transient String emailAddress; + + @Expose @Transient public String registrarId; + + /** External email address of this contact used for registry lock confirmations. */ + String registryLockEmailAddress; + + /** The voice number of the contact. */ + @Expose String phoneNumber; + + /** The fax number of the contact. */ + @Expose String faxNumber; + + /** + * Multiple types are used to associate the registrar contact with various mailing groups. This + * data is internal to the registry. + */ + @Expose Set types; + + /** A GAIA email address that was assigned to the registrar for console login purpose. */ + String loginEmailAddress; + + /** + * Whether this contact is publicly visible in WHOIS registrar query results as an Admin contact. + */ + @Expose boolean visibleInWhoisAsAdmin = false; + + /** + * Whether this contact is publicly visible in WHOIS registrar query results as a Technical + * contact. + */ + @Expose boolean visibleInWhoisAsTech = false; + + /** + * Whether this contact's phone number and email address is publicly visible in WHOIS domain query + * results as registrar abuse contact info. + */ + @Expose boolean visibleInDomainWhoisAsAbuse = false; + + /** + * Whether the contact is allowed to set their registry lock password through the registrar + * console. This will be set to false on contact creation and when the user sets a password. + */ + boolean allowedToSetRegistryLockPassword = false; + + /** + * A hashed password that exists iff this contact is registry-lock-enabled. The hash is a base64 + * encoded SHA256 string. + */ + String registryLockPasswordHash; + + /** Randomly generated hash salt. */ + String registryLockPasswordSalt; + + /** + * Helper to update the contacts associated with a Registrar. This requires querying for the + * existing contacts, deleting existing contacts that are not part of the given {@code contacts} + * set, and then saving the given {@code contacts}. + * + *

IMPORTANT NOTE: If you call this method then it is your responsibility to also persist the + * relevant Registrar entity with the {@link Registrar#contactsRequireSyncing} field set to true. + */ + public static void updateContacts( + final Registrar registrar, final ImmutableSet contacts) { + tm().transact( + () -> { + ImmutableSet emailAddressesToKeep = + contacts.stream().map(RegistrarPoc::getEmailAddress).collect(toImmutableSet()); + tm().query( + "DELETE FROM RegistrarPoc WHERE registrarId = :registrarId AND " + + "emailAddress NOT IN :emailAddressesToKeep") + .setParameter("registrarId", registrar.getRegistrarId()) + .setParameter("emailAddressesToKeep", emailAddressesToKeep) + .executeUpdate(); + + tm().putAll(contacts); + }); + } + + public String getName() { + return name; + } + + public String getEmailAddress() { + return emailAddress; + } + + public Optional getRegistryLockEmailAddress() { + return Optional.ofNullable(registryLockEmailAddress); + } + + public String getPhoneNumber() { + return phoneNumber; + } + + public String getFaxNumber() { + return faxNumber; + } + + public ImmutableSortedSet getTypes() { + return nullToEmptyImmutableSortedCopy(types); + } + + public boolean getVisibleInWhoisAsAdmin() { + return visibleInWhoisAsAdmin; + } + + public boolean getVisibleInWhoisAsTech() { + return visibleInWhoisAsTech; + } + + public boolean getVisibleInDomainWhoisAsAbuse() { + return visibleInDomainWhoisAsAbuse; + } + + public String getLoginEmailAddress() { + return loginEmailAddress; + } + + public Builder asBuilder() { + return new Builder<>(clone(this)); + } + + public boolean isAllowedToSetRegistryLockPassword() { + return allowedToSetRegistryLockPassword; + } + + public boolean isRegistryLockAllowed() { + return !isNullOrEmpty(registryLockPasswordHash) && !isNullOrEmpty(registryLockPasswordSalt); + } + + public boolean verifyRegistryLockPassword(String registryLockPassword) { + if (isNullOrEmpty(registryLockPassword) + || isNullOrEmpty(registryLockPasswordSalt) + || isNullOrEmpty(registryLockPasswordHash)) { + return false; + } + return PasswordUtils.verifyPassword( + registryLockPassword, registryLockPasswordHash, registryLockPasswordSalt); + } + + /** + * Returns a string representation that's human friendly. + * + *

The output will look something like this: + * + *

{@code
+   * Some Person
+   * person@example.com
+   * Tel: +1.2125650666
+   * Types: [ADMIN, WHOIS]
+   * Visible in WHOIS as Admin contact: Yes
+   * Visible in WHOIS as Technical contact: No
+   * Registrar-Console access: Yes
+   * Login Email Address: person@registry.example
+   * }
+ */ + public String toStringMultilinePlainText() { + StringBuilder result = new StringBuilder(256); + result.append(getName()).append('\n'); + result.append(getEmailAddress()).append('\n'); + if (phoneNumber != null) { + result.append("Tel: ").append(getPhoneNumber()).append('\n'); + } + if (faxNumber != null) { + result.append("Fax: ").append(getFaxNumber()).append('\n'); + } + result.append("Types: ").append(getTypes()).append('\n'); + result + .append("Visible in registrar WHOIS query as Admin contact: ") + .append(getVisibleInWhoisAsAdmin() ? "Yes" : "No") + .append('\n'); + result + .append("Visible in registrar WHOIS query as Technical contact: ") + .append(getVisibleInWhoisAsTech() ? "Yes" : "No") + .append('\n'); + result + .append( + "Phone number and email visible in domain WHOIS query as " + + "Registrar Abuse contact info: ") + .append(getVisibleInDomainWhoisAsAbuse() ? "Yes" : "No") + .append('\n'); + result + .append("Registrar-Console access: ") + .append(getLoginEmailAddress() != null ? "Yes" : "No") + .append('\n'); + if (getLoginEmailAddress() != null) { + result.append("Login Email Address: ").append(getLoginEmailAddress()).append('\n'); + } + return result.toString(); + } + + @Override + public Map toJsonMap() { + return new JsonMapBuilder() + .put("name", name) + .put("emailAddress", emailAddress) + .put("registryLockEmailAddress", registryLockEmailAddress) + .put("phoneNumber", phoneNumber) + .put("faxNumber", faxNumber) + .put("types", getTypes().stream().map(Object::toString).collect(joining(","))) + .put("visibleInWhoisAsAdmin", visibleInWhoisAsAdmin) + .put("visibleInWhoisAsTech", visibleInWhoisAsTech) + .put("visibleInDomainWhoisAsAbuse", visibleInDomainWhoisAsAbuse) + .put("allowedToSetRegistryLockPassword", allowedToSetRegistryLockPassword) + .put("registryLockAllowed", isRegistryLockAllowed()) + .put("loginEmailAddress", loginEmailAddress) + .build(); + } + + /** + * These methods set the email address and registrar ID + * + *

This should only be used for restoring the fields of an object being loaded in a PostLoad + * method (effectively, when it is still under construction by Hibernate). In all other cases, the + * object should be regarded as immutable and changes should go through a Builder. + * + *

In addition to this special case use, this method must exist to satisfy Hibernate. + */ + public void setEmailAddress(String emailAddress) { + this.emailAddress = emailAddress; + } + + public void setRegistrarId(String registrarId) { + this.registrarId = registrarId; + } + + /** A builder for constructing a {@link RegistrarPoc}, since it is immutable. */ + public static class Builder> + extends GenericBuilder { + public Builder() {} + + protected Builder(T instance) { + super(instance); + } + + /** Build the registrar, nullifying empty fields. */ + @Override + public T build() { + checkNotNull(getInstance().registrarId, "Registrar ID cannot be null"); + checkValidEmail(getInstance().emailAddress); + // Check allowedToSetRegistryLockPassword here because if we want to allow the user to set + // a registry lock password, we must also set up the correct registry lock email concurrently + // or beforehand. + if (getInstance().allowedToSetRegistryLockPassword) { + checkArgument( + !isNullOrEmpty(getInstance().registryLockEmailAddress), + "Registry lock email must not be null if allowing registry lock access"); + } + return cloneEmptyToNull(super.build()); + } + + public B setName(String name) { + getInstance().name = name; + return thisCastToDerived(); + } + + public B setEmailAddress(String emailAddress) { + getInstance().emailAddress = emailAddress; + return thisCastToDerived(); + } + + public B setRegistryLockEmailAddress(@Nullable String registryLockEmailAddress) { + getInstance().registryLockEmailAddress = registryLockEmailAddress; + return thisCastToDerived(); + } + + public B setPhoneNumber(String phoneNumber) { + getInstance().phoneNumber = phoneNumber; + return thisCastToDerived(); + } + + public B setRegistrarId(String registrarId) { + getInstance().registrarId = registrarId; + return thisCastToDerived(); + } + + public B setRegistrar(Registrar registrar) { + getInstance().registrarId = registrar.getRegistrarId(); + return thisCastToDerived(); + } + + public B setFaxNumber(String faxNumber) { + getInstance().faxNumber = faxNumber; + return thisCastToDerived(); + } + + public B setTypes(Iterable types) { + getInstance().types = ImmutableSet.copyOf(types); + return thisCastToDerived(); + } + + public B setVisibleInWhoisAsAdmin(boolean visible) { + getInstance().visibleInWhoisAsAdmin = visible; + return thisCastToDerived(); + } + + public B setVisibleInWhoisAsTech(boolean visible) { + getInstance().visibleInWhoisAsTech = visible; + return thisCastToDerived(); + } + + public B setVisibleInDomainWhoisAsAbuse(boolean visible) { + getInstance().visibleInDomainWhoisAsAbuse = visible; + return thisCastToDerived(); + } + + public B setLoginEmailAddress(String loginEmailAddress) { + getInstance().loginEmailAddress = loginEmailAddress; + return thisCastToDerived(); + } + + public B setAllowedToSetRegistryLockPassword(boolean allowedToSetRegistryLockPassword) { + if (allowedToSetRegistryLockPassword) { + getInstance().registryLockPasswordSalt = null; + getInstance().registryLockPasswordHash = null; + } + getInstance().allowedToSetRegistryLockPassword = allowedToSetRegistryLockPassword; + return thisCastToDerived(); + } + + public B setRegistryLockPassword(String registryLockPassword) { + checkArgument( + getInstance().allowedToSetRegistryLockPassword, + "Not allowed to set registry lock password for this contact"); + checkArgument( + !isNullOrEmpty(registryLockPassword), "Registry lock password was null or empty"); + byte[] salt = SALT_SUPPLIER.get(); + getInstance().registryLockPasswordSalt = base64().encode(salt); + getInstance().registryLockPasswordHash = hashPassword(registryLockPassword, salt); + getInstance().allowedToSetRegistryLockPassword = false; + return thisCastToDerived(); + } + } +} diff --git a/core/src/main/java/google/registry/persistence/converter/RegistrarPocSetConverter.java b/core/src/main/java/google/registry/persistence/converter/RegistrarPocSetConverter.java index 4c93fc35a..09a3e4038 100644 --- a/core/src/main/java/google/registry/persistence/converter/RegistrarPocSetConverter.java +++ b/core/src/main/java/google/registry/persistence/converter/RegistrarPocSetConverter.java @@ -14,7 +14,7 @@ package google.registry.persistence.converter; -import google.registry.model.registrar.RegistrarPoc.Type; +import google.registry.model.registrar.RegistrarPocBase.Type; import javax.persistence.AttributeConverter; import javax.persistence.Converter; diff --git a/core/src/main/java/google/registry/rde/RegistrarToXjcConverter.java b/core/src/main/java/google/registry/rde/RegistrarToXjcConverter.java index f19104e10..16c22c328 100644 --- a/core/src/main/java/google/registry/rde/RegistrarToXjcConverter.java +++ b/core/src/main/java/google/registry/rde/RegistrarToXjcConverter.java @@ -19,8 +19,9 @@ import static com.google.common.base.Preconditions.checkState; import com.google.common.collect.ImmutableMap; import google.registry.model.registrar.Registrar; -import google.registry.model.registrar.Registrar.State; import google.registry.model.registrar.RegistrarAddress; +import google.registry.model.registrar.RegistrarBase; +import google.registry.model.registrar.RegistrarBase.State; import google.registry.xjc.contact.XjcContactE164Type; import google.registry.xjc.rderegistrar.XjcRdeRegistrar; import google.registry.xjc.rderegistrar.XjcRdeRegistrarAddrType; @@ -40,7 +41,7 @@ final class RegistrarToXjcConverter { private static final String UNKNOWN_CC = "US"; /** A conversion map between internal Registrar states and external RDE states. */ - private static final ImmutableMap + private static final ImmutableMap REGISTRAR_STATUS_CONVERSIONS = ImmutableMap.of( State.ACTIVE, XjcRdeRegistrarStatusType.OK, diff --git a/core/src/main/java/google/registry/request/auth/AuthenticatedRegistrarAccessor.java b/core/src/main/java/google/registry/request/auth/AuthenticatedRegistrarAccessor.java index 7bc6a8fec..9d6aa143b 100644 --- a/core/src/main/java/google/registry/request/auth/AuthenticatedRegistrarAccessor.java +++ b/core/src/main/java/google/registry/request/auth/AuthenticatedRegistrarAccessor.java @@ -28,7 +28,7 @@ import dagger.Lazy; import google.registry.config.RegistryConfig.Config; import google.registry.groups.GroupsConnection; import google.registry.model.registrar.Registrar; -import google.registry.model.registrar.Registrar.State; +import google.registry.model.registrar.RegistrarBase.State; import google.registry.model.registrar.RegistrarPoc; import java.util.Optional; import javax.annotation.concurrent.Immutable; diff --git a/core/src/main/java/google/registry/tools/CreateOrUpdateRegistrarCommand.java b/core/src/main/java/google/registry/tools/CreateOrUpdateRegistrarCommand.java index 987b1bb54..35c27ec79 100644 --- a/core/src/main/java/google/registry/tools/CreateOrUpdateRegistrarCommand.java +++ b/core/src/main/java/google/registry/tools/CreateOrUpdateRegistrarCommand.java @@ -31,6 +31,7 @@ import com.google.common.collect.ImmutableSet; import google.registry.flows.certs.CertificateChecker; import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarAddress; +import google.registry.model.registrar.RegistrarBase; import google.registry.tools.params.KeyValueMapParameter.CurrencyUnitToStringMap; import google.registry.tools.params.OptionalLongParameter; import google.registry.tools.params.OptionalPhoneNumberParameter; @@ -59,16 +60,12 @@ abstract class CreateOrUpdateRegistrarCommand extends MutatingCommand { @Parameter(description = "Client identifier of the registrar account", required = true) List mainParameters; - @Parameter( - names = "--registrar_type", - description = "Type of the registrar") - Registrar.Type registrarType; + @Parameter(names = "--registrar_type", description = "Type of the registrar") + RegistrarBase.Type registrarType; @Nullable - @Parameter( - names = "--registrar_state", - description = "Initial state of the registrar") - Registrar.State registrarState; + @Parameter(names = "--registrar_state", description = "Initial state of the registrar") + RegistrarBase.State registrarState; @Parameter( names = "--allowed_tlds", diff --git a/core/src/main/java/google/registry/tools/CreateRegistrarCommand.java b/core/src/main/java/google/registry/tools/CreateRegistrarCommand.java index e5c1cf885..c86ce867e 100644 --- a/core/src/main/java/google/registry/tools/CreateRegistrarCommand.java +++ b/core/src/main/java/google/registry/tools/CreateRegistrarCommand.java @@ -18,7 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Strings.emptyToNull; import static com.google.common.collect.Iterables.getOnlyElement; -import static google.registry.model.registrar.Registrar.State.ACTIVE; +import static google.registry.model.registrar.RegistrarBase.State.ACTIVE; import static google.registry.tools.RegistryToolEnvironment.PRODUCTION; import static google.registry.tools.RegistryToolEnvironment.SANDBOX; import static google.registry.tools.RegistryToolEnvironment.UNITTEST; diff --git a/core/src/main/java/google/registry/tools/RegistrarPocCommand.java b/core/src/main/java/google/registry/tools/RegistrarPocCommand.java index c43c25fb4..173545eb8 100644 --- a/core/src/main/java/google/registry/tools/RegistrarPocCommand.java +++ b/core/src/main/java/google/registry/tools/RegistrarPocCommand.java @@ -30,6 +30,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarPoc; +import google.registry.model.registrar.RegistrarPocBase; import google.registry.tools.params.OptionalPhoneNumberParameter; import google.registry.tools.params.PathParameter; import google.registry.tools.params.StringListParameter; @@ -167,7 +168,7 @@ final class RegistrarPocCommand extends MutatingCommand { private static final ImmutableSet MODES_REQUIRING_CONTACT_SYNC = ImmutableSet.of(Mode.CREATE, Mode.UPDATE, Mode.DELETE); - @Nullable private ImmutableSet contactTypes; + @Nullable private ImmutableSet contactTypes; @Override protected void init() throws Exception { @@ -183,7 +184,7 @@ final class RegistrarPocCommand extends MutatingCommand { } else { contactTypes = contactTypeNames.stream() - .map(Enums.stringConverter(RegistrarPoc.Type.class)) + .map(Enums.stringConverter(RegistrarPocBase.Type.class)) .collect(toImmutableSet()); } ImmutableSet contacts = registrar.getContacts(); diff --git a/core/src/main/java/google/registry/tools/server/CreateGroupsAction.java b/core/src/main/java/google/registry/tools/server/CreateGroupsAction.java index cfea189a9..30b160393 100644 --- a/core/src/main/java/google/registry/tools/server/CreateGroupsAction.java +++ b/core/src/main/java/google/registry/tools/server/CreateGroupsAction.java @@ -24,7 +24,7 @@ import google.registry.config.RegistryConfig.Config; import google.registry.groups.GroupsConnection; import google.registry.groups.GroupsConnection.Role; import google.registry.model.registrar.Registrar; -import google.registry.model.registrar.RegistrarPoc; +import google.registry.model.registrar.RegistrarPocBase; import google.registry.request.Action; import google.registry.request.HttpException.BadRequestException; import google.registry.request.HttpException.InternalServerErrorException; @@ -64,7 +64,7 @@ public class CreateGroupsAction implements Runnable { if (registrar == null) { return; } - List types = asList(RegistrarPoc.Type.values()); + List types = asList(RegistrarPocBase.Type.values()); // Concurrently create the groups for each RegistrarContact.Type, collecting the results from // each call (which are either an Exception if it failed, or absent() if it succeeded). List> results = diff --git a/core/src/main/java/google/registry/ui/server/RegistrarFormFields.java b/core/src/main/java/google/registry/ui/server/RegistrarFormFields.java index e42a64de7..3f646c88c 100644 --- a/core/src/main/java/google/registry/ui/server/RegistrarFormFields.java +++ b/core/src/main/java/google/registry/ui/server/RegistrarFormFields.java @@ -29,6 +29,7 @@ import com.google.re2j.Pattern; import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarAddress; import google.registry.model.registrar.RegistrarPoc; +import google.registry.model.registrar.RegistrarPocBase; import google.registry.ui.forms.FormException; import google.registry.ui.forms.FormField; import google.registry.ui.forms.FormFieldException; @@ -90,12 +91,6 @@ public final class RegistrarFormFields { .matches(ASCII_PATTERN, ASCII_ERROR) .build(); - public static final FormField STATE_FIELD = - FormField.named("state") - .emptyToNull() - .asEnum(Registrar.State.class) - .build(); - public static final FormField, Set> ALLOWED_TLDS_FIELD = FormFields.LABEL.asBuilderNamed("allowedTlds") .asSet() @@ -106,14 +101,6 @@ public final class RegistrarFormFields { .transform(RegistrarFormFields::parseHostname) .build(); - public static final FormField BLOCK_PREMIUM_NAMES_FIELD = - FormField.named("blockPremiumNames", Boolean.class) - .build(); - - public static final FormField DRIVE_FOLDER_ID_FIELD = - FormFields.XS_NORMALIZED_STRING.asBuilderNamed("driveFolderId") - .build(); - public static final FormField CLIENT_CERTIFICATE_HASH_FIELD = FormField.named("clientCertificateHash") .emptyToNull() @@ -217,10 +204,10 @@ public final class RegistrarFormFields { public static final FormField CONTACT_REGISTRY_LOCK_PASSWORD_FIELD = FormFields.NAME.asBuilderNamed("registryLockPassword").build(); - public static final FormField> CONTACT_TYPES = + public static final FormField> CONTACT_TYPES = FormField.named("types") .uppercased() - .asEnum(RegistrarPoc.Type.class) + .asEnum(RegistrarPocBase.Type.class) .asSet(Splitter.on(',').omitEmptyStrings().trimResults()) .build(); 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 c0b576a19..7dac07155 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 @@ -27,7 +27,7 @@ import com.google.gson.Gson; import google.registry.model.console.ConsolePermission; import google.registry.model.console.User; import google.registry.model.registrar.Registrar; -import google.registry.model.registrar.Registrar.State; +import google.registry.model.registrar.RegistrarBase.State; import google.registry.model.registrar.RegistrarPoc; import google.registry.request.Action; import google.registry.request.Parameter; diff --git a/core/src/main/java/google/registry/ui/server/registrar/ConsoleRegistrarCreatorAction.java b/core/src/main/java/google/registry/ui/server/registrar/ConsoleRegistrarCreatorAction.java index 4344e29be..c65349fec 100644 --- a/core/src/main/java/google/registry/ui/server/registrar/ConsoleRegistrarCreatorAction.java +++ b/core/src/main/java/google/registry/ui/server/registrar/ConsoleRegistrarCreatorAction.java @@ -27,8 +27,8 @@ import com.google.common.collect.ImmutableMap; import com.google.common.flogger.FluentLogger; import com.google.template.soy.tofu.SoyTofu; import google.registry.model.registrar.Registrar; -import google.registry.model.registrar.Registrar.State; import google.registry.model.registrar.RegistrarAddress; +import google.registry.model.registrar.RegistrarBase.State; import google.registry.model.registrar.RegistrarPoc; import google.registry.request.Action; import google.registry.request.Action.Method; diff --git a/core/src/main/java/google/registry/ui/server/registrar/OteStatusAction.java b/core/src/main/java/google/registry/ui/server/registrar/OteStatusAction.java index fd0408158..190c1c06a 100644 --- a/core/src/main/java/google/registry/ui/server/registrar/OteStatusAction.java +++ b/core/src/main/java/google/registry/ui/server/registrar/OteStatusAction.java @@ -26,7 +26,7 @@ import google.registry.model.OteAccountBuilder; import google.registry.model.OteStats; import google.registry.model.OteStats.StatType; import google.registry.model.registrar.Registrar; -import google.registry.model.registrar.Registrar.Type; +import google.registry.model.registrar.RegistrarBase.Type; import google.registry.request.Action; import google.registry.request.JsonActionRunner; import google.registry.request.auth.Auth; diff --git a/core/src/main/java/google/registry/ui/server/registrar/RegistrarSettingsAction.java b/core/src/main/java/google/registry/ui/server/registrar/RegistrarSettingsAction.java index ca7d78739..c80fa4909 100644 --- a/core/src/main/java/google/registry/ui/server/registrar/RegistrarSettingsAction.java +++ b/core/src/main/java/google/registry/ui/server/registrar/RegistrarSettingsAction.java @@ -41,7 +41,7 @@ import google.registry.flows.certs.CertificateChecker; import google.registry.flows.certs.CertificateChecker.InsecureCertificateException; import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarPoc; -import google.registry.model.registrar.RegistrarPoc.Type; +import google.registry.model.registrar.RegistrarPocBase.Type; import google.registry.request.Action; import google.registry.request.Action.Service; import google.registry.request.HttpException.BadRequestException; diff --git a/core/src/main/resources/META-INF/persistence.xml b/core/src/main/resources/META-INF/persistence.xml index f1a1418fe..7df4384d6 100644 --- a/core/src/main/resources/META-INF/persistence.xml +++ b/core/src/main/resources/META-INF/persistence.xml @@ -47,7 +47,11 @@ google.registry.model.billing.BillingRecurrence google.registry.model.common.Cursor google.registry.model.common.DnsRefreshRequest + google.registry.model.console.ConsoleEppActionHistory + google.registry.model.console.RegistrarPocUpdateHistory + google.registry.model.console.RegistrarUpdateHistory google.registry.model.console.User + google.registry.model.console.UserUpdateHistory google.registry.model.contact.ContactHistory google.registry.model.contact.Contact google.registry.model.domain.Domain diff --git a/core/src/test/java/google/registry/batch/SendExpiringCertificateNotificationEmailActionTest.java b/core/src/test/java/google/registry/batch/SendExpiringCertificateNotificationEmailActionTest.java index b2021761d..2b0cd2b1a 100644 --- a/core/src/test/java/google/registry/batch/SendExpiringCertificateNotificationEmailActionTest.java +++ b/core/src/test/java/google/registry/batch/SendExpiringCertificateNotificationEmailActionTest.java @@ -36,7 +36,8 @@ import google.registry.groups.GmailClient; import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarAddress; import google.registry.model.registrar.RegistrarPoc; -import google.registry.model.registrar.RegistrarPoc.Type; +import google.registry.model.registrar.RegistrarPocBase; +import google.registry.model.registrar.RegistrarPocBase.Type; import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension; import google.registry.testing.FakeClock; @@ -223,7 +224,7 @@ class SendExpiringCertificateNotificationEmailActionTest { .setEmailAddress("will@example-registrar.tld") .setPhoneNumber("+1.3105551213") .setFaxNumber("+1.3105551213") - .setTypes(ImmutableSet.of(RegistrarPoc.Type.TECH)) + .setTypes(ImmutableSet.of(RegistrarPocBase.Type.TECH)) .setVisibleInWhoisAsAdmin(true) .setVisibleInWhoisAsTech(false) .build()); @@ -513,7 +514,7 @@ class SendExpiringCertificateNotificationEmailActionTest { .setEmailAddress("jd@example-registrar.tld") .setPhoneNumber("+1.3105551213") .setFaxNumber("+1.3105551213") - .setTypes(ImmutableSet.of(RegistrarPoc.Type.TECH)) + .setTypes(ImmutableSet.of(RegistrarPocBase.Type.TECH)) .setVisibleInWhoisAsAdmin(true) .setVisibleInWhoisAsTech(false) .build(), @@ -523,7 +524,7 @@ class SendExpiringCertificateNotificationEmailActionTest { .setEmailAddress("js@example-registrar.tld") .setPhoneNumber("+1.1111111111") .setFaxNumber("+1.1111111111") - .setTypes(ImmutableSet.of(RegistrarPoc.Type.TECH)) + .setTypes(ImmutableSet.of(RegistrarPocBase.Type.TECH)) .build(), new RegistrarPoc.Builder() .setRegistrar(registrar) @@ -531,7 +532,7 @@ class SendExpiringCertificateNotificationEmailActionTest { .setEmailAddress("will@example-registrar.tld") .setPhoneNumber("+1.3105551213") .setFaxNumber("+1.3105551213") - .setTypes(ImmutableSet.of(RegistrarPoc.Type.TECH)) + .setTypes(ImmutableSet.of(RegistrarPocBase.Type.TECH)) .setVisibleInWhoisAsAdmin(true) .setVisibleInWhoisAsTech(false) .build(), @@ -541,7 +542,7 @@ class SendExpiringCertificateNotificationEmailActionTest { .setEmailAddress("mike@example-registrar.tld") .setPhoneNumber("+1.1111111111") .setFaxNumber("+1.1111111111") - .setTypes(ImmutableSet.of(RegistrarPoc.Type.ADMIN)) + .setTypes(ImmutableSet.of(RegistrarPocBase.Type.ADMIN)) .build(), new RegistrarPoc.Builder() .setRegistrar(registrar) @@ -549,7 +550,7 @@ class SendExpiringCertificateNotificationEmailActionTest { .setEmailAddress("john@example-registrar.tld") .setPhoneNumber("+1.3105551215") .setFaxNumber("+1.3105551216") - .setTypes(ImmutableSet.of(RegistrarPoc.Type.ADMIN)) + .setTypes(ImmutableSet.of(RegistrarPocBase.Type.ADMIN)) .setVisibleInWhoisAsTech(true) .build()); persistSimpleResources(contacts); @@ -703,7 +704,7 @@ class SendExpiringCertificateNotificationEmailActionTest { /** Returns persisted sample contacts with a customized contact email type. */ private static ImmutableList persistSampleContacts( - Registrar registrar, RegistrarPoc.Type emailType) { + Registrar registrar, RegistrarPocBase.Type emailType) { return persistSimpleResources( ImmutableList.of( new RegistrarPoc.Builder() diff --git a/core/src/test/java/google/registry/beam/rde/RdePipelineTest.java b/core/src/test/java/google/registry/beam/rde/RdePipelineTest.java index cf6a4568c..99ded6601 100644 --- a/core/src/test/java/google/registry/beam/rde/RdePipelineTest.java +++ b/core/src/test/java/google/registry/beam/rde/RdePipelineTest.java @@ -69,7 +69,7 @@ import google.registry.model.rde.RdeMode; import google.registry.model.rde.RdeRevision; import google.registry.model.rde.RdeRevision.RdeRevisionId; import google.registry.model.registrar.Registrar; -import google.registry.model.registrar.Registrar.State; +import google.registry.model.registrar.RegistrarBase.State; import google.registry.model.reporting.DomainTransactionRecord; import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField; import google.registry.model.reporting.HistoryEntry; diff --git a/core/src/test/java/google/registry/export/SyncGroupMembersActionTest.java b/core/src/test/java/google/registry/export/SyncGroupMembersActionTest.java index b306e339e..d1f25d645 100644 --- a/core/src/test/java/google/registry/export/SyncGroupMembersActionTest.java +++ b/core/src/test/java/google/registry/export/SyncGroupMembersActionTest.java @@ -16,9 +16,9 @@ package google.registry.export; import static com.google.common.truth.Truth.assertThat; import static google.registry.export.SyncGroupMembersAction.getGroupEmailAddressForContactType; -import static google.registry.model.registrar.RegistrarPoc.Type.ADMIN; -import static google.registry.model.registrar.RegistrarPoc.Type.MARKETING; -import static google.registry.model.registrar.RegistrarPoc.Type.TECH; +import static google.registry.model.registrar.RegistrarPocBase.Type.ADMIN; +import static google.registry.model.registrar.RegistrarPocBase.Type.MARKETING; +import static google.registry.model.registrar.RegistrarPocBase.Type.TECH; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.testing.DatabaseHelper.loadRegistrar; import static google.registry.testing.DatabaseHelper.persistResource; diff --git a/core/src/test/java/google/registry/export/sheet/SyncRegistrarsSheetTest.java b/core/src/test/java/google/registry/export/sheet/SyncRegistrarsSheetTest.java index 28e35b7de..8531faf49 100644 --- a/core/src/test/java/google/registry/export/sheet/SyncRegistrarsSheetTest.java +++ b/core/src/test/java/google/registry/export/sheet/SyncRegistrarsSheetTest.java @@ -38,6 +38,7 @@ import google.registry.model.common.Cursor; import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarAddress; import google.registry.model.registrar.RegistrarPoc; +import google.registry.model.registrar.RegistrarPocBase; import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension; import google.registry.testing.DatabaseHelper; @@ -157,7 +158,8 @@ public class SyncRegistrarsSheetTest { .setName("Jane Doe") .setEmailAddress("contact@example.com") .setPhoneNumber("+1.1234567890") - .setTypes(ImmutableSet.of(RegistrarPoc.Type.ADMIN, RegistrarPoc.Type.BILLING)) + .setTypes( + ImmutableSet.of(RegistrarPocBase.Type.ADMIN, RegistrarPocBase.Type.BILLING)) .build(), new RegistrarPoc.Builder() .setRegistrar(registrar) @@ -165,7 +167,7 @@ public class SyncRegistrarsSheetTest { .setEmailAddress("john.doe@example.tld") .setPhoneNumber("+1.1234567890") .setFaxNumber("+1.1234567891") - .setTypes(ImmutableSet.of(RegistrarPoc.Type.ADMIN)) + .setTypes(ImmutableSet.of(RegistrarPocBase.Type.ADMIN)) // Purposely flip the internal/external admin/tech // distinction to make sure we're not relying on it. Sigh. .setVisibleInWhoisAsAdmin(false) @@ -176,7 +178,7 @@ public class SyncRegistrarsSheetTest { .setRegistrar(registrar) .setName("Jane Smith") .setEmailAddress("pride@example.net") - .setTypes(ImmutableSet.of(RegistrarPoc.Type.TECH)) + .setTypes(ImmutableSet.of(RegistrarPocBase.Type.TECH)) .build()); // Use registrar key for contacts' parent. DateTime registrarCreationTime = persistResource(registrar).getCreationTime(); diff --git a/core/src/test/java/google/registry/flows/domain/DomainCreateFlowTest.java b/core/src/test/java/google/registry/flows/domain/DomainCreateFlowTest.java index be8e2c2d5..a6073539b 100644 --- a/core/src/test/java/google/registry/flows/domain/DomainCreateFlowTest.java +++ b/core/src/test/java/google/registry/flows/domain/DomainCreateFlowTest.java @@ -173,7 +173,7 @@ import google.registry.model.eppoutput.EppResponse; import google.registry.model.poll.PendingActionNotificationResponse.DomainPendingActionNotificationResponse; import google.registry.model.poll.PollMessage; import google.registry.model.registrar.Registrar; -import google.registry.model.registrar.Registrar.State; +import google.registry.model.registrar.RegistrarBase.State; import google.registry.model.reporting.DomainTransactionRecord; import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField; import google.registry.model.reporting.HistoryEntry; diff --git a/core/src/test/java/google/registry/flows/domain/DomainRenewFlowTest.java b/core/src/test/java/google/registry/flows/domain/DomainRenewFlowTest.java index 5f863633f..0e51bef0f 100644 --- a/core/src/test/java/google/registry/flows/domain/DomainRenewFlowTest.java +++ b/core/src/test/java/google/registry/flows/domain/DomainRenewFlowTest.java @@ -93,7 +93,7 @@ import google.registry.model.domain.token.AllocationToken.TokenStatus; import google.registry.model.eppcommon.StatusValue; import google.registry.model.poll.PollMessage; import google.registry.model.registrar.Registrar; -import google.registry.model.registrar.Registrar.State; +import google.registry.model.registrar.RegistrarBase.State; import google.registry.model.reporting.DomainTransactionRecord; import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField; import google.registry.model.reporting.HistoryEntry; diff --git a/core/src/test/java/google/registry/flows/domain/DomainRestoreRequestFlowTest.java b/core/src/test/java/google/registry/flows/domain/DomainRestoreRequestFlowTest.java index ff058b73a..6a40744bd 100644 --- a/core/src/test/java/google/registry/flows/domain/DomainRestoreRequestFlowTest.java +++ b/core/src/test/java/google/registry/flows/domain/DomainRestoreRequestFlowTest.java @@ -69,7 +69,7 @@ import google.registry.model.domain.rgp.GracePeriodStatus; import google.registry.model.eppcommon.StatusValue; import google.registry.model.poll.PollMessage; import google.registry.model.registrar.Registrar; -import google.registry.model.registrar.Registrar.State; +import google.registry.model.registrar.RegistrarBase.State; import google.registry.model.reporting.DomainTransactionRecord; import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField; import google.registry.model.reporting.HistoryEntry; diff --git a/core/src/test/java/google/registry/flows/domain/DomainTransferRequestFlowTest.java b/core/src/test/java/google/registry/flows/domain/DomainTransferRequestFlowTest.java index bb6a73edf..8b1d1ea1a 100644 --- a/core/src/test/java/google/registry/flows/domain/DomainTransferRequestFlowTest.java +++ b/core/src/test/java/google/registry/flows/domain/DomainTransferRequestFlowTest.java @@ -114,7 +114,7 @@ import google.registry.model.eppcommon.Trid; import google.registry.model.poll.PendingActionNotificationResponse; import google.registry.model.poll.PollMessage; import google.registry.model.registrar.Registrar; -import google.registry.model.registrar.Registrar.State; +import google.registry.model.registrar.RegistrarBase.State; import google.registry.model.reporting.DomainTransactionRecord; import google.registry.model.reporting.HistoryEntry; import google.registry.model.reporting.HistoryEntry.HistoryEntryId; diff --git a/core/src/test/java/google/registry/flows/session/LoginFlowTestCase.java b/core/src/test/java/google/registry/flows/session/LoginFlowTestCase.java index fbb338bcc..7fd7c50e3 100644 --- a/core/src/test/java/google/registry/flows/session/LoginFlowTestCase.java +++ b/core/src/test/java/google/registry/flows/session/LoginFlowTestCase.java @@ -36,7 +36,7 @@ import google.registry.flows.session.LoginFlow.TooManyFailedLoginsException; import google.registry.flows.session.LoginFlow.UnsupportedLanguageException; import google.registry.model.eppoutput.EppOutput; import google.registry.model.registrar.Registrar; -import google.registry.model.registrar.Registrar.State; +import google.registry.model.registrar.RegistrarBase.State; import google.registry.testing.DatabaseHelper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/core/src/test/java/google/registry/model/console/ConsoleEppActionHistoryTest.java b/core/src/test/java/google/registry/model/console/ConsoleEppActionHistoryTest.java new file mode 100644 index 000000000..53bcdd669 --- /dev/null +++ b/core/src/test/java/google/registry/model/console/ConsoleEppActionHistoryTest.java @@ -0,0 +1,68 @@ +// 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.model.console; + +import static com.google.common.collect.Iterables.getOnlyElement; +import static com.google.common.truth.Truth.assertThat; +import static google.registry.persistence.transaction.TransactionManagerFactory.tm; +import static google.registry.testing.DatabaseHelper.createTld; +import static google.registry.testing.DatabaseHelper.persistActiveContact; +import static google.registry.testing.DatabaseHelper.persistDomainWithDependentResources; + +import google.registry.model.EntityTestCase; +import google.registry.model.domain.DomainHistory; +import google.registry.testing.DatabaseHelper; +import google.registry.util.DateTimeUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** Tests for {@link ConsoleEppActionHistory}. */ +public class ConsoleEppActionHistoryTest extends EntityTestCase { + + ConsoleEppActionHistoryTest() { + 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 = DatabaseHelper.createAdminUser("email@email.com"); + DomainHistory domainHistory = getOnlyElement(DatabaseHelper.loadAllOf(DomainHistory.class)); + ConsoleEppActionHistory history = + new ConsoleEppActionHistory.Builder() + .setType(ConsoleUpdateHistory.Type.EPP_ACTION) + .setActingUser(user) + .setModificationTime(fakeClock.nowUtc()) + .setMethod("POST") + .setUrl("https://some/url/for/creating/a/domain") + .setHistoryEntryClass(DomainHistory.class) + .setHistoryEntryId(domainHistory.getHistoryEntryId()) + .build(); + tm().transact(() -> tm().put(history)); + assertThat(getOnlyElement(DatabaseHelper.loadAllOf(ConsoleEppActionHistory.class))) + .isEqualTo(history); + } +} diff --git a/core/src/test/java/google/registry/model/console/RegistrarPocUpdateHistoryTest.java b/core/src/test/java/google/registry/model/console/RegistrarPocUpdateHistoryTest.java new file mode 100644 index 000000000..91cb17162 --- /dev/null +++ b/core/src/test/java/google/registry/model/console/RegistrarPocUpdateHistoryTest.java @@ -0,0 +1,61 @@ +// 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.model.console; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.persistence.transaction.TransactionManagerFactory.tm; + +import com.google.common.collect.Iterables; +import google.registry.model.EntityTestCase; +import google.registry.model.registrar.RegistrarPoc; +import google.registry.model.registrar.RegistrarPoc.RegistrarPocId; +import google.registry.persistence.VKey; +import google.registry.testing.DatabaseHelper; +import org.junit.jupiter.api.Test; + +/** Tests for {@link RegistrarPocUpdateHistory}. */ +public class RegistrarPocUpdateHistoryTest extends EntityTestCase { + + RegistrarPocUpdateHistoryTest() { + super(JpaEntityCoverageCheck.ENABLED); + } + + @Test + void testPersistence() { + User user = DatabaseHelper.createAdminUser("email@email.com"); + RegistrarPoc registrarPoc = + DatabaseHelper.loadByKey( + VKey.create( + RegistrarPoc.class, + new RegistrarPocId("johndoe@theregistrar.com", "TheRegistrar"))); + RegistrarPocUpdateHistory history = + new RegistrarPocUpdateHistory.Builder() + .setRegistrarPoc(registrarPoc) + .setActingUser(user) + .setModificationTime(fakeClock.nowUtc()) + .setType(ConsoleUpdateHistory.Type.USER_UPDATE) + .setMethod("POST") + .setUrl("someUrl") + .build(); + tm().transact(() -> tm().put(history)); + + // Change the POC and make sure the history stays the same + tm().transact(() -> tm().put(registrarPoc.asBuilder().setName("Some Othername").build())); + + RegistrarPocUpdateHistory fromDb = + Iterables.getOnlyElement(DatabaseHelper.loadAllOf(RegistrarPocUpdateHistory.class)); + assertThat(fromDb.getRegistrarPoc().getName()).isEqualTo("John Doe"); + } +} diff --git a/core/src/test/java/google/registry/model/console/RegistrarUpdateHistoryTest.java b/core/src/test/java/google/registry/model/console/RegistrarUpdateHistoryTest.java new file mode 100644 index 000000000..46634605d --- /dev/null +++ b/core/src/test/java/google/registry/model/console/RegistrarUpdateHistoryTest.java @@ -0,0 +1,55 @@ +// 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.model.console; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.persistence.transaction.TransactionManagerFactory.tm; + +import com.google.common.collect.Iterables; +import google.registry.model.EntityTestCase; +import google.registry.model.registrar.Registrar; +import google.registry.testing.DatabaseHelper; +import org.junit.jupiter.api.Test; + +/** Tests for {@link RegistrarUpdateHistory}. */ +public class RegistrarUpdateHistoryTest extends EntityTestCase { + + RegistrarUpdateHistoryTest() { + super(JpaEntityCoverageCheck.ENABLED); + } + + @Test + void testPersistence() { + User user = DatabaseHelper.createAdminUser("email@email.com"); + Registrar theRegistrar = DatabaseHelper.loadRegistrar("TheRegistrar"); + RegistrarUpdateHistory history = + new RegistrarUpdateHistory.Builder() + .setRegistrar(theRegistrar) + .setActingUser(user) + .setModificationTime(fakeClock.nowUtc()) + .setType(ConsoleUpdateHistory.Type.USER_UPDATE) + .setMethod("POST") + .setUrl("someUrl") + .build(); + tm().transact(() -> tm().put(history)); + + // Change the registrar and make sure the history stays the same + tm().transact(() -> tm().put(theRegistrar.asBuilder().setUrl("https://other.url").build())); + + RegistrarUpdateHistory fromDb = + Iterables.getOnlyElement(DatabaseHelper.loadAllOf(RegistrarUpdateHistory.class)); + assertThat(fromDb.getRegistrar().getUrl()).isEqualTo("http://my.fake.url"); + } +} diff --git a/core/src/test/java/google/registry/model/console/UserTest.java b/core/src/test/java/google/registry/model/console/UserTest.java index d2955470c..68a640381 100644 --- a/core/src/test/java/google/registry/model/console/UserTest.java +++ b/core/src/test/java/google/registry/model/console/UserTest.java @@ -20,6 +20,7 @@ import static google.registry.persistence.transaction.TransactionManagerFactory. import static org.junit.jupiter.api.Assertions.assertThrows; import google.registry.model.EntityTestCase; +import google.registry.testing.DatabaseHelper; import org.junit.jupiter.api.Test; /** Tests for {@link User}. */ @@ -31,13 +32,7 @@ public class UserTest extends EntityTestCase { @Test void testPersistence_lookupByEmail() { - User user = - new User.Builder() - .setEmailAddress("email@email.com") - .setUserRoles( - new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).setIsAdmin(true).build()) - .build(); - tm().transact(() -> tm().put(user)); + User user = DatabaseHelper.createAdminUser("email@email.com"); tm().transact( () -> { assertAboutImmutableObjects() diff --git a/core/src/test/java/google/registry/model/console/UserUpdateHistoryTest.java b/core/src/test/java/google/registry/model/console/UserUpdateHistoryTest.java new file mode 100644 index 000000000..df9b3630b --- /dev/null +++ b/core/src/test/java/google/registry/model/console/UserUpdateHistoryTest.java @@ -0,0 +1,65 @@ +// 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.model.console; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.persistence.transaction.TransactionManagerFactory.tm; +import static google.registry.testing.DatabaseHelper.loadAllOf; + +import com.google.common.collect.Iterables; +import google.registry.model.EntityTestCase; +import google.registry.testing.DatabaseHelper; +import org.junit.jupiter.api.Test; + +/** Tests for {@link UserUpdateHistory}. */ +public class UserUpdateHistoryTest extends EntityTestCase { + + UserUpdateHistoryTest() { + super(JpaEntityCoverageCheck.ENABLED); + } + + @Test + void testPersistence() { + User user = DatabaseHelper.createAdminUser("email@email.com"); + User otherUser = DatabaseHelper.createAdminUser("otherEmail@email.com"); + UserUpdateHistory history = + new UserUpdateHistory.Builder() + .setUser(otherUser) + .setActingUser(user) + .setModificationTime(fakeClock.nowUtc()) + .setType(ConsoleUpdateHistory.Type.USER_UPDATE) + .setMethod("POST") + .setUrl("someUrl") + .build(); + tm().transact(() -> tm().put(history)); + + // Change the acted-upon user and make sure that nothing changed in the history DB + tm().transact( + () -> + tm().put( + otherUser + .asBuilder() + .setUserRoles( + otherUser + .getUserRoles() + .asBuilder() + .setGlobalRole(GlobalRole.SUPPORT_LEAD) + .build()) + .build())); + + UserUpdateHistory fromDb = Iterables.getOnlyElement(loadAllOf(UserUpdateHistory.class)); + assertThat(fromDb.getUser().getUserRoles().getGlobalRole()).isEqualTo(GlobalRole.FTE); + } +} diff --git a/core/src/test/java/google/registry/model/registrar/RegistrarTest.java b/core/src/test/java/google/registry/model/registrar/RegistrarTest.java index ee4757b16..e2fa9de08 100644 --- a/core/src/test/java/google/registry/model/registrar/RegistrarTest.java +++ b/core/src/test/java/google/registry/model/registrar/RegistrarTest.java @@ -39,8 +39,8 @@ import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.ImmutableSortedSet; import google.registry.config.RegistryConfig; import google.registry.model.EntityTestCase; -import google.registry.model.registrar.Registrar.State; -import google.registry.model.registrar.Registrar.Type; +import google.registry.model.registrar.RegistrarBase.State; +import google.registry.model.registrar.RegistrarBase.Type; import google.registry.model.tld.Tld; import google.registry.model.tld.Tld.TldType; import google.registry.model.tld.Tlds; @@ -129,7 +129,7 @@ class RegistrarTest extends EntityTestCase { .setVisibleInWhoisAsTech(false) .setPhoneNumber("+1.2125551213") .setFaxNumber("+1.2125551213") - .setTypes(ImmutableSet.of(RegistrarPoc.Type.ABUSE, RegistrarPoc.Type.ADMIN)) + .setTypes(ImmutableSet.of(RegistrarPocBase.Type.ABUSE, RegistrarPocBase.Type.ADMIN)) .build(); persistSimpleResources( ImmutableList.of( @@ -140,7 +140,8 @@ class RegistrarTest extends EntityTestCase { .setEmailAddress("johndoe@example.com") .setPhoneNumber("+1.2125551213") .setFaxNumber("+1.2125551213") - .setTypes(ImmutableSet.of(RegistrarPoc.Type.LEGAL, RegistrarPoc.Type.MARKETING)) + .setTypes( + ImmutableSet.of(RegistrarPocBase.Type.LEGAL, RegistrarPocBase.Type.MARKETING)) .build())); } @@ -326,7 +327,7 @@ class RegistrarTest extends EntityTestCase { .setVisibleInWhoisAsTech(true) .setPhoneNumber("+1.2125551213") .setFaxNumber("+1.2125551213") - .setTypes(ImmutableSet.of(RegistrarPoc.Type.TECH)) + .setTypes(ImmutableSet.of(RegistrarPocBase.Type.TECH)) .build()); RegistrarPoc newTechAbuseContact = persistSimpleResource( @@ -338,13 +339,13 @@ class RegistrarTest extends EntityTestCase { .setVisibleInWhoisAsTech(true) .setPhoneNumber("+1.2125551213") .setFaxNumber("+1.2125551213") - .setTypes(ImmutableSet.of(RegistrarPoc.Type.TECH, RegistrarPoc.Type.ABUSE)) + .setTypes(ImmutableSet.of(RegistrarPocBase.Type.TECH, RegistrarPocBase.Type.ABUSE)) .build()); ImmutableSortedSet techContacts = - registrar.getContactsOfType(RegistrarPoc.Type.TECH); + registrar.getContactsOfType(RegistrarPocBase.Type.TECH); assertThat(techContacts).containsExactly(newTechContact, newTechAbuseContact).inOrder(); ImmutableSortedSet abuseContacts = - registrar.getContactsOfType(RegistrarPoc.Type.ABUSE); + registrar.getContactsOfType(RegistrarPocBase.Type.ABUSE); assertThat(abuseContacts).containsExactly(newTechAbuseContact, abuseAdminContact).inOrder(); } diff --git a/core/src/test/java/google/registry/persistence/converter/StringValueEnumeratedTest.java b/core/src/test/java/google/registry/persistence/converter/StringValueEnumeratedTest.java index 061b4f1e9..1ecfeb36b 100644 --- a/core/src/test/java/google/registry/persistence/converter/StringValueEnumeratedTest.java +++ b/core/src/test/java/google/registry/persistence/converter/StringValueEnumeratedTest.java @@ -19,7 +19,7 @@ import static google.registry.persistence.transaction.TransactionManagerFactory. import static google.registry.testing.DatabaseHelper.insertInDb; import google.registry.model.ImmutableObject; -import google.registry.model.registrar.Registrar.State; +import google.registry.model.registrar.RegistrarBase.State; import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.persistence.transaction.JpaTestExtensions.JpaUnitTestExtension; import javax.persistence.Entity; diff --git a/core/src/test/java/google/registry/persistence/transaction/JpaTransactionManagerExtension.java b/core/src/test/java/google/registry/persistence/transaction/JpaTransactionManagerExtension.java index 375d9c184..203b5c7e4 100644 --- a/core/src/test/java/google/registry/persistence/transaction/JpaTransactionManagerExtension.java +++ b/core/src/test/java/google/registry/persistence/transaction/JpaTransactionManagerExtension.java @@ -31,9 +31,10 @@ import com.google.common.collect.Maps; import com.google.common.collect.Streams; import com.google.common.io.Resources; import google.registry.model.registrar.Registrar; -import google.registry.model.registrar.Registrar.State; import google.registry.model.registrar.RegistrarAddress; +import google.registry.model.registrar.RegistrarBase.State; import google.registry.model.registrar.RegistrarPoc; +import google.registry.model.registrar.RegistrarPocBase; import google.registry.persistence.HibernateSchemaExporter; import google.registry.persistence.NomulusPostgreSql; import google.registry.persistence.PersistenceModule; @@ -410,7 +411,7 @@ public abstract class JpaTransactionManagerExtension .setVisibleInWhoisAsTech(false) .setEmailAddress("janedoe@theregistrar.com") .setPhoneNumber("+1.1234567890") - .setTypes(ImmutableSet.of(RegistrarPoc.Type.ADMIN)) + .setTypes(ImmutableSet.of(RegistrarPocBase.Type.ADMIN)) .build(); } @@ -424,7 +425,7 @@ public abstract class JpaTransactionManagerExtension .setName("John Doe") .setEmailAddress("johndoe@theregistrar.com") .setPhoneNumber("+1.1234567890") - .setTypes(ImmutableSet.of(RegistrarPoc.Type.ADMIN)) + .setTypes(ImmutableSet.of(RegistrarPocBase.Type.ADMIN)) .setLoginEmailAddress("johndoe@theregistrar.com") .build(); } @@ -436,7 +437,7 @@ public abstract class JpaTransactionManagerExtension .setEmailAddress("Marla.Singer@crr.com") .setRegistryLockEmailAddress("Marla.Singer.RegistryLock@crr.com") .setPhoneNumber("+1.2128675309") - .setTypes(ImmutableSet.of(RegistrarPoc.Type.TECH)) + .setTypes(ImmutableSet.of(RegistrarPocBase.Type.TECH)) .setLoginEmailAddress("Marla.Singer@crr.com") .setAllowedToSetRegistryLockPassword(true) .setRegistryLockPassword("hi") diff --git a/core/src/test/java/google/registry/rdap/RdapJsonFormatterTest.java b/core/src/test/java/google/registry/rdap/RdapJsonFormatterTest.java index 3978ee194..acca40c8c 100644 --- a/core/src/test/java/google/registry/rdap/RdapJsonFormatterTest.java +++ b/core/src/test/java/google/registry/rdap/RdapJsonFormatterTest.java @@ -39,6 +39,7 @@ import google.registry.model.eppcommon.StatusValue; import google.registry.model.host.Host; import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarPoc; +import google.registry.model.registrar.RegistrarPocBase; import google.registry.model.reporting.HistoryEntry; import google.registry.model.transfer.DomainTransferData; import google.registry.model.transfer.TransferStatus; @@ -259,7 +260,7 @@ class RdapJsonFormatterTest { .setEmailAddress("babydoe@example.com") .setPhoneNumber("+1.2125551217") .setFaxNumber("+1.2125551218") - .setTypes(ImmutableSet.of(RegistrarPoc.Type.ADMIN)) + .setTypes(ImmutableSet.of(RegistrarPocBase.Type.ADMIN)) .setVisibleInWhoisAsAdmin(false) .setVisibleInWhoisAsTech(false) .build(), @@ -268,7 +269,7 @@ class RdapJsonFormatterTest { .setName("John Doe") .setEmailAddress("johndoe@example.com") .setFaxNumber("+1.2125551213") - .setTypes(ImmutableSet.of(RegistrarPoc.Type.ADMIN)) + .setTypes(ImmutableSet.of(RegistrarPocBase.Type.ADMIN)) .setVisibleInWhoisAsAdmin(false) .setVisibleInWhoisAsTech(true) .build(), @@ -277,7 +278,7 @@ class RdapJsonFormatterTest { .setName("Jane Doe") .setEmailAddress("janedoe@example.com") .setPhoneNumber("+1.2125551215") - .setTypes(ImmutableSet.of(RegistrarPoc.Type.TECH, RegistrarPoc.Type.ADMIN)) + .setTypes(ImmutableSet.of(RegistrarPocBase.Type.TECH, RegistrarPocBase.Type.ADMIN)) .setVisibleInWhoisAsAdmin(true) .setVisibleInWhoisAsTech(false) .build(), @@ -287,7 +288,7 @@ class RdapJsonFormatterTest { .setEmailAddress("playdoe@example.com") .setPhoneNumber("+1.2125551217") .setFaxNumber("+1.2125551218") - .setTypes(ImmutableSet.of(RegistrarPoc.Type.BILLING)) + .setTypes(ImmutableSet.of(RegistrarPocBase.Type.BILLING)) .setVisibleInWhoisAsAdmin(true) .setVisibleInWhoisAsTech(true) .build()); diff --git a/core/src/test/java/google/registry/rdap/UpdateRegistrarRdapBaseUrlsActionTest.java b/core/src/test/java/google/registry/rdap/UpdateRegistrarRdapBaseUrlsActionTest.java index 1c05359ce..8f08e920a 100644 --- a/core/src/test/java/google/registry/rdap/UpdateRegistrarRdapBaseUrlsActionTest.java +++ b/core/src/test/java/google/registry/rdap/UpdateRegistrarRdapBaseUrlsActionTest.java @@ -30,6 +30,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarAddress; +import google.registry.model.registrar.RegistrarBase; import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension; import google.registry.request.HttpException.InternalServerErrorException; @@ -91,7 +92,7 @@ public final class UpdateRegistrarRdapBaseUrlsActionTest { } private static void persistRegistrar( - String registrarId, Long ianaId, Registrar.Type type, String... rdapBaseUrls) { + String registrarId, Long ianaId, RegistrarBase.Type type, String... rdapBaseUrls) { persistSimpleResource( new Registrar.Builder() .setRegistrarId(registrarId) diff --git a/core/src/test/java/google/registry/rde/RegistrarToXjcConverterTest.java b/core/src/test/java/google/registry/rde/RegistrarToXjcConverterTest.java index f31085ef7..c3c1a1bd2 100644 --- a/core/src/test/java/google/registry/rde/RegistrarToXjcConverterTest.java +++ b/core/src/test/java/google/registry/rde/RegistrarToXjcConverterTest.java @@ -22,8 +22,8 @@ import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.collect.ImmutableList; import google.registry.model.registrar.Registrar; -import google.registry.model.registrar.Registrar.State; import google.registry.model.registrar.RegistrarAddress; +import google.registry.model.registrar.RegistrarBase.State; import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension; import google.registry.testing.FakeClock; diff --git a/core/src/test/java/google/registry/request/auth/AuthenticatedRegistrarAccessorTest.java b/core/src/test/java/google/registry/request/auth/AuthenticatedRegistrarAccessorTest.java index 1ca49c5d6..e3e2491f0 100644 --- a/core/src/test/java/google/registry/request/auth/AuthenticatedRegistrarAccessorTest.java +++ b/core/src/test/java/google/registry/request/auth/AuthenticatedRegistrarAccessorTest.java @@ -38,7 +38,7 @@ import google.registry.model.console.GlobalRole; import google.registry.model.console.RegistrarRole; import google.registry.model.console.UserRoles; import google.registry.model.registrar.Registrar; -import google.registry.model.registrar.Registrar.State; +import google.registry.model.registrar.RegistrarBase.State; import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension; import google.registry.request.auth.AuthenticatedRegistrarAccessor.RegistrarAccessDeniedException; diff --git a/core/src/test/java/google/registry/request/auth/OidcTokenAuthenticationMechanismTest.java b/core/src/test/java/google/registry/request/auth/OidcTokenAuthenticationMechanismTest.java index 1bd7aff45..6104e5902 100644 --- a/core/src/test/java/google/registry/request/auth/OidcTokenAuthenticationMechanismTest.java +++ b/core/src/test/java/google/registry/request/auth/OidcTokenAuthenticationMechanismTest.java @@ -18,6 +18,7 @@ import static com.google.common.net.HttpHeaders.AUTHORIZATION; import static com.google.common.truth.Truth.assertThat; import static google.registry.request.auth.AuthModule.BEARER_PREFIX; import static google.registry.request.auth.AuthModule.IAP_HEADER_NAME; +import static google.registry.testing.DatabaseHelper.createAdminUser; import static google.registry.testing.DatabaseHelper.insertInDb; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -56,17 +57,12 @@ public class OidcTokenAuthenticationMechanismTest { ImmutableSet.of("service@email.test", "email@service.goog"); private final Payload payload = new Payload(); - private final User user = - new User.Builder() - .setEmailAddress(email) - .setUserRoles( - new UserRoles.Builder().setIsAdmin(true).setGlobalRole(GlobalRole.FTE).build()) - .build(); private final JsonWebSignature jwt = new JsonWebSignature(new Header(), payload, new byte[0], new byte[0]); private final TokenVerifier tokenVerifier = mock(TokenVerifier.class); private final HttpServletRequest request = mock(HttpServletRequest.class); + private User user; private AuthResult authResult; private OidcTokenAuthenticationMechanism authenticationMechanism = new OidcTokenAuthenticationMechanism(serviceAccounts, tokenVerifier, null, e -> rawToken) {}; @@ -80,7 +76,7 @@ public class OidcTokenAuthenticationMechanismTest { when(tokenVerifier.verify(rawToken)).thenReturn(jwt); payload.setEmail(email); payload.setSubject(gaiaId); - insertInDb(user); + user = createAdminUser(email); } @AfterEach 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 05c632cfe..36be06fac 100644 --- a/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java +++ b/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java @@ -23,7 +23,11 @@ import google.registry.bsa.persistence.BsaUnblockableDomainTest; import google.registry.model.billing.BillingBaseTest; import google.registry.model.common.CursorTest; import google.registry.model.common.DnsRefreshRequestTest; +import google.registry.model.console.ConsoleEppActionHistoryTest; +import google.registry.model.console.RegistrarPocUpdateHistoryTest; +import google.registry.model.console.RegistrarUpdateHistoryTest; import google.registry.model.console.UserTest; +import google.registry.model.console.UserUpdateHistoryTest; import google.registry.model.contact.ContactTest; import google.registry.model.domain.DomainSqlTest; import google.registry.model.domain.token.AllocationTokenTest; @@ -92,6 +96,7 @@ import org.junit.runner.RunWith; BsaUnblockableDomainTest.class, BulkPricingPackageTest.class, ClaimsListDaoTest.class, + ConsoleEppActionHistoryTest.class, ContactHistoryTest.class, ContactTest.class, CursorTest.class, @@ -104,6 +109,8 @@ import org.junit.runner.RunWith; PremiumListDaoTest.class, RdeRevisionTest.class, RegistrarDaoTest.class, + RegistrarPocUpdateHistoryTest.class, + RegistrarUpdateHistoryTest.class, TldTest.class, ReservedListDaoTest.class, RegistryLockDaoTest.class, @@ -112,6 +119,7 @@ import org.junit.runner.RunWith; Spec11ThreatMatchTest.class, TmchCrlTest.class, UserTest.class, + UserUpdateHistoryTest.class, // AfterSuiteTest must be the last entry. See class javadoc for details. AfterSuiteTest.class }) diff --git a/core/src/test/java/google/registry/schema/registrar/RegistrarPocTest.java b/core/src/test/java/google/registry/schema/registrar/RegistrarPocTest.java index c62aabaa3..c0b424c2b 100644 --- a/core/src/test/java/google/registry/schema/registrar/RegistrarPocTest.java +++ b/core/src/test/java/google/registry/schema/registrar/RegistrarPocTest.java @@ -15,7 +15,7 @@ package google.registry.schema.registrar; import static com.google.common.truth.Truth.assertThat; -import static google.registry.model.registrar.RegistrarPoc.Type.WHOIS; +import static google.registry.model.registrar.RegistrarPocBase.Type.WHOIS; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.testing.DatabaseHelper.insertInDb; import static google.registry.testing.DatabaseHelper.loadByEntity; diff --git a/core/src/test/java/google/registry/testing/DatabaseHelper.java b/core/src/test/java/google/registry/testing/DatabaseHelper.java index fffc19746..113cc211f 100644 --- a/core/src/test/java/google/registry/testing/DatabaseHelper.java +++ b/core/src/test/java/google/registry/testing/DatabaseHelper.java @@ -72,6 +72,10 @@ import google.registry.model.billing.BillingCancellation; import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingRecurrence; import google.registry.model.common.DnsRefreshRequest; +import google.registry.model.console.GlobalRole; +import google.registry.model.console.User; +import google.registry.model.console.UserDao; +import google.registry.model.console.UserRoles; import google.registry.model.contact.Contact; import google.registry.model.contact.ContactAuthInfo; import google.registry.model.contact.ContactHistory; @@ -91,8 +95,9 @@ import google.registry.model.host.Host; import google.registry.model.poll.PollMessage; import google.registry.model.pricing.StaticPremiumListPricingEngine; import google.registry.model.registrar.Registrar; -import google.registry.model.registrar.Registrar.State; import google.registry.model.registrar.RegistrarAddress; +import google.registry.model.registrar.RegistrarBase; +import google.registry.model.registrar.RegistrarBase.State; import google.registry.model.reporting.HistoryEntry; import google.registry.model.reporting.HistoryEntryDao; import google.registry.model.tld.Tld; @@ -752,7 +757,7 @@ public final class DatabaseHelper { public static Registrar persistNewRegistrar( String registrarId, String registrarName, - Registrar.Type type, + RegistrarBase.Type type, @Nullable Long ianaIdentifier) { return persistSimpleResource( new Registrar.Builder() @@ -1011,6 +1016,22 @@ public final class DatabaseHelper { return tm().transact(() -> tm().loadByEntity(resource)); } + /** Persists an admin {@link User} with the given email address if it doesn't already exist. */ + public static User createAdminUser(String emailAddress) { + Optional existingUser = UserDao.loadUser(emailAddress); + if (existingUser.isPresent()) { + return existingUser.get(); + } + User user = + new User.Builder() + .setEmailAddress(emailAddress) + .setUserRoles( + new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).setIsAdmin(true).build()) + .build(); + tm().transact(() -> tm().put(user)); + return UserDao.loadUser(emailAddress).get(); + } + /** Returns all the history entries that are parented off the given EppResource. */ public static List getHistoryEntries(EppResource resource) { return HistoryEntryDao.loadHistoryObjectsForResource(resource.createVKey()); diff --git a/core/src/test/java/google/registry/testing/FullFieldsTestEntityHelper.java b/core/src/test/java/google/registry/testing/FullFieldsTestEntityHelper.java index 9370b1e03..096ec693c 100644 --- a/core/src/test/java/google/registry/testing/FullFieldsTestEntityHelper.java +++ b/core/src/test/java/google/registry/testing/FullFieldsTestEntityHelper.java @@ -38,7 +38,9 @@ import google.registry.model.eppcommon.Trid; import google.registry.model.host.Host; import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarAddress; +import google.registry.model.registrar.RegistrarBase; import google.registry.model.registrar.RegistrarPoc; +import google.registry.model.registrar.RegistrarPocBase; import google.registry.model.reporting.HistoryEntry; import google.registry.persistence.VKey; import google.registry.util.Idn; @@ -51,12 +53,12 @@ import org.joda.time.DateTime; public final class FullFieldsTestEntityHelper { public static Registrar makeRegistrar( - String registrarId, String registrarName, Registrar.State state) { + String registrarId, String registrarName, RegistrarBase.State state) { return makeRegistrar(registrarId, registrarName, state, 1L); } public static Registrar makeRegistrar( - String registrarId, String registrarName, Registrar.State state, Long ianaIdentifier) { + String registrarId, String registrarName, RegistrarBase.State state, Long ianaIdentifier) { return new Registrar.Builder() .setRegistrarId(registrarId) .setRegistrarName(registrarName) @@ -98,7 +100,7 @@ public final class FullFieldsTestEntityHelper { .setEmailAddress("johndoe@example.com") .setPhoneNumber("+1.2125551213") .setFaxNumber("+1.2125551213") - .setTypes(ImmutableSet.of(RegistrarPoc.Type.ADMIN)) + .setTypes(ImmutableSet.of(RegistrarPocBase.Type.ADMIN)) // Purposely flip the internal/external admin/tech // distinction to make sure we're not relying on it. Sigh. .setVisibleInWhoisAsAdmin(false) @@ -110,7 +112,7 @@ public final class FullFieldsTestEntityHelper { .setEmailAddress("janedoe@example.com") .setPhoneNumber("+1.2125551215") .setFaxNumber("+1.2125551216") - .setTypes(ImmutableSet.of(RegistrarPoc.Type.TECH)) + .setTypes(ImmutableSet.of(RegistrarPocBase.Type.TECH)) // Purposely flip the internal/external admin/tech // distinction to make sure we're not relying on it. Sigh. .setVisibleInWhoisAsAdmin(true) diff --git a/core/src/test/java/google/registry/tools/CheckDomainClaimsCommandTest.java b/core/src/test/java/google/registry/tools/CheckDomainClaimsCommandTest.java index a902ec46b..54b7c6ffc 100644 --- a/core/src/test/java/google/registry/tools/CheckDomainClaimsCommandTest.java +++ b/core/src/test/java/google/registry/tools/CheckDomainClaimsCommandTest.java @@ -18,7 +18,7 @@ import static google.registry.testing.DatabaseHelper.persistNewRegistrar; import static org.junit.jupiter.api.Assertions.assertThrows; import com.beust.jcommander.ParameterException; -import google.registry.model.registrar.Registrar.Type; +import google.registry.model.registrar.RegistrarBase.Type; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/core/src/test/java/google/registry/tools/CheckDomainCommandTest.java b/core/src/test/java/google/registry/tools/CheckDomainCommandTest.java index 32cf4875f..6dd9d9b6c 100644 --- a/core/src/test/java/google/registry/tools/CheckDomainCommandTest.java +++ b/core/src/test/java/google/registry/tools/CheckDomainCommandTest.java @@ -18,7 +18,7 @@ import static google.registry.testing.DatabaseHelper.persistNewRegistrar; import static org.junit.jupiter.api.Assertions.assertThrows; import com.beust.jcommander.ParameterException; -import google.registry.model.registrar.Registrar.Type; +import google.registry.model.registrar.RegistrarBase.Type; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/core/src/test/java/google/registry/tools/DeleteUserCommandTest.java b/core/src/test/java/google/registry/tools/DeleteUserCommandTest.java index b21dca87f..30b3addf9 100644 --- a/core/src/test/java/google/registry/tools/DeleteUserCommandTest.java +++ b/core/src/test/java/google/registry/tools/DeleteUserCommandTest.java @@ -22,10 +22,8 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; -import google.registry.model.console.GlobalRole; -import google.registry.model.console.User; import google.registry.model.console.UserDao; -import google.registry.model.console.UserRoles; +import google.registry.testing.DatabaseHelper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -41,13 +39,7 @@ public class DeleteUserCommandTest extends CommandTestCase { @Test void testSuccess_deletesUser() throws Exception { - User user = - new User.Builder() - .setEmailAddress("email@example.test") - .setUserRoles( - new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).setIsAdmin(true).build()) - .build(); - UserDao.saveUser(user); + DatabaseHelper.createAdminUser("email@example.test"); assertThat(UserDao.loadUser("email@example.test")).isPresent(); runCommandForced("--email", "email@example.test"); assertThat(UserDao.loadUser("email@example.test")).isEmpty(); diff --git a/core/src/test/java/google/registry/tools/LockDomainCommandTest.java b/core/src/test/java/google/registry/tools/LockDomainCommandTest.java index b017812f9..cf78df9dc 100644 --- a/core/src/test/java/google/registry/tools/LockDomainCommandTest.java +++ b/core/src/test/java/google/registry/tools/LockDomainCommandTest.java @@ -26,7 +26,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import com.google.common.collect.ImmutableList; import google.registry.model.domain.Domain; -import google.registry.model.registrar.Registrar.Type; +import google.registry.model.registrar.RegistrarBase.Type; import google.registry.testing.CloudTasksHelper; import google.registry.testing.DatabaseHelper; import google.registry.testing.DeterministicStringGenerator; diff --git a/core/src/test/java/google/registry/tools/RegistrarPocCommandTest.java b/core/src/test/java/google/registry/tools/RegistrarPocCommandTest.java index b27c84b82..c7c4dfcad 100644 --- a/core/src/test/java/google/registry/tools/RegistrarPocCommandTest.java +++ b/core/src/test/java/google/registry/tools/RegistrarPocCommandTest.java @@ -15,10 +15,10 @@ package google.registry.tools; import static com.google.common.truth.Truth.assertThat; -import static google.registry.model.registrar.RegistrarPoc.Type.ABUSE; -import static google.registry.model.registrar.RegistrarPoc.Type.ADMIN; -import static google.registry.model.registrar.RegistrarPoc.Type.TECH; -import static google.registry.model.registrar.RegistrarPoc.Type.WHOIS; +import static google.registry.model.registrar.RegistrarPocBase.Type.ABUSE; +import static google.registry.model.registrar.RegistrarPocBase.Type.ADMIN; +import static google.registry.model.registrar.RegistrarPocBase.Type.TECH; +import static google.registry.model.registrar.RegistrarPocBase.Type.WHOIS; import static google.registry.testing.DatabaseHelper.loadRegistrar; import static google.registry.testing.DatabaseHelper.persistResource; import static google.registry.testing.DatabaseHelper.persistSimpleResource; diff --git a/core/src/test/java/google/registry/tools/SetupOteCommandTest.java b/core/src/test/java/google/registry/tools/SetupOteCommandTest.java index 0fc1644eb..b19d31d3f 100644 --- a/core/src/test/java/google/registry/tools/SetupOteCommandTest.java +++ b/core/src/test/java/google/registry/tools/SetupOteCommandTest.java @@ -15,7 +15,7 @@ package google.registry.tools; import static com.google.common.truth.Truth.assertThat; -import static google.registry.model.registrar.Registrar.State.ACTIVE; +import static google.registry.model.registrar.RegistrarBase.State.ACTIVE; import static google.registry.model.tld.Tld.TldState.GENERAL_AVAILABILITY; import static google.registry.model.tld.Tld.TldState.START_DATE_SUNRISE; import static google.registry.testing.CertificateSamples.SAMPLE_CERT_HASH; diff --git a/core/src/test/java/google/registry/tools/UnlockDomainCommandTest.java b/core/src/test/java/google/registry/tools/UnlockDomainCommandTest.java index c24c36fe3..0bd54c69b 100644 --- a/core/src/test/java/google/registry/tools/UnlockDomainCommandTest.java +++ b/core/src/test/java/google/registry/tools/UnlockDomainCommandTest.java @@ -29,7 +29,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import google.registry.model.domain.Domain; import google.registry.model.domain.RegistryLock; -import google.registry.model.registrar.Registrar.Type; +import google.registry.model.registrar.RegistrarBase.Type; import google.registry.testing.CloudTasksHelper; import google.registry.testing.DatabaseHelper; import google.registry.testing.DeterministicStringGenerator; diff --git a/core/src/test/java/google/registry/tools/UpdateRegistrarCommandTest.java b/core/src/test/java/google/registry/tools/UpdateRegistrarCommandTest.java index fc4fa3f37..d1b46a6df 100644 --- a/core/src/test/java/google/registry/tools/UpdateRegistrarCommandTest.java +++ b/core/src/test/java/google/registry/tools/UpdateRegistrarCommandTest.java @@ -37,8 +37,8 @@ import com.google.common.collect.ImmutableSortedMap; import google.registry.flows.certs.CertificateChecker; import google.registry.flows.certs.CertificateChecker.InsecureCertificateException; import google.registry.model.registrar.Registrar; -import google.registry.model.registrar.Registrar.State; -import google.registry.model.registrar.Registrar.Type; +import google.registry.model.registrar.RegistrarBase.State; +import google.registry.model.registrar.RegistrarBase.Type; import google.registry.persistence.transaction.JpaTransactionManagerExtension; import google.registry.util.CidrAddressBlock; import java.math.BigDecimal; diff --git a/core/src/test/java/google/registry/tools/ValidateLoginCredentialsCommandTest.java b/core/src/test/java/google/registry/tools/ValidateLoginCredentialsCommandTest.java index a21e9dcf6..8e72db07d 100644 --- a/core/src/test/java/google/registry/tools/ValidateLoginCredentialsCommandTest.java +++ b/core/src/test/java/google/registry/tools/ValidateLoginCredentialsCommandTest.java @@ -15,7 +15,7 @@ package google.registry.tools; import static com.google.common.truth.Truth.assertThat; -import static google.registry.model.registrar.Registrar.State.ACTIVE; +import static google.registry.model.registrar.RegistrarBase.State.ACTIVE; import static google.registry.testing.DatabaseHelper.createTld; import static google.registry.testing.DatabaseHelper.loadRegistrar; import static google.registry.testing.DatabaseHelper.persistResource; @@ -32,7 +32,7 @@ import google.registry.flows.EppException; import google.registry.flows.TransportCredentials.BadRegistrarPasswordException; import google.registry.flows.certs.CertificateChecker; import google.registry.model.registrar.Registrar; -import google.registry.model.registrar.Registrar.State; +import google.registry.model.registrar.RegistrarBase.State; import google.registry.testing.CertificateSamples; import google.registry.util.CidrAddressBlock; import java.nio.file.Files; diff --git a/core/src/test/java/google/registry/ui/server/console/ConsoleDomainListActionTest.java b/core/src/test/java/google/registry/ui/server/console/ConsoleDomainListActionTest.java index b6c6297b2..d9c786ad0 100644 --- a/core/src/test/java/google/registry/ui/server/console/ConsoleDomainListActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/ConsoleDomainListActionTest.java @@ -16,6 +16,7 @@ package google.registry.ui.server.console; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.truth.Truth.assertThat; +import static google.registry.testing.DatabaseHelper.createAdminUser; import static google.registry.testing.DatabaseHelper.createTld; import static google.registry.testing.DatabaseHelper.persistActiveDomain; import static google.registry.testing.DatabaseHelper.persistDomainAsDeleted; @@ -24,9 +25,6 @@ import com.google.api.client.http.HttpStatusCodes; import com.google.common.collect.Iterables; import com.google.gson.Gson; import google.registry.model.EppResourceUtils; -import google.registry.model.console.GlobalRole; -import google.registry.model.console.User; -import google.registry.model.console.UserRoles; import google.registry.model.domain.Domain; import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.request.auth.AuthResult; @@ -232,12 +230,7 @@ public class ConsoleDomainListActionTest { @Nullable String searchTerm) { response = new FakeResponse(); AuthResult authResult = - AuthResult.createUser( - UserAuthInfo.create( - new User.Builder() - .setEmailAddress("email@email.example") - .setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()) - .build())); + AuthResult.createUser(UserAuthInfo.create(createAdminUser("email@email.example"))); return new ConsoleDomainListAction( authResult, response, diff --git a/core/src/test/java/google/registry/ui/server/console/ConsoleUserDataActionTest.java b/core/src/test/java/google/registry/ui/server/console/ConsoleUserDataActionTest.java index 801ae92f2..6392b83c7 100644 --- a/core/src/test/java/google/registry/ui/server/console/ConsoleUserDataActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/ConsoleUserDataActionTest.java @@ -20,14 +20,13 @@ import static org.mockito.Mockito.when; import com.google.api.client.http.HttpStatusCodes; import com.google.gson.Gson; -import google.registry.model.console.GlobalRole; import google.registry.model.console.User; -import google.registry.model.console.UserRoles; import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.request.Action; import google.registry.request.RequestModule; import google.registry.request.auth.AuthResult; import google.registry.request.auth.UserAuthInfo; +import google.registry.testing.DatabaseHelper; import google.registry.testing.FakeConsoleApiParams; import google.registry.testing.FakeResponse; import google.registry.ui.server.registrar.ConsoleApiParams; @@ -52,12 +51,7 @@ class ConsoleUserDataActionTest { @Test void testSuccess_hasXSRFCookie() throws IOException { - User user = - new User.Builder() - .setEmailAddress("email@email.com") - .setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()) - .build(); - + User user = DatabaseHelper.createAdminUser("email@email.com"); AuthResult authResult = AuthResult.createUser(UserAuthInfo.create(user)); ConsoleUserDataAction action = createAction( @@ -70,12 +64,7 @@ class ConsoleUserDataActionTest { @Test void testSuccess_getContactInfo() throws IOException { - User user = - new User.Builder() - .setEmailAddress("email@email.com") - .setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()) - .build(); - + User user = DatabaseHelper.createAdminUser("email@email.com"); AuthResult authResult = AuthResult.createUser(UserAuthInfo.create(user)); ConsoleUserDataAction action = createAction( @@ -88,7 +77,7 @@ class ConsoleUserDataActionTest { assertThat(jsonObject) .containsExactly( "isAdmin", - false, + true, "technicalDocsUrl", "test", "globalRole", diff --git a/core/src/test/java/google/registry/ui/server/console/settings/ContactActionTest.java b/core/src/test/java/google/registry/ui/server/console/settings/ContactActionTest.java index 86ac0dbdb..f36aa88f4 100644 --- a/core/src/test/java/google/registry/ui/server/console/settings/ContactActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/settings/ContactActionTest.java @@ -16,7 +16,8 @@ package google.registry.ui.server.console.settings; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.truth.Truth.assertThat; -import static google.registry.model.registrar.RegistrarPoc.Type.WHOIS; +import static google.registry.model.registrar.RegistrarPocBase.Type.WHOIS; +import static google.registry.testing.DatabaseHelper.createAdminUser; import static google.registry.testing.DatabaseHelper.insertInDb; import static google.registry.testing.DatabaseHelper.loadAllOf; import static google.registry.testing.SqlHelper.saveRegistrar; @@ -27,7 +28,6 @@ import com.google.api.client.http.HttpStatusCodes; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.gson.Gson; -import google.registry.model.console.GlobalRole; import google.registry.model.console.RegistrarRole; import google.registry.model.console.User; import google.registry.model.console.UserRoles; @@ -102,9 +102,7 @@ class ContactActionTest { ContactAction action = createAction( Action.Method.GET, - AuthResult.createUser( - UserAuthInfo.create( - createUser(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()))), + AuthResult.createUser(UserAuthInfo.create(createAdminUser("email@email.com"))), testRegistrar.getRegistrarId(), null); action.run(); @@ -119,9 +117,7 @@ class ContactActionTest { ContactAction action = createAction( Action.Method.GET, - AuthResult.createUser( - UserAuthInfo.create( - createUser(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()))), + AuthResult.createUser(UserAuthInfo.create(createAdminUser("email@email.com"))), testRegistrar.getRegistrarId(), null); action.run(); @@ -134,9 +130,7 @@ class ContactActionTest { ContactAction action = createAction( Action.Method.POST, - AuthResult.createUser( - UserAuthInfo.create( - createUser(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()))), + AuthResult.createUser(UserAuthInfo.create(createAdminUser("email@email.com"))), testRegistrar.getRegistrarId(), "[" + jsonRegistrar1 + "," + jsonRegistrar2 + "]"); action.run(); @@ -156,9 +150,7 @@ class ContactActionTest { ContactAction action = createAction( Action.Method.POST, - AuthResult.createUser( - UserAuthInfo.create( - createUser(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()))), + AuthResult.createUser(UserAuthInfo.create(createAdminUser("email@email.com"))), testRegistrar.getRegistrarId(), "[" + jsonRegistrar1 + "," + jsonRegistrar2 + "]"); action.run(); @@ -181,9 +173,7 @@ class ContactActionTest { ContactAction action = createAction( Action.Method.POST, - AuthResult.createUser( - UserAuthInfo.create( - createUser(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()))), + AuthResult.createUser(UserAuthInfo.create(createAdminUser("email@email.com"))), testRegistrar.getRegistrarId(), "[" + jsonRegistrar2 + "]"); action.run(); @@ -204,25 +194,22 @@ class ContactActionTest { Action.Method.POST, AuthResult.createUser( UserAuthInfo.create( - createUser( - new UserRoles.Builder() - .setRegistrarRoles( - ImmutableMap.of( - testRegistrar.getRegistrarId(), RegistrarRole.ACCOUNT_MANAGER)) - .build()))), + new User.Builder() + .setEmailAddress("email@email.com") + .setUserRoles( + new UserRoles.Builder() + .setRegistrarRoles( + ImmutableMap.of( + testRegistrar.getRegistrarId(), + RegistrarRole.ACCOUNT_MANAGER)) + .build()) + .build())), testRegistrar.getRegistrarId(), "[" + jsonRegistrar2 + "]"); action.run(); assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_FORBIDDEN); } - private User createUser(UserRoles userRoles) { - return new User.Builder() - .setEmailAddress("email@email.com") - .setUserRoles(userRoles) - .build(); - } - private ContactAction createAction( Action.Method method, AuthResult authResult, String registrarId, String contacts) throws IOException { 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 4cb15dfa8..28627f4c7 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 @@ -28,15 +28,13 @@ 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.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.RequestModule; import google.registry.request.auth.AuthResult; import google.registry.request.auth.AuthenticatedRegistrarAccessor; import google.registry.request.auth.UserAuthInfo; +import google.registry.testing.DatabaseHelper; import google.registry.testing.FakeClock; import google.registry.testing.FakeResponse; import google.registry.ui.server.registrar.RegistrarConsoleModule; @@ -92,8 +90,7 @@ class SecurityActionTest { SecurityAction action = createAction( AuthResult.createUser( - UserAuthInfo.create( - createUser(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()))), + UserAuthInfo.create(DatabaseHelper.createAdminUser("email@email.com"))), testRegistrar.getRegistrarId()); action.run(); assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_OK); @@ -104,13 +101,6 @@ class SecurityActionTest { assertThat(r.getIpAddressAllowList().get(0).getNetmask()).isEqualTo(32); } - private User createUser(UserRoles userRoles) { - return new User.Builder() - .setEmailAddress("email@email.com") - .setUserRoles(userRoles) - .build(); - } - private SecurityAction createAction(AuthResult authResult, String registrarId) throws IOException { doReturn(new BufferedReader(new StringReader(jsonRegistrar1))).when(request).getReader(); @@ -124,6 +114,5 @@ class SecurityActionTest { registrarAccessor, registrarId, maybeRegistrar); - } } 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 af9e477be..1d5914618 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 @@ -25,7 +25,6 @@ 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.GlobalRole; import google.registry.model.console.RegistrarRole; import google.registry.model.console.User; import google.registry.model.console.UserRoles; @@ -146,11 +145,7 @@ public class WhoisRegistrarFieldsActionTest { private AuthResult defaultUserAuth() { return AuthResult.createUser( - UserAuthInfo.create( - new User.Builder() - .setEmailAddress("email@email.example") - .setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()) - .build())); + UserAuthInfo.create(DatabaseHelper.createAdminUser("email@email.example"))); } private WhoisRegistrarFieldsAction createAction() throws IOException { diff --git a/core/src/test/java/google/registry/ui/server/registrar/ContactSettingsTest.java b/core/src/test/java/google/registry/ui/server/registrar/ContactSettingsTest.java index 6368b4c8b..f02355595 100644 --- a/core/src/test/java/google/registry/ui/server/registrar/ContactSettingsTest.java +++ b/core/src/test/java/google/registry/ui/server/registrar/ContactSettingsTest.java @@ -24,7 +24,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarPoc; -import google.registry.model.registrar.RegistrarPoc.Type; +import google.registry.model.registrar.RegistrarPocBase.Type; import google.registry.persistence.transaction.JpaTransactionManagerExtension; import java.util.List; import java.util.Map; diff --git a/core/src/test/java/google/registry/ui/server/registrar/OteStatusActionTest.java b/core/src/test/java/google/registry/ui/server/registrar/OteStatusActionTest.java index 58a5359c8..721641718 100644 --- a/core/src/test/java/google/registry/ui/server/registrar/OteStatusActionTest.java +++ b/core/src/test/java/google/registry/ui/server/registrar/OteStatusActionTest.java @@ -26,7 +26,7 @@ import com.google.common.collect.Iterables; import google.registry.model.OteAccountBuilder; import google.registry.model.OteStats.StatType; import google.registry.model.OteStatsTestHelper; -import google.registry.model.registrar.Registrar.Type; +import google.registry.model.registrar.RegistrarBase.Type; import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension; import google.registry.request.auth.AuthenticatedRegistrarAccessor; diff --git a/core/src/test/java/google/registry/ui/server/registrar/RegistrarSettingsActionTestCase.java b/core/src/test/java/google/registry/ui/server/registrar/RegistrarSettingsActionTestCase.java index 905a1fec0..96112a39e 100644 --- a/core/src/test/java/google/registry/ui/server/registrar/RegistrarSettingsActionTestCase.java +++ b/core/src/test/java/google/registry/ui/server/registrar/RegistrarSettingsActionTestCase.java @@ -36,6 +36,7 @@ import com.google.common.collect.ImmutableSortedMap; import google.registry.flows.certs.CertificateChecker; import google.registry.groups.GmailClient; import google.registry.model.registrar.RegistrarPoc; +import google.registry.model.registrar.RegistrarPocBase; import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension; import google.registry.request.JsonActionRunner; @@ -97,7 +98,7 @@ public abstract class RegistrarSettingsActionTestCase { // Add a technical contact to the registrar (in addition to the default admin contact created by // JpaTransactionManagerExtension). techContact = - getOnlyElement(loadRegistrar(CLIENT_ID).getContactsOfType(RegistrarPoc.Type.TECH)); + getOnlyElement(loadRegistrar(CLIENT_ID).getContactsOfType(RegistrarPocBase.Type.TECH)); action.registrarAccessor = null; action.jsonActionRunner = diff --git a/core/src/test/java/google/registry/webdriver/RegistrarConsoleScreenshotTest.java b/core/src/test/java/google/registry/webdriver/RegistrarConsoleScreenshotTest.java index 07c52835e..cc64b1a34 100644 --- a/core/src/test/java/google/registry/webdriver/RegistrarConsoleScreenshotTest.java +++ b/core/src/test/java/google/registry/webdriver/RegistrarConsoleScreenshotTest.java @@ -31,7 +31,7 @@ import static google.registry.util.DateTimeUtils.START_OF_TIME; import com.google.common.collect.ImmutableMap; import google.registry.model.domain.Domain; import google.registry.model.domain.RegistryLock; -import google.registry.model.registrar.Registrar.State; +import google.registry.model.registrar.RegistrarBase.State; import google.registry.model.registrar.RegistrarPoc; import google.registry.module.frontend.FrontendServlet; import google.registry.server.RegistryTestServer; diff --git a/core/src/test/java/google/registry/whois/RegistrarWhoisResponseTest.java b/core/src/test/java/google/registry/whois/RegistrarWhoisResponseTest.java index 2150e7efc..92c98fd4f 100644 --- a/core/src/test/java/google/registry/whois/RegistrarWhoisResponseTest.java +++ b/core/src/test/java/google/registry/whois/RegistrarWhoisResponseTest.java @@ -25,6 +25,7 @@ import com.google.common.collect.ImmutableSet; import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarAddress; import google.registry.model.registrar.RegistrarPoc; +import google.registry.model.registrar.RegistrarPocBase; import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension; import google.registry.testing.FakeClock; @@ -74,7 +75,7 @@ class RegistrarWhoisResponseTest { .setEmailAddress("joeregistrar@example-registrar.tld") .setPhoneNumber("+1.3105551213") .setFaxNumber("+1.3105551213") - .setTypes(ImmutableSet.of(RegistrarPoc.Type.ADMIN)) + .setTypes(ImmutableSet.of(RegistrarPocBase.Type.ADMIN)) .setVisibleInWhoisAsAdmin(true) .setVisibleInWhoisAsTech(false) .build(), @@ -84,7 +85,7 @@ class RegistrarWhoisResponseTest { .setEmailAddress("johndoe@example-registrar.tld") .setPhoneNumber("+1.1111111111") .setFaxNumber("+1.1111111111") - .setTypes(ImmutableSet.of(RegistrarPoc.Type.ADMIN)) + .setTypes(ImmutableSet.of(RegistrarPocBase.Type.ADMIN)) .build(), new RegistrarPoc.Builder() .setRegistrar(registrar) @@ -92,7 +93,7 @@ class RegistrarWhoisResponseTest { .setEmailAddress("janeregistrar@example-registrar.tld") .setPhoneNumber("+1.3105551214") .setFaxNumber("+1.3105551213") - .setTypes(ImmutableSet.of(RegistrarPoc.Type.ADMIN)) + .setTypes(ImmutableSet.of(RegistrarPocBase.Type.ADMIN)) .setVisibleInWhoisAsAdmin(true) .build(), new RegistrarPoc.Builder() @@ -101,7 +102,7 @@ class RegistrarWhoisResponseTest { .setEmailAddress("janedoe@example-registrar.tld") .setPhoneNumber("+1.1111111112") .setFaxNumber("+1.1111111112") - .setTypes(ImmutableSet.of(RegistrarPoc.Type.TECH)) + .setTypes(ImmutableSet.of(RegistrarPocBase.Type.TECH)) .build(), new RegistrarPoc.Builder() .setRegistrar(registrar) @@ -109,7 +110,7 @@ class RegistrarWhoisResponseTest { .setEmailAddress("johngeek@example-registrar.tld") .setPhoneNumber("+1.3105551215") .setFaxNumber("+1.3105551216") - .setTypes(ImmutableSet.of(RegistrarPoc.Type.TECH)) + .setTypes(ImmutableSet.of(RegistrarPocBase.Type.TECH)) .setVisibleInWhoisAsTech(true) .build()); persistResource(registrar); diff --git a/core/src/test/java/google/registry/whois/WhoisActionTest.java b/core/src/test/java/google/registry/whois/WhoisActionTest.java index 9974860d3..385f8dd8b 100644 --- a/core/src/test/java/google/registry/whois/WhoisActionTest.java +++ b/core/src/test/java/google/registry/whois/WhoisActionTest.java @@ -17,8 +17,8 @@ package google.registry.whois; import static com.google.common.truth.Truth.assertThat; import static google.registry.bsa.persistence.BsaTestingUtils.persistBsaLabel; import static google.registry.model.EppResourceUtils.loadByForeignKeyCached; -import static google.registry.model.registrar.Registrar.State.ACTIVE; -import static google.registry.model.registrar.Registrar.Type.PDT; +import static google.registry.model.registrar.RegistrarBase.State.ACTIVE; +import static google.registry.model.registrar.RegistrarBase.Type.PDT; import static google.registry.model.tld.Tlds.getTlds; import static google.registry.testing.DatabaseHelper.createTlds; import static google.registry.testing.DatabaseHelper.loadRegistrar; diff --git a/core/src/test/java/google/registry/whois/WhoisHttpActionTest.java b/core/src/test/java/google/registry/whois/WhoisHttpActionTest.java index 0bfaeda21..6fd570b59 100644 --- a/core/src/test/java/google/registry/whois/WhoisHttpActionTest.java +++ b/core/src/test/java/google/registry/whois/WhoisHttpActionTest.java @@ -17,7 +17,7 @@ package google.registry.whois; import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8; import static com.google.common.truth.Truth.assertThat; import static google.registry.bsa.persistence.BsaTestingUtils.persistBsaLabel; -import static google.registry.model.registrar.Registrar.State.ACTIVE; +import static google.registry.model.registrar.RegistrarBase.State.ACTIVE; import static google.registry.testing.DatabaseHelper.createTlds; import static google.registry.testing.DatabaseHelper.loadRegistrar; import static google.registry.testing.DatabaseHelper.persistResource; 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 e97788845..0f9a2508a 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 - 2024-04-24 21:30:39.425345476 + 2024-04-29 19:20:24.144414867 last flyway file @@ -280,7 +280,7 @@ td.section { generated by SchemaCrawler 16.10.1 generated on - 2024-04-24 21:30:39.425345476 + 2024-04-29 19:20:24.144414867 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 29bc649d7..b6511ae9c 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">2024-04-24 21:30:36.545885131</td> + <td class="property_value">2024-04-29 19:20:21.522169488</td> </tr> <tr> <td class="property_name">last flyway file</td> @@ -280,7 +280,7 @@ td.section { <text text-anchor="start" x="4443.5" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">generated by</text> <text text-anchor="start" x="4526.5" y="-29.8" font-family="Helvetica,sans-Serif" font-size="14.00">SchemaCrawler 16.10.1</text> <text text-anchor="start" x="4442.5" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">generated on</text> - <text text-anchor="start" x="4526.5" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">2024-04-24 21:30:36.545885131</text> + <text text-anchor="start" x="4526.5" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">2024-04-29 19:20:21.522169488</text> <polygon fill="none" stroke="#888888" points="4439,-4 4439,-44 4726,-44 4726,-4 4439,-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 0633d5005..27332b18a 100644 --- a/db/src/main/resources/sql/schema/db-schema.sql.generated +++ b/db/src/main/resources/sql/schema/db-schema.sql.generated @@ -130,6 +130,20 @@ primary key (revision_id) ); + create table "ConsoleEppActionHistory" ( + history_revision_id int8 not null, + history_method text not null, + history_modification_time timestamptz not null, + history_request_body text, + history_type text not null, + history_url text not null, + history_entry_class text not null, + repo_id text not null, + revision_id int8 not null, + history_acting_user text not null, + primary key (history_revision_id) + ); + create table "Contact" ( repo_id text not null, update_timestamp timestamptz, @@ -663,6 +677,88 @@ primary key (email_address, registrar_id) ); + create table "RegistrarPocUpdateHistory" ( + history_revision_id int8 not null, + history_method text not null, + history_modification_time timestamptz not null, + history_request_body text, + history_type text not null, + history_url text not null, + email_address text not null, + registrar_id text not null, + allowed_to_set_registry_lock_password boolean not null, + fax_number text, + login_email_address text, + name text, + phone_number text, + registry_lock_email_address text, + registry_lock_password_hash text, + registry_lock_password_salt text, + types text[], + visible_in_domain_whois_as_abuse boolean not null, + visible_in_whois_as_admin boolean not null, + visible_in_whois_as_tech boolean not null, + history_acting_user text not null, + primary key (history_revision_id) + ); + + create table "RegistrarUpdateHistory" ( + history_revision_id int8 not null, + history_method text not null, + history_modification_time timestamptz not null, + history_request_body text, + history_type text not null, + history_url text not null, + allowed_tlds text[], + billing_account_map hstore, + block_premium_names boolean not null, + client_certificate text, + client_certificate_hash text, + contacts_require_syncing boolean not null, + creation_time timestamptz not null, + drive_folder_id text, + email_address text, + failover_client_certificate text, + failover_client_certificate_hash text, + fax_number text, + iana_identifier int8, + icann_referral_email text, + i18n_address_city text, + i18n_address_country_code text, + i18n_address_state text, + i18n_address_street_line1 text, + i18n_address_street_line2 text, + i18n_address_street_line3 text, + i18n_address_zip text, + ip_address_allow_list text[], + last_certificate_update_time timestamptz, + last_expiring_cert_notification_sent_date timestamptz, + last_expiring_failover_cert_notification_sent_date timestamptz, + localized_address_city text, + localized_address_country_code text, + localized_address_state text, + localized_address_street_line1 text, + localized_address_street_line2 text, + localized_address_street_line3 text, + localized_address_zip text, + password_hash text, + phone_number text, + phone_passcode text, + po_number text, + rdap_base_urls text[], + registrar_name text not null, + registry_lock_allowed boolean not null, + password_salt text, + state text, + type text not null, + url text, + whois_server text, + update_timestamp timestamptz, + registrar_id text not null, + history_acting_user text not null, + primary key (history_revision_id) + ); + create table "RegistryLock" ( revision_id bigserial not null, last_update_time timestamptz not null, @@ -795,6 +891,25 @@ registrar_roles hstore, primary key (id) ); + + create table "UserUpdateHistory" ( + history_revision_id int8 not null, + history_method text not null, + history_modification_time timestamptz not null, + history_request_body text, + history_type text not null, + history_url text not null, + user_id int8 not null, + email_address text not null, + registry_lock_password_hash text, + registry_lock_password_salt text, + global_role text not null, + is_admin boolean not null, + registrar_roles hstore, + update_timestamp timestamptz, + history_acting_user text not null, + primary key (history_revision_id) + ); create index allocation_token_domain_name_idx on "AllocationToken" (domain_name); create index IDX9g3s7mjv1yn4t06nqid39whss on "AllocationToken" (token_type); create index IDXtmlqd31dpvvd2g1h9i7erw6aj on "AllocationToken" (redemption_domain_repo_id); @@ -818,6 +933,9 @@ create index IDXp3usbtvk0v1m14i5tdp4xnxgc on "BillingRecurrence" (recurrence_end create index IDXp0pxi708hlu4n40qhbtihge8x on "BillingRecurrence" (recurrence_last_expansion); create index IDXjny8wuot75b5e6p38r47wdawu on "BillingRecurrence" (recurrence_time_of_year); create index IDXj874kw19bgdnkxo1rue45jwlw on "BsaDownload" (creation_time); +create index IDXcclyb3n5gbex8u8m9fjlujitw on "ConsoleEppActionHistory" (history_acting_user); +create index IDX6y67d6wsffmr6jcxax5ghwqhd on "ConsoleEppActionHistory" (repo_id); +create index IDXiahqo1d1fqdfknywmj2xbxl7t on "ConsoleEppActionHistory" (revision_id); create index IDX3y752kr9uh4kh6uig54vemx0l on "Contact" (creation_time); create index IDXtm415d6fe1rr35stm33s5mg18 on "Contact" (current_sponsor_registrar_id); create index IDXn1f711wicdnooa2mqb7g1m55o on "Contact" (deletion_time); @@ -877,6 +995,11 @@ create index premiumlist_name_idx on "PremiumList" (name); create index registrar_name_idx on "Registrar" (registrar_name); create index registrar_iana_identifier_idx on "Registrar" (iana_identifier); create index registrarpoc_login_email_idx on "RegistrarPoc" (login_email_address); +create index IDXrn6posxkx58de1cp09g5257cw on "RegistrarPocUpdateHistory" (history_acting_user); +create index IDXr1cxua6it0rxgt9tpyugspxk on "RegistrarPocUpdateHistory" (email_address); +create index IDXfr24wvpg8qalwqy4pni7evrpj on "RegistrarPocUpdateHistory" (registrar_id); +create index IDXm6k18dusy2lfi5y81k8g256sa on "RegistrarUpdateHistory" (history_acting_user); +create index IDX3d1mucv7axrhud8w8jl4vsu62 on "RegistrarUpdateHistory" (registrar_id); create index idx_registry_lock_verification_code on "RegistryLock" (verification_code); create index idx_registry_lock_registrar_id on "RegistryLock" (registrar_id); @@ -888,6 +1011,16 @@ create index spec11threatmatch_tld_idx on "Spec11ThreatMatch" (tld); create index spec11threatmatch_check_date_idx on "Spec11ThreatMatch" (check_date); create index user_email_address_idx on "User" (email_address); + alter table if exists "User" + add constraint UK_seshmd8qn3lcffsjw5j8wnbnd unique (email_address); +create index IDXbjacjlm8ianc4kxxvamnu94k5 on "UserUpdateHistory" (history_acting_user); +create index IDX5yqacw829y5bm6f7eajsq1cts on "UserUpdateHistory" (email_address); + + alter table if exists "ConsoleEppActionHistory" + add constraint FKb686b9os2nsjpv930npa4r3b4 + foreign key (history_acting_user) + references "User" (email_address); + alter table if exists "DelegationSignerData" add constraint FKtr24j9v14ph2mfuw2gsmt12kq foreign key (domain_repo_id) @@ -923,6 +1056,16 @@ create index user_email_address_idx on "User" (email_address); foreign key (domain_repo_id, domain_history_revision_id) references "DomainHistory"; + alter table if exists "RegistrarPocUpdateHistory" + add constraint FKftpbwctxtkc1i0njc0tdcaa2g + foreign key (history_acting_user) + references "User" (email_address); + + alter table if exists "RegistrarUpdateHistory" + add constraint FKsr7w342s7x5s5jvdti2axqeq8 + foreign key (history_acting_user) + references "User" (email_address); + alter table if exists "RegistryLock" add constraint FK2lhcwpxlnqijr96irylrh1707 foreign key (relock_revision_id) @@ -932,3 +1075,8 @@ create index user_email_address_idx on "User" (email_address); add constraint FK5ivlhvs3121yx2li5tqh54u4 foreign key (revision_id) references "SignedMarkRevocationList"; + + alter table if exists "UserUpdateHistory" + add constraint FK1s7bopbl3pwrhv3jkkofnv3o0 + foreign key (history_acting_user) + references "User" (email_address);