Skip to content

Commit

Permalink
Add console users action (#2573)
Browse files Browse the repository at this point in the history
  • Loading branch information
ptkach authored Sep 30, 2024
1 parent cbe59b6 commit d17188b
Show file tree
Hide file tree
Showing 8 changed files with 358 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import static google.registry.util.PasswordUtils.hashPassword;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;

import com.google.gson.annotations.Expose;
import google.registry.model.Buildable;
import google.registry.model.UpdateAutoTimestampEntity;
import google.registry.util.PasswordUtils;
Expand Down Expand Up @@ -50,12 +51,13 @@ public class UserBase extends UpdateAutoTimestampEntity implements Buildable {
private static final long serialVersionUID = 6936728603828566721L;

/** Email address of the user in question. */
@Transient String emailAddress;
@Transient @Expose String emailAddress;

/** Optional external email address to use for registry lock confirmation emails. */
@Column String registryLockEmailAddress;

/** Roles (which grant permissions) associated with this user. */
@Expose
@Column(nullable = false)
UserRoles userRoles;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;

import com.google.common.collect.ImmutableMap;
import com.google.gson.annotations.Expose;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.persistence.converter.RegistrarToRoleMapUserType;
Expand Down Expand Up @@ -53,6 +54,7 @@ public class UserRoles extends ImmutableObject implements Buildable {
private GlobalRole globalRole = GlobalRole.NONE;

/** Any per-registrar roles that this user may have. */
@Expose
@Type(RegistrarToRoleMapUserType.class)
private Map<String, RegistrarRole> registrarRoles = ImmutableMap.of();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
import google.registry.ui.server.console.ConsoleRegistryLockVerifyAction;
import google.registry.ui.server.console.ConsoleUpdateRegistrarAction;
import google.registry.ui.server.console.ConsoleUserDataAction;
import google.registry.ui.server.console.ConsoleUsersAction;
import google.registry.ui.server.console.RegistrarsAction;
import google.registry.ui.server.console.settings.ContactAction;
import google.registry.ui.server.console.settings.SecurityAction;
Expand Down Expand Up @@ -189,6 +190,8 @@ interface RequestComponent {

ConsoleUserDataAction consoleUserDataAction();

ConsoleUsersAction consoleUsersAction();

ConsoleDumDownloadAction consoleDumDownloadAction();

ContactAction contactAction();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import google.registry.ui.server.console.ConsoleRegistryLockVerifyAction;
import google.registry.ui.server.console.ConsoleUpdateRegistrarAction;
import google.registry.ui.server.console.ConsoleUserDataAction;
import google.registry.ui.server.console.ConsoleUsersAction;
import google.registry.ui.server.console.RegistrarsAction;
import google.registry.ui.server.console.settings.ContactAction;
import google.registry.ui.server.console.settings.SecurityAction;
Expand Down Expand Up @@ -81,6 +82,8 @@ public interface FrontendRequestComponent {

ConsoleUserDataAction consoleUserDataAction();

ConsoleUsersAction consoleUsersAction();

ConsoleDumDownloadAction consoleDumDownloadAction();

ContactAction contactAction();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// 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.ui.server.console;

import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.model.console.RegistrarRole.ACCOUNT_MANAGER;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.POST;
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;

import com.google.api.services.directory.Directory;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.persistence.VKey;
import google.registry.request.Action;
import google.registry.request.Action.GkeService;
import google.registry.request.HttpException.BadRequestException;
import google.registry.request.Parameter;
import google.registry.request.auth.Auth;
import google.registry.util.StringGenerator;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.inject.Inject;
import javax.inject.Named;

@Action(
service = Action.GaeService.DEFAULT,
gkeService = GkeService.CONSOLE,
path = ConsoleUsersAction.PATH,
method = {GET, POST},
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public class ConsoleUsersAction extends ConsoleApiAction {
static final String PATH = "/console-api/users";
private static final int PASSWORD_LENGTH = 16;

private static final Splitter EMAIL_SPLITTER = Splitter.on('@').trimResults();

private final Gson gson;
private final String registrarId;
private final Directory directory;
private final StringGenerator passwordGenerator;

@Inject
public ConsoleUsersAction(
ConsoleApiParams consoleApiParams,
Gson gson,
Directory directory,
@Named("base58StringGenerator") StringGenerator passwordGenerator,
@Parameter("registrarId") String registrarId) {
super(consoleApiParams);
this.gson = gson;
this.registrarId = registrarId;
this.directory = directory;
this.passwordGenerator = passwordGenerator;
}

private static String generateNewEmailAddress(User user, String increment) {
List<String> emailParts = EMAIL_SPLITTER.splitToList(user.getEmailAddress());
return String.format("%s-%s@%s", emailParts.get(0), increment, emailParts.get(1));
}

@Override
protected void postHandler(User user) {
// Temporary flag while testing
if (user.getUserRoles().isAdmin()) {
checkPermission(user, registrarId, ConsolePermission.MANAGE_USERS);
tm().transact(() -> runInTransaction(user));
} else {
consoleApiParams.response().setStatus(SC_FORBIDDEN);
}
}

@Override
protected void getHandler(User user) {
checkPermission(user, registrarId, ConsolePermission.MANAGE_USERS);
List<User> users =
getAllUsers().stream()
.filter(u -> u.getUserRoles().getRegistrarRoles().containsKey(registrarId))
.collect(Collectors.toList());
consoleApiParams.response().setPayload(gson.toJson(users));
consoleApiParams.response().setStatus(SC_OK);
}

private void runInTransaction(User user) throws IOException {
String nextAvailableIncrement =
Stream.of("1", "2", "3")
.filter(
increment ->
tm().loadByKeyIfPresent(
VKey.create(User.class, generateNewEmailAddress(user, increment)))
.isEmpty())
.findFirst()
.orElseThrow(() -> new BadRequestException("Extra users amount is limited to 3"));

com.google.api.services.directory.model.User newUser =
new com.google.api.services.directory.model.User();
newUser.setPassword(passwordGenerator.createString(PASSWORD_LENGTH));
newUser.setPrimaryEmail(generateNewEmailAddress(user, nextAvailableIncrement));

try {
directory.users().insert(newUser).execute();
} catch (IOException e) {
setFailedResponse("Failed to create the user workspace account", SC_INTERNAL_SERVER_ERROR);
throw e;
}

UserRoles userRoles =
new UserRoles.Builder()
.setRegistrarRoles(ImmutableMap.of(registrarId, ACCOUNT_MANAGER))
.build();

User.Builder builder =
new User.Builder().setUserRoles(userRoles).setEmailAddress(newUser.getPrimaryEmail());
tm().put(builder.build());

consoleApiParams.response().setStatus(SC_OK);
consoleApiParams
.response()
.setPayload(
gson.toJson(
ImmutableMap.of(
"password", newUser.getPassword(), "email", newUser.getPrimaryEmail())));
}

private ImmutableList<User> getAllUsers() {
return tm().transact(
() ->
tm().loadAllOf(User.class).stream()
.filter(u -> !u.getUserRoles().getRegistrarRoles().isEmpty())
.collect(toImmutableList()));
}
}
Loading

0 comments on commit d17188b

Please sign in to comment.