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);