diff --git a/config/presubmits.py b/config/presubmits.py index 516d9b5b6..87b7916b9 100644 --- a/config/presubmits.py +++ b/config/presubmits.py @@ -196,6 +196,12 @@ PRESUBMITS = { {"/node_modules/"}, ): "Use status code from jakarta.servlet.http.HttpServletResponse.", + PresubmitCheck( + r".*mock\(Response\.class\).*", + "java", + {"/node_modules/"}, + ): + "Do not mock Response, use FakeResponse.", } # Note that this regex only works for one kind of Flyway file. If we want to diff --git a/core/src/main/java/google/registry/config/RegistryConfig.java b/core/src/main/java/google/registry/config/RegistryConfig.java index 047e7683c..6af158b53 100644 --- a/core/src/main/java/google/registry/config/RegistryConfig.java +++ b/core/src/main/java/google/registry/config/RegistryConfig.java @@ -491,6 +491,18 @@ public final class RegistryConfig { return Optional.ofNullable(Strings.emptyToNull(config.gSuite.supportGroupEmailAddress)); } + /** + * Returns the email address of the group containing emails of console users. + * + *

This group should be granted the {@code roles/iap.httpsResourceAccessor} role. + */ + @Provides + @Config("gSuiteConsoleUserGroupEmailAddress") + public static Optional provideGSuiteConsoleUserGroupEmailAddress( + RegistryConfigSettings config) { + return Optional.ofNullable(Strings.emptyToNull(config.gSuite.consoleUserGroupEmailAddress)); + } + /** * Returns the email address(es) that notifications of registrar and/or registrar contact * updates should be sent to, or the empty list if updates should not be sent. diff --git a/core/src/main/java/google/registry/config/RegistryConfigSettings.java b/core/src/main/java/google/registry/config/RegistryConfigSettings.java index a63c020b8..2436f0efc 100644 --- a/core/src/main/java/google/registry/config/RegistryConfigSettings.java +++ b/core/src/main/java/google/registry/config/RegistryConfigSettings.java @@ -83,6 +83,7 @@ public class RegistryConfigSettings { public String outgoingEmailDisplayName; public String adminAccountEmailAddress; public String supportGroupEmailAddress; + public String consoleUserGroupEmailAddress; } /** Configuration options for registry policy. */ diff --git a/core/src/main/java/google/registry/config/files/default-config.yaml b/core/src/main/java/google/registry/config/files/default-config.yaml index a96a92f47..592274db0 100644 --- a/core/src/main/java/google/registry/config/files/default-config.yaml +++ b/core/src/main/java/google/registry/config/files/default-config.yaml @@ -47,6 +47,11 @@ gSuite: # given "ADMIN" role on the registrar console. supportGroupEmailAddress: support@example.com + # Group containing the emails of console users. This group should be granted + # roles/iap.httpsResourceAccessor out-of-band. If this field is empty, each + # console user will be granted to the role individually when they are created. + consoleUserGroupEmailAddress: + registryPolicy: # Repository identifier (ROID) suffix for contacts and hosts. contactAndHostRoidSuffix: ROID diff --git a/core/src/main/java/google/registry/env/common/tools/WEB-INF/web.xml b/core/src/main/java/google/registry/env/common/tools/WEB-INF/web.xml index 2257209b0..414d4936c 100644 --- a/core/src/main/java/google/registry/env/common/tools/WEB-INF/web.xml +++ b/core/src/main/java/google/registry/env/common/tools/WEB-INF/web.xml @@ -12,6 +12,11 @@ 1 + + tools-servlet + /_dr/admin/updateUserGroup + + tools-servlet /_dr/admin/verifyOte diff --git a/core/src/main/java/google/registry/module/RequestComponent.java b/core/src/main/java/google/registry/module/RequestComponent.java index 926d4f087..1461b3892 100644 --- a/core/src/main/java/google/registry/module/RequestComponent.java +++ b/core/src/main/java/google/registry/module/RequestComponent.java @@ -107,6 +107,7 @@ import google.registry.tools.server.ListReservedListsAction; import google.registry.tools.server.ListTldsAction; import google.registry.tools.server.RefreshDnsForAllDomainsAction; import google.registry.tools.server.ToolsServerModule; +import google.registry.tools.server.UpdateUserGroupAction; import google.registry.tools.server.VerifyOteAction; import google.registry.ui.server.console.ConsoleDomainGetAction; import google.registry.ui.server.console.ConsoleDomainListAction; @@ -323,6 +324,8 @@ interface RequestComponent { UpdateRegistrarRdapBaseUrlsAction updateRegistrarRdapBaseUrlsAction(); + UpdateUserGroupAction updateUserGroupAction(); + UploadBsaUnavailableDomainsAction uploadBsaUnavailableDomains(); VerifyOteAction verifyOteAction(); diff --git a/core/src/main/java/google/registry/module/tools/ToolsRequestComponent.java b/core/src/main/java/google/registry/module/tools/ToolsRequestComponent.java index 422b1e75c..6421c5528 100644 --- a/core/src/main/java/google/registry/module/tools/ToolsRequestComponent.java +++ b/core/src/main/java/google/registry/module/tools/ToolsRequestComponent.java @@ -36,6 +36,7 @@ import google.registry.tools.server.ListReservedListsAction; import google.registry.tools.server.ListTldsAction; import google.registry.tools.server.RefreshDnsForAllDomainsAction; import google.registry.tools.server.ToolsServerModule; +import google.registry.tools.server.UpdateUserGroupAction; import google.registry.tools.server.VerifyOteAction; /** Dagger component with per-request lifetime for "tools" App Engine module. */ @@ -50,9 +51,10 @@ import google.registry.tools.server.VerifyOteAction; WhiteboxModule.class, }) public interface ToolsRequestComponent { + FlowComponent.Builder flowComponentBuilder(); + CreateGroupsAction createGroupsAction(); EppToolAction eppToolAction(); - FlowComponent.Builder flowComponentBuilder(); GenerateZoneFilesAction generateZoneFilesAction(); ListDomainsAction listDomainsAction(); ListHostsAction listHostsAction(); @@ -62,6 +64,9 @@ public interface ToolsRequestComponent { ListTldsAction listTldsAction(); LoadTestAction loadTestAction(); RefreshDnsForAllDomainsAction refreshDnsForAllDomainsAction(); + + UpdateUserGroupAction updateUserGroupAction(); + VerifyOteAction verifyOteAction(); @Subcomponent.Builder diff --git a/core/src/main/java/google/registry/tools/CreateUserCommand.java b/core/src/main/java/google/registry/tools/CreateUserCommand.java index eb692e93b..b157c3ca5 100644 --- a/core/src/main/java/google/registry/tools/CreateUserCommand.java +++ b/core/src/main/java/google/registry/tools/CreateUserCommand.java @@ -17,19 +17,32 @@ package google.registry.tools; import static com.google.common.base.Preconditions.checkArgument; import com.beust.jcommander.Parameters; +import com.google.common.collect.ImmutableMap; +import com.google.common.flogger.FluentLogger; +import com.google.common.net.MediaType; +import google.registry.config.RegistryConfig.Config; import google.registry.model.console.User; import google.registry.model.console.UserDao; +import google.registry.tools.server.UpdateUserGroupAction; +import java.util.Optional; import javax.annotation.Nullable; import javax.inject.Inject; /** Command to create a new User. */ @Parameters(separators = " =", commandDescription = "Update a user account") -public class CreateUserCommand extends CreateOrUpdateUserCommand { +public class CreateUserCommand extends CreateOrUpdateUserCommand implements CommandWithConnection { static final String IAP_SECURED_WEB_APP_USER_ROLE = "roles/iap.httpsResourceAccessor"; + static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + private ServiceConnection connection; @Inject IamClient iamClient; + @Inject + @Config("gSuiteConsoleUserGroupEmailAddress") + Optional maybeGroupEmailAddress; + @Nullable @Override User getExistingUser(String email) { @@ -40,7 +53,29 @@ public class CreateUserCommand extends CreateOrUpdateUserCommand { @Override protected String execute() throws Exception { String ret = super.execute(); - iamClient.addBinding(email, IAP_SECURED_WEB_APP_USER_ROLE); + String groupEmailAddress = maybeGroupEmailAddress.orElse(null); + if (groupEmailAddress != null) { + logger.atInfo().log("Adding %s to group %s", email, groupEmailAddress); + connection.sendPostRequest( + UpdateUserGroupAction.PATH, + ImmutableMap.of( + "userEmailAddress", + email, + "groupEmailAddress", + groupEmailAddress, + "groupUpdateMode", + "ADD"), + MediaType.PLAIN_TEXT_UTF_8, + new byte[0]); + } else { + logger.atInfo().log("Granting IAP role to user %s", email); + iamClient.addBinding(email, IAP_SECURED_WEB_APP_USER_ROLE); + } return ret; } + + @Override + public void setConnection(ServiceConnection connection) { + this.connection = connection; + } } diff --git a/core/src/main/java/google/registry/tools/DeleteUserCommand.java b/core/src/main/java/google/registry/tools/DeleteUserCommand.java index 751d76827..c026a5d11 100644 --- a/core/src/main/java/google/registry/tools/DeleteUserCommand.java +++ b/core/src/main/java/google/registry/tools/DeleteUserCommand.java @@ -21,22 +21,39 @@ import static google.registry.util.PreconditionsUtils.checkArgumentPresent; import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameters; +import com.google.common.collect.ImmutableMap; +import com.google.common.flogger.FluentLogger; +import com.google.common.net.MediaType; +import google.registry.config.RegistryConfig.Config; import google.registry.model.console.User; import google.registry.model.console.UserDao; +import google.registry.tools.server.UpdateUserGroupAction; import java.util.Optional; import javax.annotation.Nullable; import javax.inject.Inject; /** Deletes a {@link User}. */ @Parameters(separators = " =", commandDescription = "Delete a user account") -public class DeleteUserCommand extends ConfirmingCommand { +public class DeleteUserCommand extends ConfirmingCommand implements CommandWithConnection { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + private ServiceConnection connection; @Inject IamClient iamClient; + @Inject + @Config("gSuiteConsoleUserGroupEmailAddress") + Optional maybeGroupEmailAddress; + @Nullable @Parameter(names = "--email", description = "Email address of the user", required = true) String email; + @Override + public void setConnection(ServiceConnection connection) { + this.connection = connection; + } + @Override protected String prompt() { checkArgumentNotNull(email, "Email must be provided"); @@ -52,7 +69,24 @@ public class DeleteUserCommand extends ConfirmingCommand { checkArgumentPresent(optionalUser, "Email no longer corresponds to a valid user"); tm().delete(optionalUser.get()); }); - iamClient.removeBinding(email, IAP_SECURED_WEB_APP_USER_ROLE); + String groupEmailAddress = maybeGroupEmailAddress.orElse(null); + if (groupEmailAddress != null) { + logger.atInfo().log("Removing %s from group %s", email, groupEmailAddress); + connection.sendPostRequest( + UpdateUserGroupAction.PATH, + ImmutableMap.of( + "userEmailAddress", + email, + "groupEmailAddress", + groupEmailAddress, + "groupUpdateMode", + "REMOVE"), + MediaType.PLAIN_TEXT_UTF_8, + new byte[0]); + } else { + logger.atInfo().log("Removing IAP role from user %s", email); + iamClient.removeBinding(email, IAP_SECURED_WEB_APP_USER_ROLE); + } return String.format("Deleted user with email %s", email); } } diff --git a/core/src/main/java/google/registry/tools/server/ToolsServerModule.java b/core/src/main/java/google/registry/tools/server/ToolsServerModule.java index 95e031761..c7979b848 100644 --- a/core/src/main/java/google/registry/tools/server/ToolsServerModule.java +++ b/core/src/main/java/google/registry/tools/server/ToolsServerModule.java @@ -23,12 +23,11 @@ import static google.registry.request.RequestParameters.extractRequiredParameter import dagger.Module; import dagger.Provides; import google.registry.request.Parameter; +import google.registry.tools.server.UpdateUserGroupAction.Mode; import jakarta.servlet.http.HttpServletRequest; import java.util.Optional; -/** - * Dagger module for the tools package. - */ +/** Dagger module for the tools package. */ @Module public class ToolsServerModule { @@ -75,4 +74,21 @@ public class ToolsServerModule { static Optional provideRefreshQps(HttpServletRequest req) { return extractOptionalIntParameter(req, "refreshQps"); } + + @Provides + static Mode provideGroupUpdateMode(HttpServletRequest req) { + return Mode.valueOf(extractRequiredParameter(req, "groupUpdateMode")); + } + + @Provides + @Parameter("userEmailAddress") + static String provideUserEmailAddress(HttpServletRequest req) { + return extractRequiredParameter(req, "userEmailAddress"); + } + + @Provides + @Parameter("groupEmailAddress") + static String provideGroupEmailAddress(HttpServletRequest req) { + return extractRequiredParameter(req, "groupEmailAddress"); + } } diff --git a/core/src/main/java/google/registry/tools/server/UpdateUserGroupAction.java b/core/src/main/java/google/registry/tools/server/UpdateUserGroupAction.java new file mode 100644 index 000000000..834ddc49c --- /dev/null +++ b/core/src/main/java/google/registry/tools/server/UpdateUserGroupAction.java @@ -0,0 +1,83 @@ +// 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.tools.server; + +import static google.registry.request.Action.Method.POST; + +import com.google.common.flogger.FluentLogger; +import google.registry.groups.GroupsConnection; +import google.registry.groups.GroupsConnection.Role; +import google.registry.request.Action; +import google.registry.request.Parameter; +import google.registry.request.Response; +import google.registry.request.auth.Auth; +import javax.inject.Inject; + +/** Action that adds or deletes a console user to/from the group that has IAP permissions. */ +@Action( + service = Action.Service.TOOLS, + path = UpdateUserGroupAction.PATH, + method = POST, + auth = Auth.AUTH_API_ADMIN) +public class UpdateUserGroupAction implements Runnable { + + public static final String PATH = "/_dr/admin/updateUserGroup"; + + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + @Inject GroupsConnection groupsConnection; + @Inject Response response; + + @Inject + @Parameter("userEmailAddress") + String userEmailAddress; + + @Inject + @Parameter("groupEmailAddress") + String groupEmailAddress; + + @Inject Mode mode; + + @Inject + UpdateUserGroupAction() {} + + enum Mode { + ADD, + REMOVE + } + + @Override + public void run() { + logger.atInfo().log( + "Updating group %s: %s user %s", + groupEmailAddress, mode == Mode.ADD ? "adding" : "removing", userEmailAddress); + try { + if (mode == Mode.ADD) { + // The group will be created if it does not exist. + groupsConnection.addMemberToGroup(groupEmailAddress, userEmailAddress, Role.MEMBER); + } else { + if (groupsConnection.isMemberOfGroup(userEmailAddress, groupEmailAddress)) { + groupsConnection.removeMemberFromGroup(groupEmailAddress, userEmailAddress); + } else { + logger.atInfo().log( + "Ignoring request to remove non-member %s from group %s", + userEmailAddress, groupEmailAddress); + } + } + } catch (Exception e) { + throw new RuntimeException("Cannot update group", e); + } + } +} diff --git a/core/src/test/java/google/registry/export/ExportPremiumTermsActionTest.java b/core/src/test/java/google/registry/export/ExportPremiumTermsActionTest.java index 7219a58b9..5a4754414 100644 --- a/core/src/test/java/google/registry/export/ExportPremiumTermsActionTest.java +++ b/core/src/test/java/google/registry/export/ExportPremiumTermsActionTest.java @@ -15,6 +15,7 @@ package google.registry.export; import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8; +import static com.google.common.truth.Truth.assertThat; import static google.registry.export.ExportPremiumTermsAction.EXPORT_MIME_TYPE; import static google.registry.export.ExportPremiumTermsAction.PREMIUM_TERMS_FILENAME; import static google.registry.testing.DatabaseHelper.createTld; @@ -41,13 +42,12 @@ import google.registry.model.tld.label.PremiumList; import google.registry.model.tld.label.PremiumListDao; import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension; -import google.registry.request.Response; import google.registry.storage.drive.DriveConnection; +import google.registry.testing.FakeResponse; import java.io.IOException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import org.mockito.ArgumentMatchers; /** Unit tests for {@link ExportPremiumTermsAction}. */ public class ExportPremiumTermsActionTest { @@ -63,7 +63,7 @@ public class ExportPremiumTermsActionTest { new JpaTestExtensions.Builder().buildIntegrationTestExtension(); private final DriveConnection driveConnection = mock(DriveConnection.class); - private final Response response = mock(Response.class); + private final FakeResponse response = new FakeResponse(); private void runAction(String tld) { ExportPremiumTermsAction action = new ExportPremiumTermsAction(); @@ -100,10 +100,9 @@ public class ExportPremiumTermsActionTest { EXPECTED_FILE_CONTENT.getBytes(UTF_8)); verifyNoMoreInteractions(driveConnection); - verify(response).setStatus(SC_OK); - verify(response).setPayload("file_id"); - verify(response).setContentType(PLAIN_TEXT_UTF_8); - verifyNoMoreInteractions(response); + assertThat(response.getStatus()).isEqualTo(SC_OK); + assertThat(response.getPayload()).isEqualTo("file_id"); + assertThat(response.getContentType()).isEqualTo(PLAIN_TEXT_UTF_8); } @Test @@ -112,10 +111,9 @@ public class ExportPremiumTermsActionTest { runAction("tld"); verifyNoInteractions(driveConnection); - verify(response).setStatus(SC_OK); - verify(response).setPayload("No premium lists configured"); - verify(response).setContentType(PLAIN_TEXT_UTF_8); - verifyNoMoreInteractions(response); + assertThat(response.getStatus()).isEqualTo(SC_OK); + assertThat(response.getPayload()).isEqualTo("No premium lists configured"); + assertThat(response.getContentType()).isEqualTo(PLAIN_TEXT_UTF_8); } @Test @@ -124,11 +122,10 @@ public class ExportPremiumTermsActionTest { runAction("tld"); verifyNoInteractions(driveConnection); - verify(response).setStatus(SC_OK); - verify(response) - .setPayload("Skipping export because no Drive folder is associated with this TLD"); - verify(response).setContentType(PLAIN_TEXT_UTF_8); - verifyNoMoreInteractions(response); + assertThat(response.getStatus()).isEqualTo(SC_OK); + assertThat(response.getPayload()) + .isEqualTo("Skipping export because no Drive folder is associated with this TLD"); + assertThat(response.getContentType()).isEqualTo(PLAIN_TEXT_UTF_8); } @Test @@ -137,10 +134,9 @@ public class ExportPremiumTermsActionTest { assertThrows(RuntimeException.class, () -> runAction("tld")); verifyNoInteractions(driveConnection); - verify(response).setStatus(SC_INTERNAL_SERVER_ERROR); - verify(response).setPayload(anyString()); - verify(response).setContentType(PLAIN_TEXT_UTF_8); - verifyNoMoreInteractions(response); + assertThat(response.getStatus()).isEqualTo(SC_INTERNAL_SERVER_ERROR); + assertThat(response.getPayload()).isNotEmpty(); + assertThat(response.getContentType()).isEqualTo(PLAIN_TEXT_UTF_8); } @Test @@ -149,10 +145,9 @@ public class ExportPremiumTermsActionTest { assertThrows(RuntimeException.class, () -> runAction("tld")); verifyNoInteractions(driveConnection); - verify(response).setStatus(SC_INTERNAL_SERVER_ERROR); - verify(response).setPayload("Could not load premium list for " + "tld"); - verify(response).setContentType(PLAIN_TEXT_UTF_8); - verifyNoMoreInteractions(response); + assertThat(response.getStatus()).isEqualTo(SC_INTERNAL_SERVER_ERROR); + assertThat(response.getPayload()).isEqualTo("Could not load premium list for " + "tld"); + assertThat(response.getContentType()).isEqualTo(PLAIN_TEXT_UTF_8); } @Test @@ -167,10 +162,8 @@ public class ExportPremiumTermsActionTest { "bad_folder_id", EXPECTED_FILE_CONTENT.getBytes(UTF_8)); verifyNoMoreInteractions(driveConnection); - verify(response).setStatus(SC_INTERNAL_SERVER_ERROR); - verify(response).setPayload( - ArgumentMatchers.contains("Error exporting premium terms file to Drive.")); - verify(response).setContentType(PLAIN_TEXT_UTF_8); - verifyNoMoreInteractions(response); + assertThat(response.getStatus()).isEqualTo(SC_INTERNAL_SERVER_ERROR); + assertThat(response.getPayload()).contains("Error exporting premium terms file to Drive."); + assertThat(response.getContentType()).isEqualTo(PLAIN_TEXT_UTF_8); } } diff --git a/core/src/test/java/google/registry/export/ExportReservedTermsActionTest.java b/core/src/test/java/google/registry/export/ExportReservedTermsActionTest.java index c95771422..21b9d32cd 100644 --- a/core/src/test/java/google/registry/export/ExportReservedTermsActionTest.java +++ b/core/src/test/java/google/registry/export/ExportReservedTermsActionTest.java @@ -36,8 +36,8 @@ import google.registry.model.tld.Tld; import google.registry.model.tld.label.ReservedList; import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension; -import google.registry.request.Response; import google.registry.storage.drive.DriveConnection; +import google.registry.testing.FakeResponse; import java.io.IOException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -50,7 +50,7 @@ public class ExportReservedTermsActionTest { JpaIntegrationTestExtension jpa = new JpaTestExtensions.Builder().buildIntegrationTestExtension(); private final DriveConnection driveConnection = mock(DriveConnection.class); - private final Response response = mock(Response.class); + private final FakeResponse response = new FakeResponse(); private void runAction(String tld) { ExportReservedTermsAction action = new ExportReservedTermsAction(); @@ -63,18 +63,13 @@ public class ExportReservedTermsActionTest { @BeforeEach void beforeEach() throws Exception { - ReservedList rl = persistReservedList( - "tld-reserved", - "lol,FULLY_BLOCKED", - "cat,FULLY_BLOCKED"); + ReservedList rl = persistReservedList("tld-reserved", "lol,FULLY_BLOCKED", "cat,FULLY_BLOCKED"); createTld("tld"); persistResource( Tld.get("tld").asBuilder().setReservedLists(rl).setDriveFolderId("brouhaha").build()); when(driveConnection.createOrUpdateFile( - anyString(), - any(MediaType.class), - anyString(), - any(byte[].class))).thenReturn("1001"); + anyString(), any(MediaType.class), anyString(), any(byte[].class))) + .thenReturn("1001"); } @Test @@ -83,8 +78,8 @@ public class ExportReservedTermsActionTest { byte[] expected = "# This is a disclaimer.\ncat\nlol\n".getBytes(UTF_8); verify(driveConnection) .createOrUpdateFile(RESERVED_TERMS_FILENAME, EXPORT_MIME_TYPE, "brouhaha", expected); - verify(response).setStatus(SC_OK); - verify(response).setPayload("1001"); + assertThat(response.getStatus()).isEqualTo(SC_OK); + assertThat(response.getPayload()).isEqualTo("1001"); } @Test @@ -96,35 +91,33 @@ public class ExportReservedTermsActionTest { .setDriveFolderId(null) .build()); runAction("tld"); - verify(response).setStatus(SC_OK); - verify(response).setPayload("No reserved lists configured"); + assertThat(response.getStatus()).isEqualTo(SC_OK); + assertThat(response.getPayload()).isEqualTo("No reserved lists configured"); } @Test void test_uploadFileToDrive_doesNothingWhenDriveFolderIdIsNull() { persistResource(Tld.get("tld").asBuilder().setDriveFolderId(null).build()); runAction("tld"); - verify(response).setStatus(SC_OK); - verify(response) - .setPayload("Skipping export because no Drive folder is associated with this TLD"); + assertThat(response.getStatus()).isEqualTo(SC_OK); + assertThat(response.getPayload()) + .isEqualTo("Skipping export because no Drive folder is associated with this TLD"); } @Test void test_uploadFileToDrive_failsWhenDriveCannotBeReached() throws Exception { when(driveConnection.createOrUpdateFile( - anyString(), - any(MediaType.class), - anyString(), - any(byte[].class))).thenThrow(new IOException("errorMessage")); + anyString(), any(MediaType.class), anyString(), any(byte[].class))) + .thenThrow(new IOException("errorMessage")); RuntimeException thrown = assertThrows(RuntimeException.class, () -> runAction("tld")); - verify(response).setStatus(SC_INTERNAL_SERVER_ERROR); + assertThat(response.getStatus()).isEqualTo(SC_INTERNAL_SERVER_ERROR); assertThat(thrown).hasCauseThat().hasMessageThat().isEqualTo("errorMessage"); } @Test void test_uploadFileToDrive_failsWhenTldDoesntExist() { RuntimeException thrown = assertThrows(RuntimeException.class, () -> runAction("fakeTld")); - verify(response).setStatus(SC_INTERNAL_SERVER_ERROR); + assertThat(response.getStatus()).isEqualTo(SC_INTERNAL_SERVER_ERROR); assertThat(thrown) .hasCauseThat() .hasMessageThat() diff --git a/core/src/test/java/google/registry/export/SyncGroupMembersActionTest.java b/core/src/test/java/google/registry/export/SyncGroupMembersActionTest.java index d1f25d645..c262417ec 100644 --- a/core/src/test/java/google/registry/export/SyncGroupMembersActionTest.java +++ b/core/src/test/java/google/registry/export/SyncGroupMembersActionTest.java @@ -40,8 +40,8 @@ import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarPoc; import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension; -import google.registry.request.Response; import google.registry.testing.FakeClock; +import google.registry.testing.FakeResponse; import google.registry.testing.FakeSleeper; import google.registry.util.Retrier; import java.io.IOException; @@ -61,7 +61,7 @@ public class SyncGroupMembersActionTest { new JpaTestExtensions.Builder().buildIntegrationTestExtension(); private final DirectoryGroupsConnection connection = mock(DirectoryGroupsConnection.class); - private final Response response = mock(Response.class); + private final FakeResponse response = new FakeResponse(); private void runAction() { SyncGroupMembersAction action = new SyncGroupMembersAction(); @@ -95,9 +95,11 @@ public class SyncGroupMembersActionTest { persistResource( loadRegistrar("TheRegistrar").asBuilder().setContactsRequireSyncing(false).build()); runAction(); - verify(response).setStatus(SC_OK); - verify(response).setPayload("NOT_MODIFIED No registrar contacts have been updated " - + "since the last time servlet ran.\n"); + assertThat(response.getStatus()).isEqualTo(SC_OK); + assertThat(response.getPayload()) + .isEqualTo( + "NOT_MODIFIED No registrar contacts have been updated " + + "since the last time servlet ran.\n"); assertThat(loadRegistrar("NewRegistrar").getContactsRequireSyncing()).isFalse(); } @@ -108,8 +110,8 @@ public class SyncGroupMembersActionTest { "newregistrar-primary-contacts@domain-registry.example", "janedoe@theregistrar.com", Role.MEMBER); - verify(response).setStatus(SC_OK); - verify(response).setPayload("OK Group memberships successfully updated.\n"); + assertThat(response.getStatus()).isEqualTo(SC_OK); + assertThat(response.getPayload()).isEqualTo("OK Group memberships successfully updated.\n"); assertThat(loadRegistrar("NewRegistrar").getContactsRequireSyncing()).isFalse(); } @@ -120,7 +122,7 @@ public class SyncGroupMembersActionTest { runAction(); verify(connection).removeMemberFromGroup( "newregistrar-primary-contacts@domain-registry.example", "defunct@example.com"); - verify(response).setStatus(SC_OK); + assertThat(response.getStatus()).isEqualTo(SC_OK); assertThat(loadRegistrar("NewRegistrar").getContactsRequireSyncing()).isFalse(); } @@ -134,7 +136,7 @@ public class SyncGroupMembersActionTest { "newregistrar-primary-contacts@domain-registry.example", "defunct@example.com"); verify(connection).removeMemberFromGroup( "newregistrar-primary-contacts@domain-registry.example", "janedoe@theregistrar.com"); - verify(response).setStatus(SC_OK); + assertThat(response.getStatus()).isEqualTo(SC_OK); assertThat(loadRegistrar("NewRegistrar").getContactsRequireSyncing()).isFalse(); } @@ -181,7 +183,7 @@ public class SyncGroupMembersActionTest { "theregistrar-technical-contacts@domain-registry.example", "hexadecimal@snow.fall", Role.MEMBER); - verify(response).setStatus(SC_OK); + assertThat(response.getStatus()).isEqualTo(SC_OK); assertThat(Iterables.filter(Registrar.loadAll(), Registrar::getContactsRequireSyncing)) .isEmpty(); } @@ -191,7 +193,7 @@ public class SyncGroupMembersActionTest { when(connection.getMembersOfGroup("newregistrar-primary-contacts@domain-registry.example")) .thenReturn(ImmutableSet.of()); when(connection.getMembersOfGroup("theregistrar-primary-contacts@domain-registry.example")) - .thenThrow(new IOException("Internet was deleted")); + .thenThrow(new IOException("Internet was neleted")); runAction(); verify(connection).addMemberToGroup( "newregistrar-primary-contacts@domain-registry.example", @@ -199,8 +201,9 @@ public class SyncGroupMembersActionTest { Role.MEMBER); verify(connection, times(3)) .getMembersOfGroup("theregistrar-primary-contacts@domain-registry.example"); - verify(response).setStatus(SC_INTERNAL_SERVER_ERROR); - verify(response).setPayload("FAILED Error occurred while updating registrar contacts.\n"); + assertThat(response.getStatus()).isEqualTo(SC_INTERNAL_SERVER_ERROR); + assertThat(response.getPayload()) + .isEqualTo("FAILED Error occurred while updating registrar contacts.\n"); assertThat(loadRegistrar("NewRegistrar").getContactsRequireSyncing()).isFalse(); assertThat(loadRegistrar("TheRegistrar").getContactsRequireSyncing()).isTrue(); } @@ -216,8 +219,8 @@ public class SyncGroupMembersActionTest { "newregistrar-primary-contacts@domain-registry.example", "janedoe@theregistrar.com", Role.MEMBER); - verify(response).setStatus(SC_OK); - verify(response).setPayload("OK Group memberships successfully updated.\n"); + assertThat(response.getStatus()).isEqualTo(SC_OK); + assertThat(response.getPayload()).isEqualTo("OK Group memberships successfully updated.\n"); assertThat(loadRegistrar("NewRegistrar").getContactsRequireSyncing()).isFalse(); } } diff --git a/core/src/test/java/google/registry/tools/CreateUserCommandTest.java b/core/src/test/java/google/registry/tools/CreateUserCommandTest.java index da1f1ff07..09280f9a5 100644 --- a/core/src/test/java/google/registry/tools/CreateUserCommandTest.java +++ b/core/src/test/java/google/registry/tools/CreateUserCommandTest.java @@ -19,15 +19,18 @@ import static google.registry.tools.CreateUserCommand.IAP_SECURED_WEB_APP_USER_R import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; +import com.google.common.net.MediaType; 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 java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -35,10 +38,13 @@ import org.junit.jupiter.api.Test; public class CreateUserCommandTest extends CommandTestCase { private final IamClient iamClient = mock(IamClient.class); + private final ServiceConnection connection = mock(ServiceConnection.class); @BeforeEach void beforeEach() { command.iamClient = iamClient; + command.maybeGroupEmailAddress = Optional.empty(); + command.setConnection(connection); } @Test @@ -51,6 +57,32 @@ public class CreateUserCommandTest extends CommandTestCase { assertThat(onlyUser.getUserRoles().getRegistrarRoles()).isEmpty(); verify(iamClient).addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE); verifyNoMoreInteractions(iamClient); + verifyNoInteractions(connection); + } + + @Test + void testSuccess_addToGroup() throws Exception { + command.maybeGroupEmailAddress = Optional.of("group@example.test"); + 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(); + verify(connection) + .sendPostRequest( + "/_dr/admin/updateUserGroup", + ImmutableMap.of( + "userEmailAddress", + "user@example.test", + "groupEmailAddress", + "group@example.test", + "groupUpdateMode", + "ADD"), + MediaType.PLAIN_TEXT_UTF_8, + new byte[0]); + verifyNoInteractions(iamClient); + verifyNoMoreInteractions(connection); } @Test @@ -70,6 +102,7 @@ public class CreateUserCommandTest extends CommandTestCase { assertThat(UserDao.loadUser("user@example.test").get().getUserRoles().isAdmin()).isTrue(); verify(iamClient).addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE); verifyNoMoreInteractions(iamClient); + verifyNoInteractions(connection); } @Test @@ -79,6 +112,7 @@ public class CreateUserCommandTest extends CommandTestCase { .isEqualTo(GlobalRole.FTE); verify(iamClient).addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE); verifyNoMoreInteractions(iamClient); + verifyNoInteractions(connection); } @Test @@ -97,6 +131,7 @@ public class CreateUserCommandTest extends CommandTestCase { RegistrarRole.PRIMARY_CONTACT)); verify(iamClient).addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE); verifyNoMoreInteractions(iamClient); + verifyNoInteractions(connection); } @Test @@ -111,6 +146,7 @@ public class CreateUserCommandTest extends CommandTestCase { .hasMessageThat() .isEqualTo("A user with email user@example.test already exists"); verifyNoMoreInteractions(iamClient); + verifyNoInteractions(connection); } @Test diff --git a/core/src/test/java/google/registry/tools/DeleteUserCommandTest.java b/core/src/test/java/google/registry/tools/DeleteUserCommandTest.java index 30b3addf9..509a08061 100644 --- a/core/src/test/java/google/registry/tools/DeleteUserCommandTest.java +++ b/core/src/test/java/google/registry/tools/DeleteUserCommandTest.java @@ -22,8 +22,11 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; +import com.google.common.collect.ImmutableMap; +import com.google.common.net.MediaType; import google.registry.model.console.UserDao; import google.registry.testing.DatabaseHelper; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -31,10 +34,13 @@ import org.junit.jupiter.api.Test; public class DeleteUserCommandTest extends CommandTestCase { private final IamClient iamClient = mock(IamClient.class); + private final ServiceConnection connection = mock(ServiceConnection.class); @BeforeEach void beforeEach() { command.iamClient = iamClient; + command.setConnection(connection); + command.maybeGroupEmailAddress = Optional.empty(); } @Test @@ -45,6 +51,30 @@ public class DeleteUserCommandTest extends CommandTestCase { assertThat(UserDao.loadUser("email@example.test")).isEmpty(); verify(iamClient).removeBinding("email@example.test", IAP_SECURED_WEB_APP_USER_ROLE); verifyNoMoreInteractions(iamClient); + verifyNoInteractions(connection); + } + + @Test + void testSuccess_deletesUser_removeFromGroup() throws Exception { + command.maybeGroupEmailAddress = Optional.of("group@example.test"); + DatabaseHelper.createAdminUser("email@example.test"); + assertThat(UserDao.loadUser("email@example.test")).isPresent(); + runCommandForced("--email", "email@example.test"); + assertThat(UserDao.loadUser("email@example.test")).isEmpty(); + verify(connection) + .sendPostRequest( + "/_dr/admin/updateUserGroup", + ImmutableMap.of( + "userEmailAddress", + "email@example.test", + "groupEmailAddress", + "group@example.test", + "groupUpdateMode", + "REMOVE"), + MediaType.PLAIN_TEXT_UTF_8, + new byte[0]); + verifyNoInteractions(iamClient); + verifyNoMoreInteractions(connection); } @Test @@ -56,5 +86,6 @@ public class DeleteUserCommandTest extends CommandTestCase { .hasMessageThat() .isEqualTo("Email does not correspond to a valid user"); verifyNoInteractions(iamClient); + verifyNoInteractions(connection); } } diff --git a/core/src/test/java/google/registry/tools/server/CreateGroupsActionTest.java b/core/src/test/java/google/registry/tools/server/CreateGroupsActionTest.java index 85f5b4741..30e2473b3 100644 --- a/core/src/test/java/google/registry/tools/server/CreateGroupsActionTest.java +++ b/core/src/test/java/google/registry/tools/server/CreateGroupsActionTest.java @@ -28,7 +28,7 @@ import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension; import google.registry.request.HttpException.BadRequestException; import google.registry.request.HttpException.InternalServerErrorException; -import google.registry.request.Response; +import google.registry.testing.FakeResponse; import java.util.Optional; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -41,7 +41,7 @@ class CreateGroupsActionTest { new JpaTestExtensions.Builder().buildIntegrationTestExtension(); private final DirectoryGroupsConnection connection = mock(DirectoryGroupsConnection.class); - private final Response response = mock(Response.class); + private final FakeResponse response = new FakeResponse(); private void runAction(String registrarId) { CreateGroupsAction action = new CreateGroupsAction(); @@ -74,8 +74,8 @@ class CreateGroupsActionTest { @Test void test_createsAllGroupsSuccessfully() throws Exception { runAction("NewRegistrar"); - verify(response).setStatus(SC_OK); - verify(response).setPayload("Success!"); + assertThat(response.getStatus()).isEqualTo(SC_OK); + assertThat(response.getPayload()).isEqualTo("Success!"); verifyGroupCreationCallsForNewRegistrar(); verify(connection).addMemberToGroup("registrar-primary-contacts@domain-registry.example", "newregistrar-primary-contacts@domain-registry.example", diff --git a/core/src/test/java/google/registry/tools/server/UpdateUserGroupActionTest.java b/core/src/test/java/google/registry/tools/server/UpdateUserGroupActionTest.java new file mode 100644 index 000000000..aa8e15b6d --- /dev/null +++ b/core/src/test/java/google/registry/tools/server/UpdateUserGroupActionTest.java @@ -0,0 +1,77 @@ +// 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.tools.server; + +import static com.google.common.truth.Truth.assertThat; +import static jakarta.servlet.http.HttpServletResponse.SC_OK; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import google.registry.groups.DirectoryGroupsConnection; +import google.registry.groups.GroupsConnection.Role; +import google.registry.testing.FakeResponse; +import google.registry.tools.server.UpdateUserGroupAction.Mode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link google.registry.tools.server.UpdateUserGroupAction}. */ +class UpdateUserGroupActionTest { + + private final DirectoryGroupsConnection connection = mock(DirectoryGroupsConnection.class); + private final FakeResponse response = new FakeResponse(); + private final UpdateUserGroupAction action = new UpdateUserGroupAction(); + private final String userEmailAddress = "user@example.com"; + private final String groupEmailAddress = "group@example.com"; + + @BeforeEach + void beforeEach() { + action.groupsConnection = connection; + action.response = response; + action.userEmailAddress = userEmailAddress; + action.groupEmailAddress = groupEmailAddress; + action.mode = Mode.ADD; + } + + @Test + void testSuccess_addMember() throws Exception { + action.run(); + assertThat(response.getStatus()).isEqualTo(SC_OK); + verify(connection).addMemberToGroup(groupEmailAddress, userEmailAddress, Role.MEMBER); + verifyNoMoreInteractions(connection); + } + + @Test + void testSuccess_removeMember() throws Exception { + action.mode = Mode.REMOVE; + when(connection.isMemberOfGroup(userEmailAddress, groupEmailAddress)).thenReturn(true); + action.run(); + assertThat(response.getStatus()).isEqualTo(SC_OK); + verify(connection).isMemberOfGroup(userEmailAddress, groupEmailAddress); + verify(connection).removeMemberFromGroup(groupEmailAddress, userEmailAddress); + verifyNoMoreInteractions(connection); + } + + @Test + void testSuccess_removeMember_notAMember() throws Exception { + action.mode = Mode.REMOVE; + when(connection.isMemberOfGroup(userEmailAddress, groupEmailAddress)).thenReturn(false); + action.run(); + assertThat(response.getStatus()).isEqualTo(SC_OK); + verify(connection).isMemberOfGroup(userEmailAddress, groupEmailAddress); + verifyNoMoreInteractions(connection); + } +} diff --git a/core/src/test/resources/google/registry/module/routing.txt b/core/src/test/resources/google/registry/module/routing.txt index fc138951b..f75109abd 100644 --- a/core/src/test/resources/google/registry/module/routing.txt +++ b/core/src/test/resources/google/registry/module/routing.txt @@ -6,6 +6,7 @@ PATH CLASS /_dr/admin/list/registrars ListRegistrarsAction GET,POST n API APP ADMIN /_dr/admin/list/reservedLists ListReservedListsAction GET,POST n API APP ADMIN /_dr/admin/list/tlds ListTldsAction GET,POST n API APP ADMIN +/_dr/admin/updateUserGroup UpdateUserGroupAction POST n API APP ADMIN /_dr/admin/verifyOte VerifyOteAction POST n API APP ADMIN /_dr/cron/fanout TldFanoutAction GET y API APP ADMIN /_dr/dnsRefresh RefreshDnsAction GET y API APP ADMIN diff --git a/core/src/test/resources/google/registry/module/tools/tools_routing.txt b/core/src/test/resources/google/registry/module/tools/tools_routing.txt index 161524c60..5933ca190 100644 --- a/core/src/test/resources/google/registry/module/tools/tools_routing.txt +++ b/core/src/test/resources/google/registry/module/tools/tools_routing.txt @@ -6,6 +6,7 @@ PATH CLASS METHODS OK AUTH /_dr/admin/list/registrars ListRegistrarsAction GET,POST n API APP ADMIN /_dr/admin/list/reservedLists ListReservedListsAction GET,POST n API APP ADMIN /_dr/admin/list/tlds ListTldsAction GET,POST n API APP ADMIN +/_dr/admin/updateUserGroup UpdateUserGroupAction POST n API APP ADMIN /_dr/admin/verifyOte VerifyOteAction POST n API APP ADMIN /_dr/epptool EppToolAction POST n API APP ADMIN /_dr/loadtest LoadTestAction POST y API APP ADMIN