diff --git a/core/src/main/java/google/registry/model/console/ConsolePermission.java b/core/src/main/java/google/registry/model/console/ConsolePermission.java
new file mode 100644
index 000000000..edcf01edb
--- /dev/null
+++ b/core/src/main/java/google/registry/model/console/ConsolePermission.java
@@ -0,0 +1,67 @@
+// 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.model.console;
+
+/** Permissions that users may have in the UI, either per-registrar or globally. */
+public enum ConsolePermission {
+ /** Add, update, or remove other console users. */
+ MANAGE_USERS,
+ /** Add, update, or remove registrars. */
+ MANAGE_REGISTRARS,
+ /** Manage related registrars, e.g. when one registrar owns another. */
+ MANAGE_ACCREDITATION,
+ /** Set up the EPP connection (e.g. certs). */
+ CONFIGURE_EPP_CONNECTION,
+ /** Retrieve the unredacted registrant email from a domain. */
+ GET_REGISTRANT_EMAIL,
+ /** Suspend a domain for compliance (non-URS) reasons. */
+ SUSPEND_DOMAIN,
+ /** Suspend a domain for the Uniform Rapid Suspension process. */
+ SUSPEND_DOMAIN_URS,
+ /** Download a list of domains under management. */
+ DOWNLOAD_DOMAINS,
+ /** Change the password for a registrar. */
+ CHANGE_NOMULUS_PASSWORD,
+ /** View all possible TLDs. */
+ VIEW_TLD_PORTFOLIO,
+ /** Onboarding the registrar(s) to extra programs like registry locking. */
+ ONBOARD_ADDITIONAL_PROGRAMS,
+ /** Execute arbitrary EPP commands through the UI. */
+ EXECUTE_EPP_COMMANDS,
+ /** Send messages to the registry support team. */
+ CONTACT_SUPPORT,
+ /** Access billing and payment details for a registrar. */
+ ACCESS_BILLING_DETAILS,
+ /** Access any available documentation about the registry. */
+ ACCESS_DOCUMENTATION,
+ /** Change the documentation available in the UI about the registry. */
+ MANAGE_DOCUMENTATION,
+ /** Viewing the onboarding status of a registrar on a TLD. */
+ CHECK_ONBOARDING_STATUS,
+ /** View premium and/or reserved lists for a TLD. */
+ VIEW_PREMIUM_RESERVED_LISTS,
+ /** Sign and/or change legal documents like RRAs. */
+ UPLOAD_CONTRACTS,
+ /** Viewing legal documents like RRAs. */
+ ACCESS_CONTRACTS,
+ /** View analytics on operations and billing data (possibly TBD). */
+ VIEW_OPERATIONAL_DATA,
+ /** Create announcements that can be displayed in the UI. */
+ SEND_ANNOUNCEMENTS,
+ /** View announcements in the UI. */
+ VIEW_ANNOUNCEMENTS,
+ /** Viewing a record of actions performed in the UI for a particular registrar. */
+ VIEW_ACTIVITY_LOG
+}
diff --git a/core/src/main/java/google/registry/model/console/ConsoleRoleDefinitions.java b/core/src/main/java/google/registry/model/console/ConsoleRoleDefinitions.java
new file mode 100644
index 000000000..fbf5db433
--- /dev/null
+++ b/core/src/main/java/google/registry/model/console/ConsoleRoleDefinitions.java
@@ -0,0 +1,104 @@
+// 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.model.console;
+
+import com.google.common.collect.ImmutableSet;
+
+/**
+ * Definitions of the {@link ConsolePermission} sets that each role contains.
+ *
+ *
Note: within the "registry" and "registrar" groupings, permissions expand hierarchically,
+ * where each role has all the permissions of the role below it plus some others.
+ */
+public class ConsoleRoleDefinitions {
+
+ /** Permissions for a registry support agent. */
+ static final ImmutableSet SUPPORT_AGENT_PERMISSIONS =
+ ImmutableSet.of(
+ ConsolePermission.MANAGE_USERS,
+ ConsolePermission.MANAGE_ACCREDITATION,
+ ConsolePermission.CONFIGURE_EPP_CONNECTION,
+ ConsolePermission.DOWNLOAD_DOMAINS,
+ ConsolePermission.VIEW_TLD_PORTFOLIO,
+ ConsolePermission.ONBOARD_ADDITIONAL_PROGRAMS,
+ ConsolePermission.CONTACT_SUPPORT,
+ ConsolePermission.ACCESS_BILLING_DETAILS,
+ ConsolePermission.ACCESS_DOCUMENTATION,
+ ConsolePermission.SUSPEND_DOMAIN_URS,
+ ConsolePermission.CHECK_ONBOARDING_STATUS,
+ ConsolePermission.VIEW_PREMIUM_RESERVED_LISTS,
+ ConsolePermission.UPLOAD_CONTRACTS,
+ ConsolePermission.ACCESS_CONTRACTS,
+ ConsolePermission.VIEW_OPERATIONAL_DATA,
+ ConsolePermission.SEND_ANNOUNCEMENTS,
+ ConsolePermission.VIEW_ANNOUNCEMENTS,
+ ConsolePermission.VIEW_ACTIVITY_LOG);
+
+ /** Permissions for a registry support lead. */
+ static final ImmutableSet SUPPORT_LEAD_PERMISSIONS =
+ new ImmutableSet.Builder()
+ .addAll(SUPPORT_AGENT_PERMISSIONS)
+ .add(
+ ConsolePermission.MANAGE_REGISTRARS,
+ ConsolePermission.GET_REGISTRANT_EMAIL,
+ ConsolePermission.SUSPEND_DOMAIN,
+ ConsolePermission.EXECUTE_EPP_COMMANDS,
+ ConsolePermission.MANAGE_DOCUMENTATION)
+ .build();
+
+ /** Permissions for a registry full-time employee. */
+ static final ImmutableSet FTE_PERMISSIONS =
+ new ImmutableSet.Builder()
+ .addAll(SUPPORT_LEAD_PERMISSIONS)
+ .add(ConsolePermission.CHANGE_NOMULUS_PASSWORD)
+ .build();
+
+ /** Permissions for a registrar partner account manager. */
+ static final ImmutableSet ACCOUNT_MANAGER_PERMISSIONS =
+ ImmutableSet.of(
+ ConsolePermission.DOWNLOAD_DOMAINS,
+ ConsolePermission.VIEW_TLD_PORTFOLIO,
+ ConsolePermission.CONTACT_SUPPORT,
+ ConsolePermission.ACCESS_DOCUMENTATION,
+ ConsolePermission.VIEW_PREMIUM_RESERVED_LISTS,
+ ConsolePermission.VIEW_OPERATIONAL_DATA,
+ ConsolePermission.VIEW_ANNOUNCEMENTS);
+
+ /** Permissions for the tech contact of a registrar. */
+ static final ImmutableSet TECH_CONTACT_PERMISSIONS =
+ new ImmutableSet.Builder()
+ .addAll(ACCOUNT_MANAGER_PERMISSIONS)
+ .add(
+ ConsolePermission.MANAGE_ACCREDITATION,
+ ConsolePermission.CONFIGURE_EPP_CONNECTION,
+ ConsolePermission.CHANGE_NOMULUS_PASSWORD,
+ ConsolePermission.ONBOARD_ADDITIONAL_PROGRAMS,
+ ConsolePermission.EXECUTE_EPP_COMMANDS,
+ ConsolePermission.ACCESS_BILLING_DETAILS,
+ ConsolePermission.CHECK_ONBOARDING_STATUS,
+ ConsolePermission.UPLOAD_CONTRACTS,
+ ConsolePermission.ACCESS_CONTRACTS,
+ ConsolePermission.VIEW_ACTIVITY_LOG)
+ .build();
+
+ /** Permissions for the primary registrar contact. */
+ static final ImmutableSet PRIMARY_CONTACT_PERMISSIONS =
+ new ImmutableSet.Builder()
+ .addAll(TECH_CONTACT_PERMISSIONS)
+ .add(ConsolePermission.MANAGE_USERS)
+ .build();
+
+ private ConsoleRoleDefinitions() {}
+}
diff --git a/core/src/main/java/google/registry/model/console/GlobalRole.java b/core/src/main/java/google/registry/model/console/GlobalRole.java
new file mode 100644
index 000000000..ad17f7703
--- /dev/null
+++ b/core/src/main/java/google/registry/model/console/GlobalRole.java
@@ -0,0 +1,44 @@
+// 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.model.console;
+
+import static google.registry.model.console.ConsoleRoleDefinitions.FTE_PERMISSIONS;
+import static google.registry.model.console.ConsoleRoleDefinitions.SUPPORT_AGENT_PERMISSIONS;
+import static google.registry.model.console.ConsoleRoleDefinitions.SUPPORT_LEAD_PERMISSIONS;
+
+import com.google.common.collect.ImmutableSet;
+
+/** Roles for registry employees that apply across all registrars. */
+public enum GlobalRole {
+
+ /** The user has no global role, i.e. they're a registrar partner. */
+ NONE(ImmutableSet.of()),
+ /** The user is a registry support agent. */
+ SUPPORT_AGENT(SUPPORT_AGENT_PERMISSIONS),
+ /** The user is a registry support lead. */
+ SUPPORT_LEAD(SUPPORT_LEAD_PERMISSIONS),
+ /** The user is a registry full-time employee. */
+ FTE(FTE_PERMISSIONS);
+
+ private final ImmutableSet permissions;
+
+ GlobalRole(ImmutableSet permissions) {
+ this.permissions = permissions;
+ }
+
+ public boolean hasPermission(ConsolePermission permission) {
+ return permissions.contains(permission);
+ }
+}
diff --git a/core/src/main/java/google/registry/model/console/RegistrarRole.java b/core/src/main/java/google/registry/model/console/RegistrarRole.java
new file mode 100644
index 000000000..503f0ca29
--- /dev/null
+++ b/core/src/main/java/google/registry/model/console/RegistrarRole.java
@@ -0,0 +1,42 @@
+// 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.model.console;
+
+import static google.registry.model.console.ConsoleRoleDefinitions.ACCOUNT_MANAGER_PERMISSIONS;
+import static google.registry.model.console.ConsoleRoleDefinitions.PRIMARY_CONTACT_PERMISSIONS;
+import static google.registry.model.console.ConsoleRoleDefinitions.TECH_CONTACT_PERMISSIONS;
+
+import com.google.common.collect.ImmutableSet;
+
+/** Roles for registrar partners that apply to only one registrar. */
+public enum RegistrarRole {
+
+ /** The user is a standard account manager at a registrar. */
+ ACCOUNT_MANAGER(ACCOUNT_MANAGER_PERMISSIONS),
+ /** The user is a technical contact of a registrar. */
+ TECH_CONTACT(TECH_CONTACT_PERMISSIONS),
+ /** The user is the primary contact at a registrar. */
+ PRIMARY_CONTACT(PRIMARY_CONTACT_PERMISSIONS);
+
+ private final ImmutableSet permissions;
+
+ RegistrarRole(ImmutableSet permissions) {
+ this.permissions = permissions;
+ }
+
+ public boolean hasPermission(ConsolePermission permission) {
+ return permissions.contains(permission);
+ }
+}
diff --git a/core/src/main/java/google/registry/model/console/User.java b/core/src/main/java/google/registry/model/console/User.java
new file mode 100644
index 000000000..b47a4fa31
--- /dev/null
+++ b/core/src/main/java/google/registry/model/console/User.java
@@ -0,0 +1,148 @@
+// 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.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.ImmutableObject;
+
+/** A console user, either a registry employee or a registrar partner. */
+public class User extends ImmutableObject implements Buildable {
+
+ /** Autogenerated unique ID of this user. */
+ private long id;
+
+ /** GAIA ID associated with the user in question. */
+ private String gaiaId;
+
+ /** Email address of the user in question. */
+ private String emailAddress;
+
+ /** Roles (which grant permissions) associated with this user. */
+ private UserRoles userRoles;
+
+ /**
+ * 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;
+
+ public String getGaiaId() {
+ return gaiaId;
+ }
+
+ public String getEmailAddress() {
+ return emailAddress;
+ }
+
+ public UserRoles getUserRoles() {
+ return userRoles;
+ }
+
+ 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 hashPassword(registryLockPassword, registryLockPasswordSalt)
+ .equals(registryLockPasswordHash);
+ }
+
+ @Override
+ public Builder asBuilder() {
+ return new Builder(clone(this));
+ }
+
+ /** Builder for constructing immutable {@link User} objects. */
+ public static class Builder extends Buildable.Builder {
+
+ public Builder() {}
+
+ public Builder(User user) {
+ super(user);
+ }
+
+ public User build() {
+ checkArgumentNotNull(getInstance().gaiaId, "Gaia ID cannot be null");
+ checkArgumentNotNull(getInstance().emailAddress, "Email address cannot be null");
+ checkArgumentNotNull(getInstance().userRoles, "User roles cannot be null");
+ return super.build();
+ }
+
+ public Builder setGaiaId(String gaiaId) {
+ checkArgument(!isNullOrEmpty(gaiaId), "Gaia ID cannot be null or empty");
+ getInstance().gaiaId = gaiaId;
+ return this;
+ }
+
+ 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 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 user");
+ checkArgument(
+ !isNullOrEmpty(registryLockPassword), "Registry lock password was null or empty");
+ getInstance().registryLockPasswordSalt = base64().encode(SALT_SUPPLIER.get());
+ getInstance().registryLockPasswordHash =
+ hashPassword(registryLockPassword, getInstance().registryLockPasswordSalt);
+ getInstance().allowedToSetRegistryLockPassword = false;
+ return this;
+ }
+ }
+}
diff --git a/core/src/main/java/google/registry/model/console/UserRoles.java b/core/src/main/java/google/registry/model/console/UserRoles.java
new file mode 100644
index 000000000..882935bd5
--- /dev/null
+++ b/core/src/main/java/google/registry/model/console/UserRoles.java
@@ -0,0 +1,120 @@
+// 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.model.console;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
+
+import com.google.common.collect.ImmutableMap;
+import google.registry.model.Buildable;
+import google.registry.model.ImmutableObject;
+import java.util.Map;
+
+/**
+ * Contains the global and per-registrar roles for a given user.
+ *
+ * See go/nomulus-console-authz for more
+ * information.
+ */
+public class UserRoles extends ImmutableObject implements Buildable {
+
+ /** Whether the user is a global admin, who has access to everything. */
+ private boolean isAdmin = false;
+
+ /**
+ * The global role (e.g. {@link GlobalRole#SUPPORT_AGENT}) that the user has across all
+ * registrars.
+ */
+ private GlobalRole globalRole = GlobalRole.NONE;
+
+ /** Any per-registrar roles that this user may have. */
+ private Map registrarRoles = ImmutableMap.of();
+
+ /** Whether the user is a global admin, who has access to everything. */
+ public boolean isAdmin() {
+ return isAdmin;
+ }
+
+ /**
+ * The global role (e.g. {@link GlobalRole#SUPPORT_AGENT}) that the user has across all
+ * registrars.
+ */
+ public GlobalRole getGlobalRole() {
+ return globalRole;
+ }
+
+ /** Any per-registrar roles that this user may have. */
+ public Map getRegistrarRoles() {
+ return ImmutableMap.copyOf(registrarRoles);
+ }
+
+ /** If the user has the given permission either globally or on the given registrar. */
+ public boolean hasPermission(String registrarId, ConsolePermission permission) {
+ if (hasGlobalPermission(permission)) {
+ return true;
+ }
+ if (!registrarRoles.containsKey(registrarId)) {
+ return false;
+ }
+ return registrarRoles.get(registrarId).hasPermission(permission);
+ }
+
+ /** If the user has the given permission globally across all registrars. */
+ public boolean hasGlobalPermission(ConsolePermission permission) {
+ return isAdmin || globalRole.hasPermission(permission);
+ }
+
+ @Override
+ public Builder asBuilder() {
+ return new Builder(clone(this));
+ }
+
+ /** Builder for constructing immutable {@link UserRoles} objects. */
+ public static class Builder extends Buildable.Builder {
+
+ public Builder() {}
+
+ private Builder(UserRoles userRoles) {
+ super(userRoles);
+ }
+
+ @Override
+ public UserRoles build() {
+ // Users should only have a global role or per-registrar roles, not both
+ checkArgument(
+ getInstance().globalRole.equals(GlobalRole.NONE)
+ || getInstance().registrarRoles.isEmpty(),
+ "Users cannot have both global and per-registrar roles");
+ return super.build();
+ }
+
+ public Builder setIsAdmin(boolean isAdmin) {
+ getInstance().isAdmin = isAdmin;
+ return this;
+ }
+
+ public Builder setGlobalRole(GlobalRole globalRole) {
+ checkArgumentNotNull(globalRole, "Global role cannot be null");
+ getInstance().globalRole = globalRole;
+ return this;
+ }
+
+ public Builder setRegistrarRoles(Map registrarRoles) {
+ checkArgumentNotNull(registrarRoles, "Registrar roles map cannot be null");
+ getInstance().registrarRoles = ImmutableMap.copyOf(registrarRoles);
+ 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 c5ed3a0c6..185a65e14 100644
--- a/core/src/main/java/google/registry/model/registrar/Registrar.java
+++ b/core/src/main/java/google/registry/model/registrar/Registrar.java
@@ -964,7 +964,7 @@ public class Registrar extends ImmutableObject
}
/** Verifies that the email address in question is not null and has a valid format. */
- static String checkValidEmail(String email) {
+ public static String checkValidEmail(String email) {
checkNotNull(email, "Provided email was null");
try {
InternetAddress internetAddress = new InternetAddress(email, true);
diff --git a/core/src/test/java/google/registry/model/console/ConsoleRoleDefinitionsTest.java b/core/src/test/java/google/registry/model/console/ConsoleRoleDefinitionsTest.java
new file mode 100644
index 000000000..15dcddbd3
--- /dev/null
+++ b/core/src/test/java/google/registry/model/console/ConsoleRoleDefinitionsTest.java
@@ -0,0 +1,69 @@
+// 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.model.console;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link ConsoleRoleDefinitions}. */
+public class ConsoleRoleDefinitionsTest {
+
+ @Test
+ void testHierarchicalPermissions_registry() {
+ // Note: we can't use Truth's IterableSubject to check all the subset/superset restrictions
+ // because it is generic to iterables and doesn't know about sets.
+ assertThat(
+ ConsoleRoleDefinitions.SUPPORT_LEAD_PERMISSIONS.containsAll(
+ ConsoleRoleDefinitions.SUPPORT_AGENT_PERMISSIONS))
+ .isTrue();
+ assertThat(
+ ConsoleRoleDefinitions.SUPPORT_AGENT_PERMISSIONS.containsAll(
+ ConsoleRoleDefinitions.SUPPORT_LEAD_PERMISSIONS))
+ .isFalse();
+
+ assertThat(
+ ConsoleRoleDefinitions.FTE_PERMISSIONS.containsAll(
+ ConsoleRoleDefinitions.SUPPORT_LEAD_PERMISSIONS))
+ .isTrue();
+ assertThat(
+ ConsoleRoleDefinitions.SUPPORT_LEAD_PERMISSIONS.containsAll(
+ ConsoleRoleDefinitions.FTE_PERMISSIONS))
+ .isFalse();
+ }
+
+ @Test
+ void testHierarchicalPermissions_registrar() {
+ // Note: we can't use Truth's IterableSubject to check all the subset/superset restrictions
+ // because it is generic to iterables and doesn't know about sets.
+ assertThat(
+ ConsoleRoleDefinitions.SUPPORT_LEAD_PERMISSIONS.containsAll(
+ ConsoleRoleDefinitions.SUPPORT_AGENT_PERMISSIONS))
+ .isTrue();
+ assertThat(
+ ConsoleRoleDefinitions.SUPPORT_AGENT_PERMISSIONS.containsAll(
+ ConsoleRoleDefinitions.SUPPORT_LEAD_PERMISSIONS))
+ .isFalse();
+
+ assertThat(
+ ConsoleRoleDefinitions.SUPPORT_LEAD_PERMISSIONS.containsAll(
+ ConsoleRoleDefinitions.SUPPORT_AGENT_PERMISSIONS))
+ .isTrue();
+ assertThat(
+ ConsoleRoleDefinitions.SUPPORT_AGENT_PERMISSIONS.containsAll(
+ ConsoleRoleDefinitions.SUPPORT_LEAD_PERMISSIONS))
+ .isFalse();
+ }
+}
diff --git a/core/src/test/java/google/registry/model/console/UserRolesTest.java b/core/src/test/java/google/registry/model/console/UserRolesTest.java
new file mode 100644
index 000000000..f40a7e663
--- /dev/null
+++ b/core/src/test/java/google/registry/model/console/UserRolesTest.java
@@ -0,0 +1,77 @@
+// 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.model.console;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.google.common.collect.ImmutableMap;
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link UserRoles}. */
+public class UserRolesTest {
+
+ @Test
+ void testDefaults() {
+ UserRoles userRoles = new UserRoles.Builder().build();
+ for (ConsolePermission permission : ConsolePermission.values()) {
+ assertThat(userRoles.getGlobalRole().hasPermission(permission)).isFalse();
+ }
+ assertThat(userRoles.getRegistrarRoles()).isEmpty();
+ assertThat(userRoles.isAdmin()).isFalse();
+ }
+
+ @Test
+ void testAdmin_overridesAll() {
+ UserRoles userRoles = new UserRoles.Builder().setIsAdmin(true).build();
+ for (ConsolePermission permission : ConsolePermission.values()) {
+ assertThat(userRoles.hasGlobalPermission(permission)).isTrue();
+ assertThat(userRoles.hasPermission("TheRegistrar", permission)).isTrue();
+ }
+ }
+
+ @Test
+ void testRegistrarPermission_withGlobal() {
+ UserRoles userRoles = new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build();
+ for (ConsolePermission permission : ConsolePermission.values()) {
+ assertThat(userRoles.hasGlobalPermission(permission)).isTrue();
+ assertThat(userRoles.hasPermission("TheRegistrar", permission)).isTrue();
+ }
+ }
+
+ @Test
+ void testRegistrarRoles() {
+ UserRoles userRoles =
+ new UserRoles.Builder()
+ .setGlobalRole(GlobalRole.NONE)
+ .setIsAdmin(false)
+ .setRegistrarRoles(ImmutableMap.of("TheRegistrar", RegistrarRole.PRIMARY_CONTACT))
+ .build();
+ assertThat(userRoles.hasPermission("TheRegistrar", ConsolePermission.MANAGE_USERS)).isTrue();
+ assertThat(userRoles.hasPermission("TheRegistrar", ConsolePermission.SUSPEND_DOMAIN)).isFalse();
+ assertThat(userRoles.hasPermission("nonexistent", ConsolePermission.MANAGE_USERS)).isFalse();
+ }
+
+ @Test
+ void testFailure_globalOrPerRegistrar() {
+ UserRoles.Builder builder =
+ new UserRoles.Builder()
+ .setGlobalRole(GlobalRole.SUPPORT_AGENT)
+ .setRegistrarRoles(ImmutableMap.of("TheRegistrar", RegistrarRole.PRIMARY_CONTACT));
+ assertThat(assertThrows(IllegalArgumentException.class, builder::build))
+ .hasMessageThat()
+ .isEqualTo("Users cannot have both global and per-registrar roles");
+ }
+}
diff --git a/core/src/test/java/google/registry/model/console/UserTest.java b/core/src/test/java/google/registry/model/console/UserTest.java
new file mode 100644
index 000000000..596a35482
--- /dev/null
+++ b/core/src/test/java/google/registry/model/console/UserTest.java
@@ -0,0 +1,61 @@
+// 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.model.console;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link User}. */
+public class UserTest {
+
+ @Test
+ void testFailure_badInputs() {
+ User.Builder builder = new User.Builder();
+ assertThat(assertThrows(IllegalArgumentException.class, () -> builder.setGaiaId(null)))
+ .hasMessageThat()
+ .isEqualTo("Gaia ID cannot be null or empty");
+ assertThat(assertThrows(IllegalArgumentException.class, () -> builder.setEmailAddress("")))
+ .hasMessageThat()
+ .isEqualTo("Provided email is not a valid email address");
+ assertThat(assertThrows(NullPointerException.class, () -> builder.setEmailAddress(null)))
+ .hasMessageThat()
+ .isEqualTo("Provided email was null");
+ assertThat(
+ assertThrows(
+ IllegalArgumentException.class, () -> builder.setEmailAddress("invalidEmail")))
+ .hasMessageThat()
+ .isEqualTo("Provided email invalidEmail is not a valid email address");
+ assertThat(assertThrows(IllegalArgumentException.class, () -> builder.setUserRoles(null)))
+ .hasMessageThat()
+ .isEqualTo("User roles cannot be null");
+
+ assertThat(assertThrows(IllegalArgumentException.class, builder::build))
+ .hasMessageThat()
+ .isEqualTo("Gaia ID cannot be null");
+ builder.setGaiaId("gaiaId");
+ assertThat(assertThrows(IllegalArgumentException.class, builder::build))
+ .hasMessageThat()
+ .isEqualTo("Email address cannot be null");
+ builder.setEmailAddress("email@email.com");
+ assertThat(assertThrows(IllegalArgumentException.class, builder::build))
+ .hasMessageThat()
+ .isEqualTo("User roles cannot be null");
+
+ builder.setUserRoles(new UserRoles.Builder().build());
+ builder.build();
+ }
+}