diff --git a/core/src/main/java/google/registry/ui/server/console/ConsoleUsersAction.java b/core/src/main/java/google/registry/ui/server/console/ConsoleUsersAction.java index 4f188ad16..7b43dd2e6 100644 --- a/core/src/main/java/google/registry/ui/server/console/ConsoleUsersAction.java +++ b/core/src/main/java/google/registry/ui/server/console/ConsoleUsersAction.java @@ -33,6 +33,7 @@ import com.google.api.services.directory.model.UserName; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.flogger.FluentLogger; import com.google.gson.annotations.Expose; import google.registry.config.RegistryConfig.Config; import google.registry.model.console.ConsolePermission; @@ -58,6 +59,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.annotation.Nullable; @@ -69,6 +71,7 @@ import javax.annotation.Nullable; public class ConsoleUsersAction extends ConsoleApiAction { static final String PATH = "/console-api/users"; + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private static final int PASSWORD_LENGTH = 16; private final String registrarId; @@ -184,11 +187,20 @@ public class ConsoleUsersAction extends ConsoleApiAction { // User has no registrars assigned if (updatedUser.getUserRoles().getRegistrarRoles().isEmpty()) { - try { - directory.users().delete(email).execute(); - } catch (IOException e) { - setFailedResponse("Failed to delete the user workspace account", SC_INTERNAL_SERVER_ERROR); - throw e; + // Only delete the Workspace account if runCreate() provably minted it. Addresses that + // reached this row via CLI create_user, OteAccountBuilder, or other means must survive + if (isConsoleMintedAddress(email)) { + try { + directory.users().delete(email).execute(); + } catch (IOException e) { + setFailedResponse( + "Failed to delete the user workspace account", SC_INTERNAL_SERVER_ERROR); + throw e; + } + } else { + logger.atInfo().log( + "Skipping Workspace deletion for %s because the console did not mint the account", + email); } VKey key = VKey.create(User.class, email); @@ -368,6 +380,22 @@ public class ConsoleUsersAction extends ConsoleApiAction { return true; } + /** + * True iff {@code email} matches the exact shape produced by {@link #runCreate()} for the current + * {@code registrarId}: {@code <3 alnum>.@}. + * + *

Addresses of any other shape were provisioned outside this action and should not be passed + * to {@code directory.users().delete()}. + */ + private boolean isConsoleMintedAddress(String email) { + return email.endsWith("@" + gSuiteDomainName) + && Pattern.matches( + String.format( + "^[a-zA-Z0-9]{3}\\.%s@%s$", + Pattern.quote(registrarId), Pattern.quote(gSuiteDomainName)), + email); + } + public record UserData( @Expose String emailAddress, @Expose String registryLockEmailAddress, diff --git a/core/src/test/java/google/registry/ui/server/console/ConsoleUsersActionTest.java b/core/src/test/java/google/registry/ui/server/console/ConsoleUsersActionTest.java index 43941f84f..f96042bf7 100644 --- a/core/src/test/java/google/registry/ui/server/console/ConsoleUsersActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/ConsoleUsersActionTest.java @@ -20,7 +20,10 @@ import static jakarta.servlet.http.HttpServletResponse.SC_CREATED; import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN; import static jakarta.servlet.http.HttpServletResponse.SC_OK; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.google.api.services.directory.Directory; @@ -243,7 +246,8 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase { } @Test - void testSuccess_deletesUser() throws IOException { + void testSuccess_deletesUser_nonConsoleMintedAddress_skipsWorkspaceAccountDeletion() + throws IOException { User user1 = DatabaseHelper.loadByKey(VKey.create(User.class, "test1@test.com")); AuthResult authResult = AuthResult.createUser( @@ -265,6 +269,44 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase { assertThat(response.getStatus()).isEqualTo(SC_OK); assertThat(DatabaseHelper.loadByKeyIfPresent(VKey.create(User.class, "test2@test.com"))) .isEmpty(); + verify(users, never()).delete(anyString()); + } + + @Test + void testSuccess_deletesUser_consoleMintedAddress_deletesWorkspaceAccount() throws IOException { + User user1 = DatabaseHelper.loadByKey(VKey.create(User.class, "test1@test.com")); + AuthResult authResult = + AuthResult.createUser( + user1 + .asBuilder() + .setUserRoles(user1.getUserRoles().asBuilder().setIsAdmin(true).build()) + .build()); + String mintedEmail = "abc.TheRegistrar@email.com"; + DatabaseHelper.persistResource( + new User.Builder() + .setEmailAddress(mintedEmail) + .setUserRoles( + new UserRoles() + .asBuilder() + .setRegistrarRoles( + ImmutableMap.of("TheRegistrar", RegistrarRole.PRIMARY_CONTACT)) + .build()) + .build()); + + ConsoleUsersAction action = + createAction( + Optional.of(ConsoleApiParamsUtils.createFake(authResult)), + Optional.of("DELETE"), + Optional.of( + new UserData(mintedEmail, null, RegistrarRole.ACCOUNT_MANAGER.toString(), null))); + action.cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils(); + when(directory.users()).thenReturn(users); + when(users.delete(mintedEmail)).thenReturn(delete); + action.run(); + assertThat(response.getStatus()).isEqualTo(SC_OK); + assertThat(DatabaseHelper.loadByKeyIfPresent(VKey.create(User.class, mintedEmail))).isEmpty(); + verify(users).delete(mintedEmail); + verify(delete).execute(); } @Test