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 a9685c242..60f0ca8fc 100644 --- a/core/src/main/java/google/registry/model/console/User.java +++ b/core/src/main/java/google/registry/model/console/User.java @@ -24,6 +24,7 @@ import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; import google.registry.model.Buildable; import google.registry.model.UpdateAutoTimestampEntity; +import google.registry.persistence.VKey; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; @@ -47,7 +48,6 @@ public class User extends UpdateAutoTimestampEntity implements Buildable { private Long id; /** GAIA ID associated with the user in question. */ - @Column(nullable = false) private String gaiaId; /** Email address of the user in question. */ @@ -118,6 +118,11 @@ public class User extends UpdateAutoTimestampEntity implements Buildable { return new Builder(clone(this)); } + @Override + public VKey createVKey() { + return VKey.create(User.class, getId()); + } + /** Builder for constructing immutable {@link User} objects. */ public static class Builder extends Buildable.Builder { @@ -129,7 +134,6 @@ public class User extends UpdateAutoTimestampEntity implements Buildable { @Override 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(); diff --git a/core/src/main/java/google/registry/tools/CreateOrUpdateUserCommand.java b/core/src/main/java/google/registry/tools/CreateOrUpdateUserCommand.java new file mode 100644 index 000000000..42a4bfb2d --- /dev/null +++ b/core/src/main/java/google/registry/tools/CreateOrUpdateUserCommand.java @@ -0,0 +1,85 @@ +// Copyright 2023 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.tools; + +import static google.registry.persistence.transaction.TransactionManagerFactory.tm; +import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; + +import com.beust.jcommander.Parameter; +import com.google.common.collect.ImmutableMap; +import google.registry.model.console.GlobalRole; +import google.registry.model.console.RegistrarRole; +import google.registry.model.console.User; +import google.registry.model.console.UserDao; +import google.registry.model.console.UserRoles; +import google.registry.tools.params.KeyValueMapParameter.StringToRegistrarRoleMap; +import java.util.Optional; +import javax.annotation.Nullable; + +/** Shared base class for commands that create or modify a {@link User}. */ +public abstract class CreateOrUpdateUserCommand extends ConfirmingCommand { + + @Nullable + @Parameter(names = "--email", description = "Email address of the user", required = true) + String email; + + @Nullable + @Parameter( + names = "--admin", + description = "Whether or not the user in question is an admin", + arity = 1) + private Boolean isAdmin; + + @Nullable + @Parameter( + names = "--global_role", + description = "Global role, e.g. SUPPORT_LEAD, to apply to the user") + private GlobalRole globalRole; + + @Nullable + @Parameter( + names = "--registrar_roles", + converter = StringToRegistrarRoleMap.class, + validateWith = StringToRegistrarRoleMap.class, + description = + "Comma-delimited mapping of registrar name to role that the user has on that registrar") + private ImmutableMap registrarRolesMap; + + @Nullable + abstract User getExistingUser(String email); + + @Override + protected final String execute() throws Exception { + checkArgumentNotNull(email, "Email must be provided"); + tm().transact(this::executeInTransaction); + return String.format("Saved user with email %s", email); + } + + private void executeInTransaction() { + User user = getExistingUser(email); + UserRoles.Builder userRolesBuilder = + (user == null) ? new UserRoles.Builder() : user.getUserRoles().asBuilder(); + + Optional.ofNullable(globalRole).ifPresent(userRolesBuilder::setGlobalRole); + Optional.ofNullable(registrarRolesMap).ifPresent(userRolesBuilder::setRegistrarRoles); + Optional.ofNullable(isAdmin).ifPresent(userRolesBuilder::setIsAdmin); + + User.Builder builder = + (user == null) ? new User.Builder().setEmailAddress(email) : user.asBuilder(); + builder.setUserRoles(userRolesBuilder.build()); + User newUser = builder.build(); + UserDao.saveUser(newUser); + } +} diff --git a/core/src/main/java/google/registry/tools/CreateUserCommand.java b/core/src/main/java/google/registry/tools/CreateUserCommand.java new file mode 100644 index 000000000..056c906f6 --- /dev/null +++ b/core/src/main/java/google/registry/tools/CreateUserCommand.java @@ -0,0 +1,35 @@ +// Copyright 2023 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.tools; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.beust.jcommander.Parameters; +import google.registry.model.console.User; +import google.registry.model.console.UserDao; +import javax.annotation.Nullable; + +/** Command to create a new User. */ +@Parameters(separators = " =", commandDescription = "Update a user account") +public class CreateUserCommand extends CreateOrUpdateUserCommand { + + @Nullable + @Override + User getExistingUser(String email) { + checkArgument( + !UserDao.loadUser(email).isPresent(), "A user with email %s already exists", email); + return null; + } +} diff --git a/core/src/main/java/google/registry/tools/DeleteUserCommand.java b/core/src/main/java/google/registry/tools/DeleteUserCommand.java new file mode 100644 index 000000000..4b2054e0e --- /dev/null +++ b/core/src/main/java/google/registry/tools/DeleteUserCommand.java @@ -0,0 +1,53 @@ +// Copyright 2023 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.tools; + +import static google.registry.persistence.transaction.TransactionManagerFactory.tm; +import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; +import static google.registry.util.PreconditionsUtils.checkArgumentPresent; + +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import google.registry.model.console.User; +import google.registry.model.console.UserDao; +import java.util.Optional; +import javax.annotation.Nullable; + +/** Deletes a {@link User}. */ +@Parameters(separators = " =", commandDescription = "Delete a user account") +public class DeleteUserCommand extends ConfirmingCommand { + + @Nullable + @Parameter(names = "--email", description = "Email address of the user", required = true) + String email; + + @Override + protected String prompt() { + checkArgumentNotNull(email, "Email must be provided"); + checkArgumentPresent(UserDao.loadUser(email), "Email does not correspond to a valid user"); + return String.format("Delete user with email %s?", email); + } + + @Override + protected String execute() throws Exception { + tm().transact( + () -> { + Optional optionalUser = UserDao.loadUser(email); + checkArgumentPresent(optionalUser, "Email no longer corresponds to a valid user"); + tm().delete(optionalUser.get()); + }); + return String.format("Deleted user with email %s", email); + } +} diff --git a/core/src/main/java/google/registry/tools/RegistryTool.java b/core/src/main/java/google/registry/tools/RegistryTool.java index 69b83faf7..464671870 100644 --- a/core/src/main/java/google/registry/tools/RegistryTool.java +++ b/core/src/main/java/google/registry/tools/RegistryTool.java @@ -46,6 +46,7 @@ public final class RegistryTool { .put("create_registrar_groups", CreateRegistrarGroupsCommand.class) .put("create_reserved_list", CreateReservedListCommand.class) .put("create_tld", CreateTldCommand.class) + .put("create_user", CreateUserCommand.class) .put("curl", CurlCommand.class) .put("delete_allocation_tokens", DeleteAllocationTokensCommand.class) .put("delete_domain", DeleteDomainCommand.class) @@ -53,6 +54,7 @@ public final class RegistryTool { .put("delete_premium_list", DeletePremiumListCommand.class) .put("delete_reserved_list", DeleteReservedListCommand.class) .put("delete_tld", DeleteTldCommand.class) + .put("delete_user", DeleteUserCommand.class) .put("encrypt_escrow_deposit", EncryptEscrowDepositCommand.class) .put("enqueue_poll_message", EnqueuePollMessageCommand.class) .put("execute_epp", ExecuteEppCommand.class) @@ -110,6 +112,7 @@ public final class RegistryTool { .put("update_reserved_list", UpdateReservedListCommand.class) .put("update_server_locks", UpdateServerLocksCommand.class) .put("update_tld", UpdateTldCommand.class) + .put("update_user", UpdateUserCommand.class) .put("upload_claims_list", UploadClaimsListCommand.class) .put("validate_escrow_deposit", ValidateEscrowDepositCommand.class) .put("validate_login_credentials", ValidateLoginCredentialsCommand.class) diff --git a/core/src/main/java/google/registry/tools/UpdateUserCommand.java b/core/src/main/java/google/registry/tools/UpdateUserCommand.java new file mode 100644 index 000000000..6e836ac2f --- /dev/null +++ b/core/src/main/java/google/registry/tools/UpdateUserCommand.java @@ -0,0 +1,33 @@ +// Copyright 2023 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.tools; + +import static google.registry.util.PreconditionsUtils.checkArgumentPresent; + +import com.beust.jcommander.Parameters; +import google.registry.model.console.User; +import google.registry.model.console.UserDao; +import javax.annotation.Nullable; + +/** Updates a user, assuming that the user in question already exists. */ +@Parameters(separators = " =", commandDescription = "Update a user account") +public class UpdateUserCommand extends CreateOrUpdateUserCommand { + + @Nullable + @Override + User getExistingUser(String email) { + return checkArgumentPresent(UserDao.loadUser(email), "User %s not found", email); + } +} diff --git a/core/src/main/java/google/registry/tools/params/KeyValueMapParameter.java b/core/src/main/java/google/registry/tools/params/KeyValueMapParameter.java index bf489353b..28ffb5afe 100644 --- a/core/src/main/java/google/registry/tools/params/KeyValueMapParameter.java +++ b/core/src/main/java/google/registry/tools/params/KeyValueMapParameter.java @@ -17,6 +17,7 @@ package google.registry.tools.params; import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; +import google.registry.model.console.RegistrarRole; import java.util.Map; import org.joda.money.CurrencyUnit; @@ -107,4 +108,18 @@ public abstract class KeyValueMapParameter return value; } } + + /** Combined converter/validator class for maps of registrar names to registrar roles. */ + public static class StringToRegistrarRoleMap extends KeyValueMapParameter { + + @Override + protected String parseKey(String rawKey) { + return rawKey; + } + + @Override + protected RegistrarRole parseValue(String rawValue) { + return RegistrarRole.valueOf(rawValue); + } + } } 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 6d971b455..70f2ae92e 100644 --- a/core/src/test/java/google/registry/model/console/UserTest.java +++ b/core/src/test/java/google/registry/model/console/UserTest.java @@ -72,11 +72,7 @@ public class UserTest extends EntityTestCase { 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"); diff --git a/core/src/test/java/google/registry/tools/CreateUserCommandTest.java b/core/src/test/java/google/registry/tools/CreateUserCommandTest.java new file mode 100644 index 000000000..77f012bb7 --- /dev/null +++ b/core/src/test/java/google/registry/tools/CreateUserCommandTest.java @@ -0,0 +1,81 @@ +// Copyright 2023 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.tools; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import google.registry.model.console.GlobalRole; +import google.registry.model.console.RegistrarRole; +import google.registry.model.console.User; +import google.registry.model.console.UserDao; +import google.registry.testing.DatabaseHelper; +import org.junit.jupiter.api.Test; + +/** Tests for {@link CreateUserCommand}. */ +public class CreateUserCommandTest extends CommandTestCase { + + @Test + void testSuccess() throws Exception { + runCommandForced("--email", "user@example.test"); + User onlyUser = Iterables.getOnlyElement(DatabaseHelper.loadAllOf(User.class)); + assertThat(onlyUser.getEmailAddress()).isEqualTo("user@example.test"); + assertThat(onlyUser.getUserRoles().isAdmin()).isFalse(); + assertThat(onlyUser.getUserRoles().getGlobalRole()).isEqualTo(GlobalRole.NONE); + assertThat(onlyUser.getUserRoles().getRegistrarRoles()).isEmpty(); + } + + @Test + void testSuccess_admin() throws Exception { + runCommandForced("--email", "user@example.test", "--admin", "true"); + assertThat(UserDao.loadUser("user@example.test").get().getUserRoles().isAdmin()).isTrue(); + } + + @Test + void testSuccess_globalRole() throws Exception { + runCommandForced("--email", "user@example.test", "--global_role", "FTE"); + assertThat(UserDao.loadUser("user@example.test").get().getUserRoles().getGlobalRole()) + .isEqualTo(GlobalRole.FTE); + } + + @Test + void testSuccess_registrarRoles() throws Exception { + runCommandForced( + "--email", + "user@example.test", + "--registrar_roles", + "TheRegistrar=ACCOUNT_MANAGER,NewRegistrar=PRIMARY_CONTACT"); + assertThat(UserDao.loadUser("user@example.test").get().getUserRoles().getRegistrarRoles()) + .isEqualTo( + ImmutableMap.of( + "TheRegistrar", + RegistrarRole.ACCOUNT_MANAGER, + "NewRegistrar", + RegistrarRole.PRIMARY_CONTACT)); + } + + @Test + void testFailure_alreadyExists() throws Exception { + runCommandForced("--email", "user@example.test"); + assertThat( + assertThrows( + IllegalArgumentException.class, + () -> runCommandForced("--email", "user@example.test"))) + .hasMessageThat() + .isEqualTo("A user with email user@example.test already exists"); + } +} diff --git a/core/src/test/java/google/registry/tools/DeleteUserCommandTest.java b/core/src/test/java/google/registry/tools/DeleteUserCommandTest.java new file mode 100644 index 000000000..f22ac5508 --- /dev/null +++ b/core/src/test/java/google/registry/tools/DeleteUserCommandTest.java @@ -0,0 +1,53 @@ +// Copyright 2023 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.tools; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; +import static org.junit.Assert.assertThrows; + +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 org.junit.jupiter.api.Test; + +/** Tests for {@link DeleteUserCommand}. */ +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); + assertThat(UserDao.loadUser("email@example.test")).isPresent(); + runCommandForced("--email", "email@example.test"); + assertThat(UserDao.loadUser("email@example.test")).isEmpty(); + } + + @Test + void testFailure_nonexistent() { + assertThat( + assertThrows( + IllegalArgumentException.class, + () -> runCommandForced("--email", "nonexistent@example.test"))) + .hasMessageThat() + .isEqualTo("Email does not correspond to a valid user"); + } +} diff --git a/core/src/test/java/google/registry/tools/UpdateUserCommandTest.java b/core/src/test/java/google/registry/tools/UpdateUserCommandTest.java new file mode 100644 index 000000000..aa7bc07b4 --- /dev/null +++ b/core/src/test/java/google/registry/tools/UpdateUserCommandTest.java @@ -0,0 +1,89 @@ +// Copyright 2023 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.tools; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableMap; +import google.registry.model.console.GlobalRole; +import google.registry.model.console.RegistrarRole; +import google.registry.model.console.User; +import google.registry.model.console.UserDao; +import google.registry.model.console.UserRoles; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** Tests for {@link UpdateUserCommand}. */ +public class UpdateUserCommandTest extends CommandTestCase { + + @BeforeEach + void beforeEach() throws Exception { + UserDao.saveUser( + new User.Builder() + .setEmailAddress("user@example.test") + .setUserRoles(new UserRoles.Builder().build()) + .build()); + } + + @Test + void testSuccess_admin() throws Exception { + assertThat(UserDao.loadUser("user@example.test").get().getUserRoles().isAdmin()).isFalse(); + runCommandForced("--email", "user@example.test", "--admin", "true"); + assertThat(UserDao.loadUser("user@example.test").get().getUserRoles().isAdmin()).isTrue(); + runCommandForced("--email", "user@example.test", "--admin", "false"); + assertThat(UserDao.loadUser("user@example.test").get().getUserRoles().isAdmin()).isFalse(); + } + + @Test + void testSuccess_registrarRoles() throws Exception { + assertThat(UserDao.loadUser("user@example.test").get().getUserRoles().getRegistrarRoles()) + .isEmpty(); + runCommandForced( + "--email", + "user@example.test", + "--registrar_roles", + "TheRegistrar=ACCOUNT_MANAGER,NewRegistrar=PRIMARY_CONTACT"); + assertThat(UserDao.loadUser("user@example.test").get().getUserRoles().getRegistrarRoles()) + .isEqualTo( + ImmutableMap.of( + "TheRegistrar", + RegistrarRole.ACCOUNT_MANAGER, + "NewRegistrar", + RegistrarRole.PRIMARY_CONTACT)); + runCommandForced("--email", "user@example.test", "--registrar_roles", ""); + assertThat(UserDao.loadUser("user@example.test").get().getUserRoles().getRegistrarRoles()) + .isEmpty(); + } + + @Test + void testSuccess_globalRole() throws Exception { + assertThat(UserDao.loadUser("user@example.test").get().getUserRoles().getGlobalRole()) + .isEqualTo(GlobalRole.NONE); + runCommandForced("--email", "user@example.test", "--global_role", "FTE"); + assertThat(UserDao.loadUser("user@example.test").get().getUserRoles().getGlobalRole()) + .isEqualTo(GlobalRole.FTE); + } + + @Test + void testFailure_doesntExist() { + assertThat( + assertThrows( + IllegalArgumentException.class, + () -> runCommandForced("--email", "nonexistent@example.test"))) + .hasMessageThat() + .isEqualTo("User nonexistent@example.test not found"); + } +} 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 6569d64cb..4c687fe00 100644 --- a/db/src/main/resources/sql/schema/db-schema.sql.generated +++ b/db/src/main/resources/sql/schema/db-schema.sql.generated @@ -747,7 +747,7 @@ id bigserial not null, update_timestamp timestamptz, email_address text not null, - gaia_id text not null, + gaia_id text, registry_lock_password_hash text, registry_lock_password_salt text, global_role text not null,