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