1
0
mirror of https://github.com/google/nomulus synced 2026-02-06 21:11:34 +00:00

Add the ability to add/remove console users from a Google Group (#2450)

# Conflicts:
#	config/presubmits.py
This commit is contained in:
Lai Jiang
2024-05-28 13:00:37 -04:00
committed by GitHub
parent ec6c77927f
commit 3b565b96b7
20 changed files with 419 additions and 79 deletions

View File

@@ -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

View File

@@ -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.
*
* <p>This group should be granted the {@code roles/iap.httpsResourceAccessor} role.
*/
@Provides
@Config("gSuiteConsoleUserGroupEmailAddress")
public static Optional<String> 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.

View File

@@ -83,6 +83,7 @@ public class RegistryConfigSettings {
public String outgoingEmailDisplayName;
public String adminAccountEmailAddress;
public String supportGroupEmailAddress;
public String consoleUserGroupEmailAddress;
}
/** Configuration options for registry policy. */

View File

@@ -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

View File

@@ -12,6 +12,11 @@
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>tools-servlet</servlet-name>
<url-pattern>/_dr/admin/updateUserGroup</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>tools-servlet</servlet-name>
<url-pattern>/_dr/admin/verifyOte</url-pattern>

View File

@@ -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();

View File

@@ -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

View File

@@ -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<String> 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;
}
}

View File

@@ -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<String> 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);
}
}

View File

@@ -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<Integer> 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");
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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()

View File

@@ -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();
}
}

View File

@@ -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<CreateUserCommand> {
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<CreateUserCommand> {
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<CreateUserCommand> {
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<CreateUserCommand> {
.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<CreateUserCommand> {
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<CreateUserCommand> {
.hasMessageThat()
.isEqualTo("A user with email user@example.test already exists");
verifyNoMoreInteractions(iamClient);
verifyNoInteractions(connection);
}
@Test

View File

@@ -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<DeleteUserCommand> {
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<DeleteUserCommand> {
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<DeleteUserCommand> {
.hasMessageThat()
.isEqualTo("Email does not correspond to a valid user");
verifyNoInteractions(iamClient);
verifyNoInteractions(connection);
}
}

View File

@@ -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",

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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