1
0
mirror of https://github.com/google/nomulus synced 2026-07-19 06:22:33 +00:00

Only delete Workspace accounts if we created them (#3161)

Just in case, we should check to make sure that we created the
account-with-no-registrars before we delete it.
This commit is contained in:
gbrodman
2026-07-17 12:10:34 -04:00
committed by GitHub
parent 6b74924067
commit d6dd95b052
2 changed files with 76 additions and 6 deletions
@@ -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<User> 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>.<registrarId>@<gSuiteDomainName>}.
*
* <p>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,
@@ -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