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 5550307d6..6a1345199 100644
--- a/core/src/main/java/google/registry/model/console/User.java
+++ b/core/src/main/java/google/registry/model/console/User.java
@@ -24,20 +24,34 @@ import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Index;
+import javax.persistence.Table;
/** A console user, either a registry employee or a registrar partner. */
+@Entity
+@Table(
+ indexes = {
+ @Index(columnList = "gaiaId", name = "user_gaia_id_idx"),
+ @Index(columnList = "emailAddress", name = "user_email_address_idx")
+ })
public class User extends ImmutableObject implements Buildable {
/** Autogenerated unique ID of this user. */
- private long id;
+ @Id private long id;
/** GAIA ID associated with the user in question. */
+ @Column(nullable = false)
private String gaiaId;
/** 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;
/**
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 882935bd5..e21d8616f 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,10 @@ import com.google.common.collect.ImmutableMap;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import java.util.Map;
+import javax.persistence.Column;
+import javax.persistence.Embeddable;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
/**
* Contains the global and per-registrar roles for a given user.
@@ -28,15 +32,19 @@ import java.util.Map;
*
See go/nomulus-console-authz for more
* information.
*/
+@Embeddable
public class UserRoles extends ImmutableObject implements Buildable {
/** Whether the user is a global admin, who has access to everything. */
+ @Column(nullable = false)
private boolean isAdmin = false;
/**
* The global role (e.g. {@link GlobalRole#SUPPORT_AGENT}) that the user has across all
* registrars.
*/
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false)
private GlobalRole globalRole = GlobalRole.NONE;
/** Any per-registrar roles that this user may have. */
diff --git a/core/src/main/java/google/registry/persistence/converter/RegistrarToRoleConverter.java b/core/src/main/java/google/registry/persistence/converter/RegistrarToRoleConverter.java
new file mode 100644
index 000000000..0ac303875
--- /dev/null
+++ b/core/src/main/java/google/registry/persistence/converter/RegistrarToRoleConverter.java
@@ -0,0 +1,50 @@
+// Copyright 2022 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.persistence.converter;
+
+import google.registry.model.console.RegistrarRole;
+import java.util.Map;
+import javax.persistence.Converter;
+
+/** JPA converter for storing / retrieving {@code Map} objects. */
+@Converter(autoApply = true)
+public class RegistrarToRoleConverter
+ extends StringMapConverterBase> {
+
+ @Override
+ protected String convertKeyToString(String key) {
+ return key;
+ }
+
+ @Override
+ protected String convertValueToString(RegistrarRole value) {
+ return value.toString();
+ }
+
+ @Override
+ protected String convertStringToKey(String string) {
+ return string;
+ }
+
+ @Override
+ protected RegistrarRole convertStringToValue(String string) {
+ return RegistrarRole.valueOf(string);
+ }
+
+ @Override
+ protected Map convertMapToDerivedType(Map map) {
+ return map;
+ }
+}
diff --git a/core/src/main/resources/META-INF/persistence.xml b/core/src/main/resources/META-INF/persistence.xml
index ccbe28e1a..1a790641f 100644
--- a/core/src/main/resources/META-INF/persistence.xml
+++ b/core/src/main/resources/META-INF/persistence.xml
@@ -43,6 +43,7 @@
google.registry.model.billing.BillingEvent$Recurring
google.registry.model.common.Cursor
google.registry.model.common.DatabaseMigrationStateSchedule
+ google.registry.model.console.User
google.registry.model.contact.ContactHistory
google.registry.model.contact.ContactResource
google.registry.model.domain.Domain
@@ -91,6 +92,7 @@
google.registry.persistence.converter.LocalDateConverter
google.registry.persistence.converter.PostalInfoChoiceListConverter
google.registry.persistence.converter.RegistrarPocSetConverter
+ google.registry.persistence.converter.RegistrarToRoleConverter
google.registry.persistence.converter.Spec11ThreatMatchThreatTypeSetConverter
google.registry.persistence.converter.StatusValueSetConverter
google.registry.persistence.converter.StringListConverter
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 ac4fb0403..fe03f1458 100644
--- a/core/src/test/java/google/registry/model/console/UserTest.java
+++ b/core/src/test/java/google/registry/model/console/UserTest.java
@@ -15,12 +15,71 @@
package google.registry.model.console;
import static com.google.common.truth.Truth.assertThat;
+import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
+import static google.registry.testing.DatabaseHelper.persistResource;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import google.registry.model.EntityTestCase;
import org.junit.jupiter.api.Test;
/** Tests for {@link User}. */
-public class UserTest {
+public class UserTest extends EntityTestCase {
+
+ UserTest() {
+ super(JpaEntityCoverageCheck.ENABLED);
+ }
+
+ @Test
+ void testPersistence_lookupByEmail() {
+ User user =
+ new User.Builder()
+ .setGaiaId("gaiaId")
+ .setEmailAddress("email@email.com")
+ .setUserRoles(
+ new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).setIsAdmin(true).build())
+ .build();
+ persistResource(user);
+ jpaTm()
+ .transact(
+ () -> {
+ assertThat(
+ jpaTm()
+ .query("FROM User WHERE emailAddress = 'email@email.com'", User.class)
+ .getSingleResult())
+ .isEqualTo(user);
+ assertThat(
+ jpaTm()
+ .query("FROM User WHERE emailAddress = 'bad@fake.com'", User.class)
+ .getResultList())
+ .isEmpty();
+ });
+ }
+
+ @Test
+ void testPersistence_lookupByGaiaId() {
+ User user =
+ new User.Builder()
+ .setGaiaId("gaiaId")
+ .setEmailAddress("email@email.com")
+ .setUserRoles(
+ new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).setIsAdmin(true).build())
+ .build();
+ persistResource(user);
+ jpaTm()
+ .transact(
+ () -> {
+ assertThat(
+ jpaTm()
+ .query("FROM User WHERE gaiaId = 'gaiaId'", User.class)
+ .getSingleResult())
+ .isEqualTo(user);
+ assertThat(
+ jpaTm()
+ .query("FROM User WHERE gaiaId = 'badGaiaId'", User.class)
+ .getResultList())
+ .isEmpty();
+ });
+ }
@Test
void testFailure_badInputs() {
diff --git a/core/src/test/java/google/registry/persistence/converter/RegistrarToRoleConverterTest.java b/core/src/test/java/google/registry/persistence/converter/RegistrarToRoleConverterTest.java
new file mode 100644
index 000000000..68168f995
--- /dev/null
+++ b/core/src/test/java/google/registry/persistence/converter/RegistrarToRoleConverterTest.java
@@ -0,0 +1,68 @@
+// Copyright 2022 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.persistence.converter;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import google.registry.model.ImmutableObject;
+import google.registry.model.console.RegistrarRole;
+import google.registry.persistence.transaction.JpaTestExtensions;
+import google.registry.persistence.transaction.JpaTestExtensions.JpaUnitTestExtension;
+import google.registry.testing.DatabaseHelper;
+import java.util.Map;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+/** Tests for {@link RegistrarToRoleConverter}. */
+public class RegistrarToRoleConverterTest {
+
+ @RegisterExtension
+ public final JpaUnitTestExtension jpa =
+ new JpaTestExtensions.Builder().withEntityClass(TestEntity.class).buildUnitTestExtension();
+
+ @Test
+ void testRoundTripConversion() {
+ Map map =
+ ImmutableMap.of(
+ "TheRegistrar",
+ RegistrarRole.ACCOUNT_MANAGER,
+ "NewRegistrar",
+ RegistrarRole.PRIMARY_CONTACT,
+ "FooRegistrar",
+ RegistrarRole.TECH_CONTACT);
+ TestEntity entity = new TestEntity(map);
+ DatabaseHelper.insertInDb(entity);
+ TestEntity persisted = Iterables.getOnlyElement(DatabaseHelper.loadAllOf(TestEntity.class));
+ assertThat(persisted.map).isEqualTo(map);
+ }
+
+ @Entity(name = "TestEntity")
+ private static class TestEntity extends ImmutableObject {
+
+ @Id String name = "id";
+
+ Map map;
+
+ private TestEntity() {}
+
+ private TestEntity(Map map) {
+ this.map = map;
+ }
+ }
+}
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 f60920d3c..a3be844f0 100644
--- a/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java
+++ b/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java
@@ -18,6 +18,7 @@ import static com.google.common.truth.Truth.assert_;
import google.registry.model.billing.BillingEventTest;
import google.registry.model.common.CursorTest;
+import google.registry.model.console.UserTest;
import google.registry.model.contact.ContactResourceTest;
import google.registry.model.domain.DomainSqlTest;
import google.registry.model.domain.token.AllocationTokenTest;
@@ -101,6 +102,7 @@ import org.junit.runner.RunWith;
SignedMarkRevocationListDaoTest.class,
Spec11ThreatMatchTest.class,
TmchCrlTest.class,
+ UserTest.class,
// AfterSuiteTest must be the last entry. See class javadoc for details.
AfterSuiteTest.class
})
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 65efc7421..d0065992e 100644
--- a/db/src/main/resources/sql/schema/db-schema.sql.generated
+++ b/db/src/main/resources/sql/schema/db-schema.sql.generated
@@ -740,6 +740,18 @@
url text not null,
primary key (id)
);
+
+ create table "User" (
+ id int8 not null,
+ email_address text not null,
+ gaia_id 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,
+ primary key (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);
@@ -825,6 +837,8 @@ create index reservedlist_name_idx on "ReservedList" (name);
create index spec11threatmatch_registrar_id_idx on "Spec11ThreatMatch" (registrar_id);
create index spec11threatmatch_tld_idx on "Spec11ThreatMatch" (tld);
create index spec11threatmatch_check_date_idx on "Spec11ThreatMatch" (check_date);
+create index user_gaia_id_idx on "User" (gaia_id);
+create index user_email_address_idx on "User" (email_address);
alter table if exists "DelegationSignerData"
add constraint FKtr24j9v14ph2mfuw2gsmt12kq