diff --git a/console-webapp/src/app/shared/services/backend.service.ts b/console-webapp/src/app/shared/services/backend.service.ts index 5b2c2b93d..a655c423f 100644 --- a/console-webapp/src/app/shared/services/backend.service.ts +++ b/console-webapp/src/app/shared/services/backend.service.ts @@ -172,6 +172,14 @@ export class BackendService { .pipe(catchError((err) => this.errorCatcher(err))); } + deleteUser(registrarId: string, emailAddress: string): Observable { + return this.http + .delete(`/console-api/users?registrarId=${registrarId}`, { + body: JSON.stringify({ emailAddress }), + }) + .pipe(catchError((err) => this.errorCatcher(err))); + } + getUserData(): Observable { return this.http .get('/console-api/userdata') diff --git a/console-webapp/src/app/users/userEdit.component.html b/console-webapp/src/app/users/userEdit.component.html new file mode 100644 index 000000000..aa9e24e9d --- /dev/null +++ b/console-webapp/src/app/users/userEdit.component.html @@ -0,0 +1,79 @@ +
+ @if(isNewUser) { +

+ {{ userDetails.emailAddress + " succesfully created" }} +

+ } @else { +

User details

+ } + +
+
+ +
+ +
+
+

+ +

+ + + + + +

User details

+
+ + + User email + {{ + userDetails.emailAddress + }} + + + + User role + {{ + roleToDescription(userDetails.role) + }} + + @if (userDetails.password) { + + + Password + + + + + + } +
+
+
+
diff --git a/console-webapp/src/app/users/userEdit.component.scss b/console-webapp/src/app/users/userEdit.component.scss new file mode 100644 index 000000000..c24ece06a --- /dev/null +++ b/console-webapp/src/app/users/userEdit.component.scss @@ -0,0 +1,30 @@ +// 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. + +.console-app { + &__user-details { + &-controls { + display: flex; + align-items: center; + margin: 20px 0; + } + &-password { + input { + border: none; + background: transparent; + } + } + max-width: 616px; + } +} diff --git a/console-webapp/src/app/users/userEdit.component.ts b/console-webapp/src/app/users/userEdit.component.ts new file mode 100644 index 000000000..19490dafe --- /dev/null +++ b/console-webapp/src/app/users/userEdit.component.ts @@ -0,0 +1,81 @@ +// 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. + +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { SelectedRegistrarModule } from '../app.module'; +import { MaterialModule } from '../material.module'; +import { RegistrarService } from '../registrar/registrar.service'; +import { SnackBarModule } from '../snackbar.module'; +import { User, UsersService, roleToDescription } from './users.service'; + +@Component({ + selector: 'app-user-edit', + templateUrl: './userEdit.component.html', + styleUrls: ['./userEdit.component.scss'], + standalone: true, + imports: [ + MaterialModule, + SnackBarModule, + CommonModule, + SelectedRegistrarModule, + ], + providers: [], +}) +export class UserEditComponent { + inEdit = false; + isPasswordVisible = false; + isNewUser = false; + isLoading = false; + userDetails: User; + + constructor( + protected registrarService: RegistrarService, + protected usersService: UsersService, + private _snackBar: MatSnackBar + ) { + this.userDetails = this.usersService + .users() + .filter( + (u) => u.emailAddress === this.usersService.currentlyOpenUserEmail() + )[0]; + if (this.usersService.isNewUser) { + this.isNewUser = true; + this.usersService.isNewUser = false; + } + } + + roleToDescription(role: string) { + return roleToDescription(role); + } + + deleteUser() { + this.isLoading = true; + this.usersService.deleteUser(this.userDetails.emailAddress).subscribe({ + error: (err) => { + this._snackBar.open(err.error || err.message); + this.isLoading = false; + }, + complete: () => { + this.isLoading = false; + this.goBack(); + }, + }); + } + + goBack() { + this.usersService.currentlyOpenUserEmail.set(''); + } +} diff --git a/console-webapp/src/app/users/users.component.html b/console-webapp/src/app/users/users.component.html index 41f67c60a..a193ca137 100644 --- a/console-webapp/src/app/users/users.component.html +++ b/console-webapp/src/app/users/users.component.html @@ -1,5 +1,11 @@ - @if (!isLoading) { + @if(isLoading) { +
+ +
+ } @else if(usersService.currentlyOpenUserEmail()) { + + } @else {

Users

@@ -32,12 +38,11 @@ > - +
- } @else { -
- -
} diff --git a/console-webapp/src/app/users/users.component.ts b/console-webapp/src/app/users/users.component.ts index 478764da6..f2ae187b3 100644 --- a/console-webapp/src/app/users/users.component.ts +++ b/console-webapp/src/app/users/users.component.ts @@ -22,15 +22,8 @@ import { SelectedRegistrarModule } from '../app.module'; import { MaterialModule } from '../material.module'; import { RegistrarService } from '../registrar/registrar.service'; import { SnackBarModule } from '../snackbar.module'; -import { User, UsersService } from './users.service'; - -const roleToDescription = (role: String) => { - if (!role) return 'N/A'; - else if (role.toLowerCase().startsWith('account_manager')) { - return 'Viewer'; - } - return 'Editor'; -}; +import { UserEditComponent } from './userEdit.component'; +import { roleToDescription, User, UsersService } from './users.service'; export const columns = [ { @@ -55,6 +48,7 @@ export const columns = [ SnackBarModule, CommonModule, SelectedRegistrarModule, + UserEditComponent, ], providers: [UsersService], }) @@ -92,6 +86,7 @@ export class UsersComponent { this.usersService.fetchUsers().subscribe({ error: (err: HttpErrorResponse) => { this._snackBar.open(err.error || err.message); + this.isLoading = false; }, complete: () => { this.isLoading = false; @@ -102,21 +97,17 @@ export class UsersComponent { createNewUser() { this.isLoading = true; this.usersService.createNewUser().subscribe({ - next: (newUser) => { - this._snackBar.open( - `New user with email ${newUser.emailAddress} has been created.`, - '', - { - duration: 2000, - } - ); - }, error: (err: HttpErrorResponse) => { this._snackBar.open(err.error || err.message); + this.isLoading = false; }, complete: () => { this.isLoading = false; }, }); } + + openDetails(emailAddress: string) { + this.usersService.currentlyOpenUserEmail.set(emailAddress); + } } diff --git a/console-webapp/src/app/users/users.service.ts b/console-webapp/src/app/users/users.service.ts index 06ee32399..871210067 100644 --- a/console-webapp/src/app/users/users.service.ts +++ b/console-webapp/src/app/users/users.service.ts @@ -17,19 +17,29 @@ import { tap } from 'rxjs'; import { RegistrarService } from '../registrar/registrar.service'; import { BackendService } from '../shared/services/backend.service'; +export const roleToDescription = (role: string) => { + if (!role) return 'N/A'; + else if (role.toLowerCase().startsWith('account_manager')) { + return 'Viewer'; + } + return 'Editor'; +}; + export interface CreateAutoTimestamp { creationTime: string; } export interface User { - emailAddress: String; - role: String; - password?: String; + emailAddress: string; + role: string; + password?: string; } @Injectable() export class UsersService { users = signal([]); + currentlyOpenUserEmail = signal(''); + isNewUser: boolean = false; constructor( private backendService: BackendService, @@ -52,7 +62,15 @@ export class UsersService { .pipe( tap((newUser: User) => { this.users.set([...this.users(), newUser]); + this.currentlyOpenUserEmail.set(newUser.emailAddress); + this.isNewUser = true; }) ); } + + deleteUser(emailAddress: string) { + return this.backendService + .deleteUser(this.registrarService.registrarId(), emailAddress) + .pipe(tap((_) => this.fetchUsers())); + } } diff --git a/core/src/main/java/google/registry/request/Action.java b/core/src/main/java/google/registry/request/Action.java index 2445006e1..30dab90c8 100644 --- a/core/src/main/java/google/registry/request/Action.java +++ b/core/src/main/java/google/registry/request/Action.java @@ -33,7 +33,8 @@ public @interface Action { enum Method { GET, HEAD, - POST + POST, + DELETE } interface Service { diff --git a/core/src/main/java/google/registry/ui/server/console/ConsoleApiAction.java b/core/src/main/java/google/registry/ui/server/console/ConsoleApiAction.java index 8625bef98..05fe9553c 100644 --- a/core/src/main/java/google/registry/ui/server/console/ConsoleApiAction.java +++ b/core/src/main/java/google/registry/ui/server/console/ConsoleApiAction.java @@ -16,7 +16,9 @@ package google.registry.ui.server.console; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static google.registry.request.Action.Method.DELETE; import static google.registry.request.Action.Method.GET; +import static google.registry.request.Action.Method.HEAD; import static google.registry.request.Action.Method.POST; import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN; @@ -75,13 +77,19 @@ public abstract class ConsoleApiAction implements Runnable { return; } User user = consoleApiParams.authResult().user().get(); - + String requestMethod = consoleApiParams.request().getMethod(); try { - if (consoleApiParams.request().getMethod().equals(GET.toString())) { + if (requestMethod.equals(GET.toString())) { getHandler(user); + } else if (requestMethod.equals(HEAD.toString())) { + headHandler(user); } else { if (verifyXSRF(user)) { - postHandler(user); + if (requestMethod.equals(DELETE.toString())) { + deleteHandler(user); + } else { + postHandler(user); + } } } } catch (ConsolePermissionForbiddenException e) { @@ -113,6 +121,14 @@ public abstract class ConsoleApiAction implements Runnable { throw new UnsupportedOperationException("Console API GET handler not implemented"); } + protected void deleteHandler(User user) { + throw new UnsupportedOperationException("Console API DELETE handler not implemented"); + } + + protected void headHandler(User user) { + throw new UnsupportedOperationException("Console API HEAD handler not implemented"); + } + protected void setFailedResponse(String message, int code) { consoleApiParams.response().setStatus(code); consoleApiParams.response().setPayload(message); diff --git a/core/src/main/java/google/registry/ui/server/console/ConsoleModule.java b/core/src/main/java/google/registry/ui/server/console/ConsoleModule.java index 181d84cf0..4f19e1843 100644 --- a/core/src/main/java/google/registry/ui/server/console/ConsoleModule.java +++ b/core/src/main/java/google/registry/ui/server/console/ConsoleModule.java @@ -36,6 +36,7 @@ import google.registry.ui.server.SendEmailUtils; import google.registry.ui.server.console.ConsoleEppPasswordAction.EppPasswordData; import google.registry.ui.server.console.ConsoleOteAction.OteCreateData; import google.registry.ui.server.console.ConsoleRegistryLockAction.ConsoleRegistryLockPostInput; +import google.registry.ui.server.console.ConsoleUsersAction.UserDeleteData; import jakarta.servlet.http.HttpServletRequest; import java.util.Optional; import org.joda.time.DateTime; @@ -245,6 +246,13 @@ public final class ConsoleModule { return payload.map(s -> gson.fromJson(s, EppPasswordData.class)); } + @Provides + @Parameter("userDeleteData") + public static Optional provideUserDeleteData( + Gson gson, @OptionalJsonPayload Optional payload) { + return payload.map(s -> gson.fromJson(s, UserDeleteData.class)); + } + @Provides @Parameter("oteCreateData") public static Optional provideOteCreateData( 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 9d88ec854..7dbc94f42 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 @@ -14,11 +14,14 @@ package google.registry.ui.server.console; +import static com.google.common.base.Strings.isNullOrEmpty; 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.DELETE; import static google.registry.request.Action.Method.GET; import static google.registry.request.Action.Method.POST; +import static jakarta.servlet.http.HttpServletResponse.SC_CREATED; 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; @@ -29,6 +32,8 @@ import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.gson.Gson; +import com.google.gson.annotations.Expose; +import google.registry.config.RegistryConfig.Config; import google.registry.model.console.ConsolePermission; import google.registry.model.console.User; import google.registry.model.console.UserRoles; @@ -38,11 +43,13 @@ 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.tools.IamClient; import google.registry.util.StringGenerator; import java.io.IOException; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; -import java.util.stream.Stream; +import java.util.stream.IntStream; import javax.inject.Inject; import javax.inject.Named; @@ -50,36 +57,42 @@ import javax.inject.Named; service = Action.GaeService.DEFAULT, gkeService = GkeService.CONSOLE, path = ConsoleUsersAction.PATH, - method = {GET, POST}, + method = {GET, POST, DELETE}, 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; + private final Optional userDeleteData; + private final Optional maybeGroupEmailAddress; + private final IamClient iamClient; + private final String gSuiteDomainName; @Inject public ConsoleUsersAction( ConsoleApiParams consoleApiParams, Gson gson, Directory directory, + IamClient iamClient, + @Config("gSuiteDomainName") String gSuiteDomainName, + @Config("gSuiteConsoleUserGroupEmailAddress") Optional maybeGroupEmailAddress, @Named("base58StringGenerator") StringGenerator passwordGenerator, + @Parameter("userDeleteData") Optional userDeleteData, @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 emailParts = EMAIL_SPLITTER.splitToList(user.getEmailAddress()); - return String.format("%s-%s@%s", emailParts.get(0), increment, emailParts.get(1)); + this.userDeleteData = userDeleteData; + this.maybeGroupEmailAddress = maybeGroupEmailAddress; + this.iamClient = iamClient; + this.gSuiteDomainName = gSuiteDomainName; } @Override @@ -87,7 +100,7 @@ public class ConsoleUsersAction extends ConsoleApiAction { // Temporary flag while testing if (user.getUserRoles().isAdmin()) { checkPermission(user, registrarId, ConsolePermission.MANAGE_USERS); - tm().transact(() -> runInTransaction(user)); + tm().transact(() -> runCreateInTransaction()); } else { consoleApiParams.response().setStatus(SC_FORBIDDEN); } @@ -97,8 +110,7 @@ public class ConsoleUsersAction extends ConsoleApiAction { protected void getHandler(User user) { checkPermission(user, registrarId, ConsolePermission.MANAGE_USERS); List users = - getAllUsers().stream() - .filter(u -> u.getUserRoles().getRegistrarRoles().containsKey(registrarId)) + getAllRegistrarUsers(registrarId).stream() .map( u -> ImmutableMap.of( @@ -112,24 +124,71 @@ public class ConsoleUsersAction extends ConsoleApiAction { 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()) + @Override + protected void deleteHandler(User user) { + // Temporary flag while testing + if (user.getUserRoles().isAdmin()) { + checkPermission(user, registrarId, ConsolePermission.MANAGE_USERS); + tm().transact(() -> runDeleteInTransaction()); + } else { + consoleApiParams.response().setStatus(SC_FORBIDDEN); + } + } + + private void runDeleteInTransaction() throws IOException { + if (userDeleteData.isEmpty() || isNullOrEmpty(userDeleteData.get().userEmail)) { + throw new BadRequestException("Missing user data param"); + } + String email = userDeleteData.get().userEmail; + User userToDelete = + tm().loadByKeyIfPresent(VKey.create(User.class, email)) + .orElseThrow( + () -> new BadRequestException(String.format("User %s doesn't exist", email))); + + if (!userToDelete.getUserRoles().getRegistrarRoles().containsKey(registrarId)) { + setFailedResponse( + String.format("Can't delete user not associated with registrarId %s", registrarId), + SC_FORBIDDEN); + return; + } + + try { + directory.users().delete(email).execute(); + } catch (IOException e) { + setFailedResponse("Failed to delete the user workspace account", SC_INTERNAL_SERVER_ERROR); + throw e; + } + + VKey key = VKey.create(User.class, email); + tm().delete(key); + User.revokeIapPermission(email, maybeGroupEmailAddress, cloudTasksUtils, null, iamClient); + + consoleApiParams.response().setStatus(SC_OK); + } + + private void runCreateInTransaction() throws IOException { + ImmutableList allRegistrarUsers = getAllRegistrarUsers(registrarId); + if (allRegistrarUsers.size() >= 4) + throw new BadRequestException("Total users amount per registrar is limited to 4"); + + String nextAvailableEmail = + IntStream.range(1, 5) + .mapToObj(i -> String.format("%s-user%s@%s", registrarId, i, gSuiteDomainName)) + .filter(email -> tm().loadByKeyIfPresent(VKey.create(User.class, email)).isEmpty()) .findFirst() - .orElseThrow(() -> new BadRequestException("Extra users amount is limited to 3")); + // Can only happen if registrar cycled through 20 users, which is unlikely + .orElseThrow( + () -> new BadRequestException("Failed to find available increment for new user")); com.google.api.services.directory.model.User newUser = new com.google.api.services.directory.model.User(); newUser.setName( - new UserName().setFamilyName(registrarId).setGivenName("User" + nextAvailableIncrement)); + new UserName() + .setFamilyName(registrarId) + .setGivenName(EMAIL_SPLITTER.splitToList(nextAvailableEmail).get(0))); newUser.setPassword(passwordGenerator.createString(PASSWORD_LENGTH)); - newUser.setPrimaryEmail(generateNewEmailAddress(user, nextAvailableIncrement)); + newUser.setPrimaryEmail(nextAvailableEmail); try { directory.users().insert(newUser).execute(); @@ -146,8 +205,10 @@ public class ConsoleUsersAction extends ConsoleApiAction { User.Builder builder = new User.Builder().setUserRoles(userRoles).setEmailAddress(newUser.getPrimaryEmail()); tm().put(builder.build()); + User.grantIapPermission( + nextAvailableEmail, maybeGroupEmailAddress, cloudTasksUtils, null, iamClient); - consoleApiParams.response().setStatus(SC_OK); + consoleApiParams.response().setStatus(SC_CREATED); consoleApiParams .response() .setPayload( @@ -161,11 +222,13 @@ public class ConsoleUsersAction extends ConsoleApiAction { ACCOUNT_MANAGER))); } - private ImmutableList getAllUsers() { + private ImmutableList getAllRegistrarUsers(String registrarId) { return tm().transact( () -> tm().loadAllOf(User.class).stream() - .filter(u -> !u.getUserRoles().getRegistrarRoles().isEmpty()) + .filter(u -> u.getUserRoles().getRegistrarRoles().containsKey(registrarId)) .collect(toImmutableList())); } + + public record UserDeleteData(@Expose String userEmail) {} } 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 dc5e28ade..8195fe859 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 @@ -16,6 +16,8 @@ package google.registry.ui.server.console; import static com.google.common.truth.Truth.assertThat; import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +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.Mockito.mock; @@ -23,6 +25,7 @@ import static org.mockito.Mockito.when; import com.google.api.services.directory.Directory; import com.google.api.services.directory.Directory.Users; +import com.google.api.services.directory.Directory.Users.Delete; import com.google.api.services.directory.Directory.Users.Insert; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -31,17 +34,23 @@ import google.registry.model.console.GlobalRole; import google.registry.model.console.RegistrarRole; import google.registry.model.console.User; import google.registry.model.console.UserRoles; +import google.registry.persistence.VKey; import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.request.RequestModule; import google.registry.request.auth.AuthResult; +import google.registry.testing.CloudTasksHelper; import google.registry.testing.ConsoleApiParamsUtils; import google.registry.testing.DatabaseHelper; import google.registry.testing.DeterministicStringGenerator; import google.registry.testing.FakeResponse; +import google.registry.tools.IamClient; +import google.registry.ui.server.console.ConsoleUsersAction.UserDeleteData; import google.registry.util.StringGenerator; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -53,6 +62,10 @@ class ConsoleUsersActionTest { private final Directory directory = mock(Directory.class); private final Users users = mock(Users.class); private final Insert insert = mock(Insert.class); + private final Delete delete = mock(Delete.class); + private final IamClient iamClient = mock(IamClient.class); + + private final CloudTasksHelper cloudTasksHelper = new CloudTasksHelper(); private StringGenerator passwordGenerator = new DeterministicStringGenerator("abcdefghijklmnopqrstuvwxyz"); @@ -112,7 +125,10 @@ class ConsoleUsersActionTest { AuthResult authResult = AuthResult.createUser(user); ConsoleUsersAction action = - createAction(Optional.of(ConsoleApiParamsUtils.createFake(authResult)), Optional.of("GET")); + createAction( + Optional.of(ConsoleApiParamsUtils.createFake(authResult)), + Optional.of("GET"), + Optional.empty()); action.run(); var response = ((FakeResponse) consoleApiParams.response()); assertThat(response.getPayload()) @@ -134,7 +150,10 @@ class ConsoleUsersActionTest { AuthResult authResult = AuthResult.createUser(user); ConsoleUsersAction action = - createAction(Optional.of(ConsoleApiParamsUtils.createFake(authResult)), Optional.of("GET")); + createAction( + Optional.of(ConsoleApiParamsUtils.createFake(authResult)), + Optional.of("GET"), + Optional.empty()); action.run(); var response = ((FakeResponse) consoleApiParams.response()); assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN); @@ -146,43 +165,141 @@ class ConsoleUsersActionTest { AuthResult authResult = AuthResult.createUser(user); ConsoleUsersAction action = createAction( - Optional.of(ConsoleApiParamsUtils.createFake(authResult)), Optional.of("POST")); + Optional.of(ConsoleApiParamsUtils.createFake(authResult)), + Optional.of("POST"), + Optional.empty()); + action.cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils(); when(directory.users()).thenReturn(users); when(users.insert(any(com.google.api.services.directory.model.User.class))).thenReturn(insert); action.run(); var response = ((FakeResponse) consoleApiParams.response()); - assertThat(response.getStatus()).isEqualTo(SC_OK); + assertThat(response.getStatus()).isEqualTo(SC_CREATED); assertThat(response.getPayload()) .contains( - "{\"password\":\"abcdefghijklmnop\",\"emailAddress\":\"email-1@email.com\",\"role\":\"ACCOUNT_MANAGER\"}"); + "{\"password\":\"abcdefghijklmnop\",\"emailAddress\":\"TheRegistrar-user1@email.com\",\"role\":\"ACCOUNT_MANAGER\"}"); } @Test - void testFailure_limitedTo3NewUsers() throws IOException { + void testFailure_noPermissionToDeleteUser() 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()); + ConsoleUsersAction action = + createAction( + Optional.of(ConsoleApiParamsUtils.createFake(authResult)), + Optional.of("DELETE"), + Optional.of(new UserDeleteData("test3@test.com"))); + when(directory.users()).thenReturn(users); + when(users.delete(any(String.class))).thenReturn(delete); + action.run(); + var response = ((FakeResponse) consoleApiParams.response()); + assertThat(response.getStatus()).isEqualTo(SC_FORBIDDEN); + assertThat(response.getPayload()) + .contains("Can't delete user not associated with registrarId TheRegistrar"); + } + + @Test + void testFailure_userDoesntExist() throws IOException { User user = DatabaseHelper.createAdminUser("email@email.com"); - DatabaseHelper.createAdminUser("email-1@email.com"); - DatabaseHelper.createAdminUser("email-2@email.com"); - DatabaseHelper.createAdminUser("email-3@email.com"); AuthResult authResult = AuthResult.createUser(user); ConsoleUsersAction action = createAction( - Optional.of(ConsoleApiParamsUtils.createFake(authResult)), Optional.of("POST")); + Optional.of(ConsoleApiParamsUtils.createFake(authResult)), + Optional.of("DELETE"), + Optional.of(new UserDeleteData("email-1@email.com"))); + when(directory.users()).thenReturn(users); + when(users.delete(any(String.class))).thenReturn(delete); + action.run(); + var response = ((FakeResponse) consoleApiParams.response()); + assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); + assertThat(response.getPayload()).contains("User email-1@email.com doesn't exist"); + } + + @Test + void testSuccess_deletesUser() 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()); + ConsoleUsersAction action = + createAction( + Optional.of(ConsoleApiParamsUtils.createFake(authResult)), + Optional.of("DELETE"), + Optional.of(new UserDeleteData("test2@test.com"))); + action.cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils(); + when(directory.users()).thenReturn(users); + when(users.delete(any(String.class))).thenReturn(delete); + action.run(); + var response = ((FakeResponse) consoleApiParams.response()); + assertThat(response.getStatus()).isEqualTo(SC_OK); + assertThat(DatabaseHelper.loadByKeyIfPresent(VKey.create(User.class, "test2@test.com"))) + .isEmpty(); + } + + @Test + void testFailure_limitedTo4UsersPerRegistrar() 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()); + + DatabaseHelper.persistResources( + IntStream.range(3, 5) + .mapToObj( + i -> + new User.Builder() + .setEmailAddress(String.format("test%s@test.com", i)) + .setUserRoles( + new UserRoles() + .asBuilder() + .setRegistrarRoles( + ImmutableMap.of("TheRegistrar", RegistrarRole.PRIMARY_CONTACT)) + .build()) + .build()) + .collect(Collectors.toList())); + + ConsoleUsersAction action = + createAction( + Optional.of(ConsoleApiParamsUtils.createFake(authResult)), + Optional.of("POST"), + Optional.empty()); + action.cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils(); when(directory.users()).thenReturn(users); when(users.insert(any(com.google.api.services.directory.model.User.class))).thenReturn(insert); action.run(); var response = ((FakeResponse) consoleApiParams.response()); assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); - assertThat(response.getPayload()).contains("Extra users amount is limited to 3"); + assertThat(response.getPayload()).contains("Total users amount per registrar is limited to 4"); } private ConsoleUsersAction createAction( - Optional maybeConsoleApiParams, Optional method) + Optional maybeConsoleApiParams, + Optional method, + Optional userDeleteData) throws IOException { consoleApiParams = maybeConsoleApiParams.orElseGet( () -> ConsoleApiParamsUtils.createFake(AuthResult.NOT_AUTHENTICATED)); when(consoleApiParams.request().getMethod()).thenReturn(method.orElse("GET")); return new ConsoleUsersAction( - consoleApiParams, GSON, directory, passwordGenerator, "TheRegistrar"); + consoleApiParams, + GSON, + directory, + iamClient, + "email.com", + Optional.of("someRandomString"), + passwordGenerator, + userDeleteData, + "TheRegistrar"); } } diff --git a/core/src/test/resources/google/registry/module/frontend/frontend_routing.txt b/core/src/test/resources/google/registry/module/frontend/frontend_routing.txt index 3412abb69..d7f98a8d0 100644 --- a/core/src/test/resources/google/registry/module/frontend/frontend_routing.txt +++ b/core/src/test/resources/google/registry/module/frontend/frontend_routing.txt @@ -1,16 +1,16 @@ -SERVICE PATH CLASS METHODS OK MIN USER_POLICY -FRONTEND /_dr/epp EppTlsAction POST n APP ADMIN -CONSOLE /console-api/domain ConsoleDomainGetAction GET n USER PUBLIC -CONSOLE /console-api/domain-list ConsoleDomainListAction GET n USER PUBLIC -CONSOLE /console-api/dum-download ConsoleDumDownloadAction GET n USER PUBLIC -CONSOLE /console-api/eppPassword ConsoleEppPasswordAction POST n USER PUBLIC -CONSOLE /console-api/ote ConsoleOteAction GET,POST n USER PUBLIC -CONSOLE /console-api/registrar ConsoleUpdateRegistrarAction POST n USER PUBLIC -CONSOLE /console-api/registrars RegistrarsAction GET,POST n USER PUBLIC -CONSOLE /console-api/registry-lock ConsoleRegistryLockAction GET,POST n USER PUBLIC -CONSOLE /console-api/registry-lock-verify ConsoleRegistryLockVerifyAction GET n USER PUBLIC -CONSOLE /console-api/settings/contacts ContactAction GET,POST n USER PUBLIC -CONSOLE /console-api/settings/security SecurityAction POST n USER PUBLIC -CONSOLE /console-api/settings/whois-fields WhoisRegistrarFieldsAction POST n USER PUBLIC -CONSOLE /console-api/userdata ConsoleUserDataAction GET n USER PUBLIC -CONSOLE /console-api/users ConsoleUsersAction GET,POST n USER PUBLIC \ No newline at end of file +SERVICE PATH CLASS METHODS OK MIN USER_POLICY +FRONTEND /_dr/epp EppTlsAction POST n APP ADMIN +CONSOLE /console-api/domain ConsoleDomainGetAction GET n USER PUBLIC +CONSOLE /console-api/domain-list ConsoleDomainListAction GET n USER PUBLIC +CONSOLE /console-api/dum-download ConsoleDumDownloadAction GET n USER PUBLIC +CONSOLE /console-api/eppPassword ConsoleEppPasswordAction POST n USER PUBLIC +CONSOLE /console-api/ote ConsoleOteAction GET,POST n USER PUBLIC +CONSOLE /console-api/registrar ConsoleUpdateRegistrarAction POST n USER PUBLIC +CONSOLE /console-api/registrars RegistrarsAction GET,POST n USER PUBLIC +CONSOLE /console-api/registry-lock ConsoleRegistryLockAction GET,POST n USER PUBLIC +CONSOLE /console-api/registry-lock-verify ConsoleRegistryLockVerifyAction GET n USER PUBLIC +CONSOLE /console-api/settings/contacts ContactAction GET,POST n USER PUBLIC +CONSOLE /console-api/settings/security SecurityAction POST n USER PUBLIC +CONSOLE /console-api/settings/whois-fields WhoisRegistrarFieldsAction POST n USER PUBLIC +CONSOLE /console-api/userdata ConsoleUserDataAction GET n USER PUBLIC +CONSOLE /console-api/users ConsoleUsersAction GET,POST,DELETE n USER PUBLIC \ No newline at end of file diff --git a/core/src/test/resources/google/registry/module/routing.txt b/core/src/test/resources/google/registry/module/routing.txt index 094240104..6c33545a6 100644 --- a/core/src/test/resources/google/registry/module/routing.txt +++ b/core/src/test/resources/google/registry/module/routing.txt @@ -1,82 +1,82 @@ -SERVICE PATH CLASS METHODS OK MIN USER_POLICY -FRONTEND /_dr/epp EppTlsAction POST n APP ADMIN -BACKEND /_dr/admin/createGroups CreateGroupsAction POST n APP ADMIN -BACKEND /_dr/admin/list/domains ListDomainsAction GET,POST n APP ADMIN -BACKEND /_dr/admin/list/hosts ListHostsAction GET,POST n APP ADMIN -BACKEND /_dr/admin/list/premiumLists ListPremiumListsAction GET,POST n APP ADMIN -BACKEND /_dr/admin/list/registrars ListRegistrarsAction GET,POST n APP ADMIN -BACKEND /_dr/admin/list/reservedLists ListReservedListsAction GET,POST n APP ADMIN -BACKEND /_dr/admin/list/tlds ListTldsAction GET,POST n APP ADMIN -BACKEND /_dr/admin/updateUserGroup UpdateUserGroupAction POST n APP ADMIN -BACKEND /_dr/admin/verifyOte VerifyOteAction POST n APP ADMIN -BACKEND /_dr/cron/fanout TldFanoutAction GET y APP ADMIN -BACKEND /_dr/epptool EppToolAction POST n APP ADMIN -BACKEND /_dr/loadtest LoadTestAction POST y APP ADMIN -BACKEND /_dr/task/brdaCopy BrdaCopyAction POST y APP ADMIN -BACKEND /_dr/task/bsaDownload BsaDownloadAction GET,POST n APP ADMIN -BACKEND /_dr/task/bsaRefresh BsaRefreshAction GET,POST n APP ADMIN -BACKEND /_dr/task/bsaValidate BsaValidateAction GET,POST n APP ADMIN -BACKEND /_dr/task/copyDetailReports CopyDetailReportsAction POST n APP ADMIN -BACKEND /_dr/task/deleteExpiredDomains DeleteExpiredDomainsAction GET n APP ADMIN -BACKEND /_dr/task/deleteLoadTestData DeleteLoadTestDataAction POST n APP ADMIN -BACKEND /_dr/task/deleteProberData DeleteProberDataAction POST n APP ADMIN -BACKEND /_dr/task/dnsRefresh RefreshDnsAction GET y APP ADMIN -BACKEND /_dr/task/executeCannedScript CannedScriptExecutionAction POST,GET y APP ADMIN -BACKEND /_dr/task/expandBillingRecurrences ExpandBillingRecurrencesAction GET n APP ADMIN -BACKEND /_dr/task/exportDomainLists ExportDomainListsAction POST n APP ADMIN -BACKEND /_dr/task/exportPremiumTerms ExportPremiumTermsAction POST n APP ADMIN -BACKEND /_dr/task/exportReservedTerms ExportReservedTermsAction POST n APP ADMIN -BACKEND /_dr/task/generateInvoices GenerateInvoicesAction POST n APP ADMIN -BACKEND /_dr/task/generateSpec11 GenerateSpec11ReportAction POST n APP ADMIN -BACKEND /_dr/task/generateZoneFiles GenerateZoneFilesAction POST n APP ADMIN -BACKEND /_dr/task/icannReportingStaging IcannReportingStagingAction POST n APP ADMIN -BACKEND /_dr/task/icannReportingUpload IcannReportingUploadAction POST n APP ADMIN -BACKEND /_dr/task/nordnUpload NordnUploadAction POST y APP ADMIN -BACKEND /_dr/task/nordnVerify NordnVerifyAction POST y APP ADMIN -BACKEND /_dr/task/publishDnsUpdates PublishDnsUpdatesAction POST y APP ADMIN -BACKEND /_dr/task/publishInvoices PublishInvoicesAction POST n APP ADMIN -BACKEND /_dr/task/publishSpec11 PublishSpec11ReportAction POST n APP ADMIN -BACKEND /_dr/task/rdeReport RdeReportAction POST n APP ADMIN -BACKEND /_dr/task/rdeStaging RdeStagingAction GET,POST n APP ADMIN -BACKEND /_dr/task/rdeUpload RdeUploadAction POST n APP ADMIN -BACKEND /_dr/task/readDnsRefreshRequests ReadDnsRefreshRequestsAction POST y APP ADMIN -BACKEND /_dr/task/refreshDnsForAllDomains RefreshDnsForAllDomainsAction GET n APP ADMIN -BACKEND /_dr/task/refreshDnsOnHostRename RefreshDnsOnHostRenameAction POST n APP ADMIN -BACKEND /_dr/task/relockDomain RelockDomainAction POST y APP ADMIN -BACKEND /_dr/task/resaveAllEppResourcesPipeline ResaveAllEppResourcesPipelineAction GET n APP ADMIN -BACKEND /_dr/task/resaveEntity ResaveEntityAction POST n APP ADMIN -BACKEND /_dr/task/sendExpiringCertificateNotificationEmail SendExpiringCertificateNotificationEmailAction GET n APP ADMIN -BACKEND /_dr/task/syncGroupMembers SyncGroupMembersAction POST n APP ADMIN -BACKEND /_dr/task/syncRegistrarsSheet SyncRegistrarsSheetAction POST n APP ADMIN -BACKEND /_dr/task/tmchCrl TmchCrlAction POST y APP ADMIN -BACKEND /_dr/task/tmchDnl TmchDnlAction POST y APP ADMIN -BACKEND /_dr/task/tmchSmdrl TmchSmdrlAction POST y APP ADMIN -BACKEND /_dr/task/updateRegistrarRdapBaseUrls UpdateRegistrarRdapBaseUrlsAction GET y APP ADMIN -BACKEND /_dr/task/uploadBsaUnavailableNames UploadBsaUnavailableDomainsAction GET,POST n APP ADMIN -BACKEND /_dr/task/wipeOutContactHistoryPii WipeOutContactHistoryPiiAction GET n APP ADMIN -PUBAPI /_dr/whois WhoisAction POST n APP ADMIN -PUBAPI /check CheckApiAction GET n NONE PUBLIC -PUBAPI /rdap/autnum/(*) RdapAutnumAction GET,HEAD n NONE PUBLIC -PUBAPI /rdap/domain/(*) RdapDomainAction GET,HEAD n NONE PUBLIC -PUBAPI /rdap/domains RdapDomainSearchAction GET,HEAD n NONE PUBLIC -PUBAPI /rdap/entities RdapEntitySearchAction GET,HEAD n NONE PUBLIC -PUBAPI /rdap/entity/(*) RdapEntityAction GET,HEAD n NONE PUBLIC -PUBAPI /rdap/help(*) RdapHelpAction GET,HEAD n NONE PUBLIC -PUBAPI /rdap/ip/(*) RdapIpAction GET,HEAD n NONE PUBLIC -PUBAPI /rdap/nameserver/(*) RdapNameserverAction GET,HEAD n NONE PUBLIC -PUBAPI /rdap/nameservers RdapNameserverSearchAction GET,HEAD n NONE PUBLIC -PUBAPI /whois/(*) WhoisHttpAction GET n NONE PUBLIC -CONSOLE /console-api/domain ConsoleDomainGetAction GET n USER PUBLIC -CONSOLE /console-api/domain-list ConsoleDomainListAction GET n USER PUBLIC -CONSOLE /console-api/dum-download ConsoleDumDownloadAction GET n USER PUBLIC -CONSOLE /console-api/eppPassword ConsoleEppPasswordAction POST n USER PUBLIC -CONSOLE /console-api/ote ConsoleOteAction GET,POST n USER PUBLIC -CONSOLE /console-api/registrar ConsoleUpdateRegistrarAction POST n USER PUBLIC -CONSOLE /console-api/registrars RegistrarsAction GET,POST n USER PUBLIC -CONSOLE /console-api/registry-lock ConsoleRegistryLockAction GET,POST n USER PUBLIC -CONSOLE /console-api/registry-lock-verify ConsoleRegistryLockVerifyAction GET n USER PUBLIC -CONSOLE /console-api/settings/contacts ContactAction GET,POST n USER PUBLIC -CONSOLE /console-api/settings/security SecurityAction POST n USER PUBLIC -CONSOLE /console-api/settings/whois-fields WhoisRegistrarFieldsAction POST n USER PUBLIC -CONSOLE /console-api/userdata ConsoleUserDataAction GET n USER PUBLIC -CONSOLE /console-api/users ConsoleUsersAction GET,POST n USER PUBLIC +SERVICE PATH CLASS METHODS OK MIN USER_POLICY +FRONTEND /_dr/epp EppTlsAction POST n APP ADMIN +BACKEND /_dr/admin/createGroups CreateGroupsAction POST n APP ADMIN +BACKEND /_dr/admin/list/domains ListDomainsAction GET,POST n APP ADMIN +BACKEND /_dr/admin/list/hosts ListHostsAction GET,POST n APP ADMIN +BACKEND /_dr/admin/list/premiumLists ListPremiumListsAction GET,POST n APP ADMIN +BACKEND /_dr/admin/list/registrars ListRegistrarsAction GET,POST n APP ADMIN +BACKEND /_dr/admin/list/reservedLists ListReservedListsAction GET,POST n APP ADMIN +BACKEND /_dr/admin/list/tlds ListTldsAction GET,POST n APP ADMIN +BACKEND /_dr/admin/updateUserGroup UpdateUserGroupAction POST n APP ADMIN +BACKEND /_dr/admin/verifyOte VerifyOteAction POST n APP ADMIN +BACKEND /_dr/cron/fanout TldFanoutAction GET y APP ADMIN +BACKEND /_dr/epptool EppToolAction POST n APP ADMIN +BACKEND /_dr/loadtest LoadTestAction POST y APP ADMIN +BACKEND /_dr/task/brdaCopy BrdaCopyAction POST y APP ADMIN +BACKEND /_dr/task/bsaDownload BsaDownloadAction GET,POST n APP ADMIN +BACKEND /_dr/task/bsaRefresh BsaRefreshAction GET,POST n APP ADMIN +BACKEND /_dr/task/bsaValidate BsaValidateAction GET,POST n APP ADMIN +BACKEND /_dr/task/copyDetailReports CopyDetailReportsAction POST n APP ADMIN +BACKEND /_dr/task/deleteExpiredDomains DeleteExpiredDomainsAction GET n APP ADMIN +BACKEND /_dr/task/deleteLoadTestData DeleteLoadTestDataAction POST n APP ADMIN +BACKEND /_dr/task/deleteProberData DeleteProberDataAction POST n APP ADMIN +BACKEND /_dr/task/dnsRefresh RefreshDnsAction GET y APP ADMIN +BACKEND /_dr/task/executeCannedScript CannedScriptExecutionAction POST,GET y APP ADMIN +BACKEND /_dr/task/expandBillingRecurrences ExpandBillingRecurrencesAction GET n APP ADMIN +BACKEND /_dr/task/exportDomainLists ExportDomainListsAction POST n APP ADMIN +BACKEND /_dr/task/exportPremiumTerms ExportPremiumTermsAction POST n APP ADMIN +BACKEND /_dr/task/exportReservedTerms ExportReservedTermsAction POST n APP ADMIN +BACKEND /_dr/task/generateInvoices GenerateInvoicesAction POST n APP ADMIN +BACKEND /_dr/task/generateSpec11 GenerateSpec11ReportAction POST n APP ADMIN +BACKEND /_dr/task/generateZoneFiles GenerateZoneFilesAction POST n APP ADMIN +BACKEND /_dr/task/icannReportingStaging IcannReportingStagingAction POST n APP ADMIN +BACKEND /_dr/task/icannReportingUpload IcannReportingUploadAction POST n APP ADMIN +BACKEND /_dr/task/nordnUpload NordnUploadAction POST y APP ADMIN +BACKEND /_dr/task/nordnVerify NordnVerifyAction POST y APP ADMIN +BACKEND /_dr/task/publishDnsUpdates PublishDnsUpdatesAction POST y APP ADMIN +BACKEND /_dr/task/publishInvoices PublishInvoicesAction POST n APP ADMIN +BACKEND /_dr/task/publishSpec11 PublishSpec11ReportAction POST n APP ADMIN +BACKEND /_dr/task/rdeReport RdeReportAction POST n APP ADMIN +BACKEND /_dr/task/rdeStaging RdeStagingAction GET,POST n APP ADMIN +BACKEND /_dr/task/rdeUpload RdeUploadAction POST n APP ADMIN +BACKEND /_dr/task/readDnsRefreshRequests ReadDnsRefreshRequestsAction POST y APP ADMIN +BACKEND /_dr/task/refreshDnsForAllDomains RefreshDnsForAllDomainsAction GET n APP ADMIN +BACKEND /_dr/task/refreshDnsOnHostRename RefreshDnsOnHostRenameAction POST n APP ADMIN +BACKEND /_dr/task/relockDomain RelockDomainAction POST y APP ADMIN +BACKEND /_dr/task/resaveAllEppResourcesPipeline ResaveAllEppResourcesPipelineAction GET n APP ADMIN +BACKEND /_dr/task/resaveEntity ResaveEntityAction POST n APP ADMIN +BACKEND /_dr/task/sendExpiringCertificateNotificationEmail SendExpiringCertificateNotificationEmailAction GET n APP ADMIN +BACKEND /_dr/task/syncGroupMembers SyncGroupMembersAction POST n APP ADMIN +BACKEND /_dr/task/syncRegistrarsSheet SyncRegistrarsSheetAction POST n APP ADMIN +BACKEND /_dr/task/tmchCrl TmchCrlAction POST y APP ADMIN +BACKEND /_dr/task/tmchDnl TmchDnlAction POST y APP ADMIN +BACKEND /_dr/task/tmchSmdrl TmchSmdrlAction POST y APP ADMIN +BACKEND /_dr/task/updateRegistrarRdapBaseUrls UpdateRegistrarRdapBaseUrlsAction GET y APP ADMIN +BACKEND /_dr/task/uploadBsaUnavailableNames UploadBsaUnavailableDomainsAction GET,POST n APP ADMIN +BACKEND /_dr/task/wipeOutContactHistoryPii WipeOutContactHistoryPiiAction GET n APP ADMIN +PUBAPI /_dr/whois WhoisAction POST n APP ADMIN +PUBAPI /check CheckApiAction GET n NONE PUBLIC +PUBAPI /rdap/autnum/(*) RdapAutnumAction GET,HEAD n NONE PUBLIC +PUBAPI /rdap/domain/(*) RdapDomainAction GET,HEAD n NONE PUBLIC +PUBAPI /rdap/domains RdapDomainSearchAction GET,HEAD n NONE PUBLIC +PUBAPI /rdap/entities RdapEntitySearchAction GET,HEAD n NONE PUBLIC +PUBAPI /rdap/entity/(*) RdapEntityAction GET,HEAD n NONE PUBLIC +PUBAPI /rdap/help(*) RdapHelpAction GET,HEAD n NONE PUBLIC +PUBAPI /rdap/ip/(*) RdapIpAction GET,HEAD n NONE PUBLIC +PUBAPI /rdap/nameserver/(*) RdapNameserverAction GET,HEAD n NONE PUBLIC +PUBAPI /rdap/nameservers RdapNameserverSearchAction GET,HEAD n NONE PUBLIC +PUBAPI /whois/(*) WhoisHttpAction GET n NONE PUBLIC +CONSOLE /console-api/domain ConsoleDomainGetAction GET n USER PUBLIC +CONSOLE /console-api/domain-list ConsoleDomainListAction GET n USER PUBLIC +CONSOLE /console-api/dum-download ConsoleDumDownloadAction GET n USER PUBLIC +CONSOLE /console-api/eppPassword ConsoleEppPasswordAction POST n USER PUBLIC +CONSOLE /console-api/ote ConsoleOteAction GET,POST n USER PUBLIC +CONSOLE /console-api/registrar ConsoleUpdateRegistrarAction POST n USER PUBLIC +CONSOLE /console-api/registrars RegistrarsAction GET,POST n USER PUBLIC +CONSOLE /console-api/registry-lock ConsoleRegistryLockAction GET,POST n USER PUBLIC +CONSOLE /console-api/registry-lock-verify ConsoleRegistryLockVerifyAction GET n USER PUBLIC +CONSOLE /console-api/settings/contacts ContactAction GET,POST n USER PUBLIC +CONSOLE /console-api/settings/security SecurityAction POST n USER PUBLIC +CONSOLE /console-api/settings/whois-fields WhoisRegistrarFieldsAction POST n USER PUBLIC +CONSOLE /console-api/userdata ConsoleUserDataAction GET n USER PUBLIC +CONSOLE /console-api/users ConsoleUsersAction GET,POST,DELETE n USER PUBLIC \ No newline at end of file