1
0
mirror of https://github.com/google/nomulus synced 2026-01-03 11:45:39 +00:00

Add delete user to the console (#2603)

* Add delete user to the console

* Add delete user to the console

* Add delete user to the console
This commit is contained in:
Pavlo Tkach
2024-11-08 13:20:01 -05:00
committed by GitHub
parent ae61cd443d
commit 35f95bbbe4
14 changed files with 584 additions and 167 deletions

View File

@@ -172,6 +172,14 @@ export class BackendService {
.pipe(catchError((err) => this.errorCatcher<User>(err))); .pipe(catchError((err) => this.errorCatcher<User>(err)));
} }
deleteUser(registrarId: string, emailAddress: string): Observable<any> {
return this.http
.delete<any>(`/console-api/users?registrarId=${registrarId}`, {
body: JSON.stringify({ emailAddress }),
})
.pipe(catchError((err) => this.errorCatcher<any>(err)));
}
getUserData(): Observable<UserData> { getUserData(): Observable<UserData> {
return this.http return this.http
.get<UserData>('/console-api/userdata') .get<UserData>('/console-api/userdata')

View File

@@ -0,0 +1,79 @@
<div class="console-app__user-details">
@if(isNewUser) {
<h1 class="mat-headline-4">
{{ userDetails.emailAddress + " succesfully created" }}
</h1>
} @else {
<h1 class="mat-headline-4">User details</h1>
}
<mat-divider></mat-divider>
<div>
<div class="console-app__user-details-controls">
<button
mat-icon-button
aria-label="Back to users list"
(click)="goBack()"
>
<mat-icon>arrow_back</mat-icon>
</button>
<div class="spacer"></div>
<button
mat-icon-button
aria-label="Delete User"
(click)="deleteUser()"
[disabled]="isLoading"
>
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
<p *ngIf="isLoading">
<mat-progress-bar mode="query"></mat-progress-bar>
</p>
<mat-card appearance="outlined">
<mat-card-content>
<mat-list role="list">
<mat-list-item role="listitem">
<h2>User details</h2>
</mat-list-item>
<mat-divider></mat-divider>
<mat-list-item role="listitem">
<span class="console-app__list-key">User email</span>
<span class="console-app__list-value">{{
userDetails.emailAddress
}}</span>
</mat-list-item>
<mat-divider></mat-divider>
<mat-list-item role="listitem">
<span class="console-app__list-key">User role</span>
<span class="console-app__list-value">{{
roleToDescription(userDetails.role)
}}</span>
</mat-list-item>
@if (userDetails.password) {
<mat-divider></mat-divider>
<mat-list-item role="listitem">
<span class="console-app__list-key">Password</span>
<span
class="console-app__list-value console-app__user-details-password"
>
<input
[type]="isPasswordVisible ? 'text' : 'password'"
[value]="userDetails.password"
disabled
/>
<button
mat-button
aria-label="Show password"
(click)="isPasswordVisible = !isPasswordVisible"
>
{{ isPasswordVisible ? "Hide" : "View" }} password
</button>
</span>
</mat-list-item>
}
</mat-list>
</mat-card-content>
</mat-card>
</div>

View File

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

View File

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

View File

@@ -1,5 +1,11 @@
<app-selected-registrar-wrapper> <app-selected-registrar-wrapper>
@if (!isLoading) { @if(isLoading) {
<div class="console-app__users-spinner">
<mat-spinner />
</div>
} @else if(usersService.currentlyOpenUserEmail()) {
<app-user-edit></app-user-edit>
} @else {
<div class="console-app__users"> <div class="console-app__users">
<div class="console-app__users-header"> <div class="console-app__users-header">
<h1 class="mat-headline-4">Users</h1> <h1 class="mat-headline-4">Users</h1>
@@ -32,12 +38,11 @@
></mat-cell> ></mat-cell>
</ng-container> </ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row> <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row> <mat-row
*matRowDef="let row; columns: displayedColumns"
(click)="openDetails(row.emailAddress)"
></mat-row>
</mat-table> </mat-table>
</div> </div>
} @else {
<div class="console-app__users-spinner">
<mat-spinner />
</div>
} }
</app-selected-registrar-wrapper> </app-selected-registrar-wrapper>

View File

@@ -22,15 +22,8 @@ import { SelectedRegistrarModule } from '../app.module';
import { MaterialModule } from '../material.module'; import { MaterialModule } from '../material.module';
import { RegistrarService } from '../registrar/registrar.service'; import { RegistrarService } from '../registrar/registrar.service';
import { SnackBarModule } from '../snackbar.module'; import { SnackBarModule } from '../snackbar.module';
import { User, UsersService } from './users.service'; import { UserEditComponent } from './userEdit.component';
import { roleToDescription, 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';
};
export const columns = [ export const columns = [
{ {
@@ -55,6 +48,7 @@ export const columns = [
SnackBarModule, SnackBarModule,
CommonModule, CommonModule,
SelectedRegistrarModule, SelectedRegistrarModule,
UserEditComponent,
], ],
providers: [UsersService], providers: [UsersService],
}) })
@@ -92,6 +86,7 @@ export class UsersComponent {
this.usersService.fetchUsers().subscribe({ this.usersService.fetchUsers().subscribe({
error: (err: HttpErrorResponse) => { error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error || err.message); this._snackBar.open(err.error || err.message);
this.isLoading = false;
}, },
complete: () => { complete: () => {
this.isLoading = false; this.isLoading = false;
@@ -102,21 +97,17 @@ export class UsersComponent {
createNewUser() { createNewUser() {
this.isLoading = true; this.isLoading = true;
this.usersService.createNewUser().subscribe({ this.usersService.createNewUser().subscribe({
next: (newUser) => {
this._snackBar.open(
`New user with email ${newUser.emailAddress} has been created.`,
'',
{
duration: 2000,
}
);
},
error: (err: HttpErrorResponse) => { error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error || err.message); this._snackBar.open(err.error || err.message);
this.isLoading = false;
}, },
complete: () => { complete: () => {
this.isLoading = false; this.isLoading = false;
}, },
}); });
} }
openDetails(emailAddress: string) {
this.usersService.currentlyOpenUserEmail.set(emailAddress);
}
} }

View File

@@ -17,19 +17,29 @@ import { tap } from 'rxjs';
import { RegistrarService } from '../registrar/registrar.service'; import { RegistrarService } from '../registrar/registrar.service';
import { BackendService } from '../shared/services/backend.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 { export interface CreateAutoTimestamp {
creationTime: string; creationTime: string;
} }
export interface User { export interface User {
emailAddress: String; emailAddress: string;
role: String; role: string;
password?: String; password?: string;
} }
@Injectable() @Injectable()
export class UsersService { export class UsersService {
users = signal<User[]>([]); users = signal<User[]>([]);
currentlyOpenUserEmail = signal<string>('');
isNewUser: boolean = false;
constructor( constructor(
private backendService: BackendService, private backendService: BackendService,
@@ -52,7 +62,15 @@ export class UsersService {
.pipe( .pipe(
tap((newUser: User) => { tap((newUser: User) => {
this.users.set([...this.users(), newUser]); 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()));
}
} }

View File

@@ -33,7 +33,8 @@ public @interface Action {
enum Method { enum Method {
GET, GET,
HEAD, HEAD,
POST POST,
DELETE
} }
interface Service { interface Service {

View File

@@ -16,7 +16,9 @@ package google.registry.ui.server.console;
import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet; 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.GET;
import static google.registry.request.Action.Method.HEAD;
import static google.registry.request.Action.Method.POST; import static google.registry.request.Action.Method.POST;
import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN; import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
@@ -75,13 +77,19 @@ public abstract class ConsoleApiAction implements Runnable {
return; return;
} }
User user = consoleApiParams.authResult().user().get(); User user = consoleApiParams.authResult().user().get();
String requestMethod = consoleApiParams.request().getMethod();
try { try {
if (consoleApiParams.request().getMethod().equals(GET.toString())) { if (requestMethod.equals(GET.toString())) {
getHandler(user); getHandler(user);
} else if (requestMethod.equals(HEAD.toString())) {
headHandler(user);
} else { } else {
if (verifyXSRF(user)) { if (verifyXSRF(user)) {
postHandler(user); if (requestMethod.equals(DELETE.toString())) {
deleteHandler(user);
} else {
postHandler(user);
}
} }
} }
} catch (ConsolePermissionForbiddenException e) { } catch (ConsolePermissionForbiddenException e) {
@@ -113,6 +121,14 @@ public abstract class ConsoleApiAction implements Runnable {
throw new UnsupportedOperationException("Console API GET handler not implemented"); 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) { protected void setFailedResponse(String message, int code) {
consoleApiParams.response().setStatus(code); consoleApiParams.response().setStatus(code);
consoleApiParams.response().setPayload(message); consoleApiParams.response().setPayload(message);

View File

@@ -36,6 +36,7 @@ import google.registry.ui.server.SendEmailUtils;
import google.registry.ui.server.console.ConsoleEppPasswordAction.EppPasswordData; import google.registry.ui.server.console.ConsoleEppPasswordAction.EppPasswordData;
import google.registry.ui.server.console.ConsoleOteAction.OteCreateData; import google.registry.ui.server.console.ConsoleOteAction.OteCreateData;
import google.registry.ui.server.console.ConsoleRegistryLockAction.ConsoleRegistryLockPostInput; import google.registry.ui.server.console.ConsoleRegistryLockAction.ConsoleRegistryLockPostInput;
import google.registry.ui.server.console.ConsoleUsersAction.UserDeleteData;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import java.util.Optional; import java.util.Optional;
import org.joda.time.DateTime; import org.joda.time.DateTime;
@@ -245,6 +246,13 @@ public final class ConsoleModule {
return payload.map(s -> gson.fromJson(s, EppPasswordData.class)); return payload.map(s -> gson.fromJson(s, EppPasswordData.class));
} }
@Provides
@Parameter("userDeleteData")
public static Optional<UserDeleteData> provideUserDeleteData(
Gson gson, @OptionalJsonPayload Optional<JsonElement> payload) {
return payload.map(s -> gson.fromJson(s, UserDeleteData.class));
}
@Provides @Provides
@Parameter("oteCreateData") @Parameter("oteCreateData")
public static Optional<OteCreateData> provideOteCreateData( public static Optional<OteCreateData> provideOteCreateData(

View File

@@ -14,11 +14,14 @@
package google.registry.ui.server.console; package google.registry.ui.server.console;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.model.console.RegistrarRole.ACCOUNT_MANAGER; import static google.registry.model.console.RegistrarRole.ACCOUNT_MANAGER;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm; 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.GET;
import static google.registry.request.Action.Method.POST; 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_FORBIDDEN;
import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static jakarta.servlet.http.HttpServletResponse.SC_OK; 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.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson; 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.ConsolePermission;
import google.registry.model.console.User; import google.registry.model.console.User;
import google.registry.model.console.UserRoles; 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.HttpException.BadRequestException;
import google.registry.request.Parameter; import google.registry.request.Parameter;
import google.registry.request.auth.Auth; import google.registry.request.auth.Auth;
import google.registry.tools.IamClient;
import google.registry.util.StringGenerator; import google.registry.util.StringGenerator;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.IntStream;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
@@ -50,36 +57,42 @@ import javax.inject.Named;
service = Action.GaeService.DEFAULT, service = Action.GaeService.DEFAULT,
gkeService = GkeService.CONSOLE, gkeService = GkeService.CONSOLE,
path = ConsoleUsersAction.PATH, path = ConsoleUsersAction.PATH,
method = {GET, POST}, method = {GET, POST, DELETE},
auth = Auth.AUTH_PUBLIC_LOGGED_IN) auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public class ConsoleUsersAction extends ConsoleApiAction { public class ConsoleUsersAction extends ConsoleApiAction {
static final String PATH = "/console-api/users"; static final String PATH = "/console-api/users";
private static final int PASSWORD_LENGTH = 16; private static final int PASSWORD_LENGTH = 16;
private static final Splitter EMAIL_SPLITTER = Splitter.on('@').trimResults(); private static final Splitter EMAIL_SPLITTER = Splitter.on('@').trimResults();
private final Gson gson; private final Gson gson;
private final String registrarId; private final String registrarId;
private final Directory directory; private final Directory directory;
private final StringGenerator passwordGenerator; private final StringGenerator passwordGenerator;
private final Optional<UserDeleteData> userDeleteData;
private final Optional<String> maybeGroupEmailAddress;
private final IamClient iamClient;
private final String gSuiteDomainName;
@Inject @Inject
public ConsoleUsersAction( public ConsoleUsersAction(
ConsoleApiParams consoleApiParams, ConsoleApiParams consoleApiParams,
Gson gson, Gson gson,
Directory directory, Directory directory,
IamClient iamClient,
@Config("gSuiteDomainName") String gSuiteDomainName,
@Config("gSuiteConsoleUserGroupEmailAddress") Optional<String> maybeGroupEmailAddress,
@Named("base58StringGenerator") StringGenerator passwordGenerator, @Named("base58StringGenerator") StringGenerator passwordGenerator,
@Parameter("userDeleteData") Optional<UserDeleteData> userDeleteData,
@Parameter("registrarId") String registrarId) { @Parameter("registrarId") String registrarId) {
super(consoleApiParams); super(consoleApiParams);
this.gson = gson; this.gson = gson;
this.registrarId = registrarId; this.registrarId = registrarId;
this.directory = directory; this.directory = directory;
this.passwordGenerator = passwordGenerator; this.passwordGenerator = passwordGenerator;
} this.userDeleteData = userDeleteData;
this.maybeGroupEmailAddress = maybeGroupEmailAddress;
private static String generateNewEmailAddress(User user, String increment) { this.iamClient = iamClient;
List<String> emailParts = EMAIL_SPLITTER.splitToList(user.getEmailAddress()); this.gSuiteDomainName = gSuiteDomainName;
return String.format("%s-%s@%s", emailParts.get(0), increment, emailParts.get(1));
} }
@Override @Override
@@ -87,7 +100,7 @@ public class ConsoleUsersAction extends ConsoleApiAction {
// Temporary flag while testing // Temporary flag while testing
if (user.getUserRoles().isAdmin()) { if (user.getUserRoles().isAdmin()) {
checkPermission(user, registrarId, ConsolePermission.MANAGE_USERS); checkPermission(user, registrarId, ConsolePermission.MANAGE_USERS);
tm().transact(() -> runInTransaction(user)); tm().transact(() -> runCreateInTransaction());
} else { } else {
consoleApiParams.response().setStatus(SC_FORBIDDEN); consoleApiParams.response().setStatus(SC_FORBIDDEN);
} }
@@ -97,8 +110,7 @@ public class ConsoleUsersAction extends ConsoleApiAction {
protected void getHandler(User user) { protected void getHandler(User user) {
checkPermission(user, registrarId, ConsolePermission.MANAGE_USERS); checkPermission(user, registrarId, ConsolePermission.MANAGE_USERS);
List<ImmutableMap> users = List<ImmutableMap> users =
getAllUsers().stream() getAllRegistrarUsers(registrarId).stream()
.filter(u -> u.getUserRoles().getRegistrarRoles().containsKey(registrarId))
.map( .map(
u -> u ->
ImmutableMap.of( ImmutableMap.of(
@@ -112,24 +124,71 @@ public class ConsoleUsersAction extends ConsoleApiAction {
consoleApiParams.response().setStatus(SC_OK); consoleApiParams.response().setStatus(SC_OK);
} }
private void runInTransaction(User user) throws IOException { @Override
String nextAvailableIncrement = protected void deleteHandler(User user) {
Stream.of("1", "2", "3") // Temporary flag while testing
.filter( if (user.getUserRoles().isAdmin()) {
increment -> checkPermission(user, registrarId, ConsolePermission.MANAGE_USERS);
tm().loadByKeyIfPresent( tm().transact(() -> runDeleteInTransaction());
VKey.create(User.class, generateNewEmailAddress(user, increment))) } else {
.isEmpty()) 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<User> 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<User> 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() .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 = com.google.api.services.directory.model.User newUser =
new com.google.api.services.directory.model.User(); new com.google.api.services.directory.model.User();
newUser.setName( 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.setPassword(passwordGenerator.createString(PASSWORD_LENGTH));
newUser.setPrimaryEmail(generateNewEmailAddress(user, nextAvailableIncrement)); newUser.setPrimaryEmail(nextAvailableEmail);
try { try {
directory.users().insert(newUser).execute(); directory.users().insert(newUser).execute();
@@ -146,8 +205,10 @@ public class ConsoleUsersAction extends ConsoleApiAction {
User.Builder builder = User.Builder builder =
new User.Builder().setUserRoles(userRoles).setEmailAddress(newUser.getPrimaryEmail()); new User.Builder().setUserRoles(userRoles).setEmailAddress(newUser.getPrimaryEmail());
tm().put(builder.build()); tm().put(builder.build());
User.grantIapPermission(
nextAvailableEmail, maybeGroupEmailAddress, cloudTasksUtils, null, iamClient);
consoleApiParams.response().setStatus(SC_OK); consoleApiParams.response().setStatus(SC_CREATED);
consoleApiParams consoleApiParams
.response() .response()
.setPayload( .setPayload(
@@ -161,11 +222,13 @@ public class ConsoleUsersAction extends ConsoleApiAction {
ACCOUNT_MANAGER))); ACCOUNT_MANAGER)));
} }
private ImmutableList<User> getAllUsers() { private ImmutableList<User> getAllRegistrarUsers(String registrarId) {
return tm().transact( return tm().transact(
() -> () ->
tm().loadAllOf(User.class).stream() tm().loadAllOf(User.class).stream()
.filter(u -> !u.getUserRoles().getRegistrarRoles().isEmpty()) .filter(u -> u.getUserRoles().getRegistrarRoles().containsKey(registrarId))
.collect(toImmutableList())); .collect(toImmutableList()));
} }
public record UserDeleteData(@Expose String userEmail) {}
} }

View File

@@ -16,6 +16,8 @@ package google.registry.ui.server.console;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST; 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 jakarta.servlet.http.HttpServletResponse.SC_OK;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; 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;
import com.google.api.services.directory.Directory.Users; 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.api.services.directory.Directory.Users.Insert;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; 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.RegistrarRole;
import google.registry.model.console.User; import google.registry.model.console.User;
import google.registry.model.console.UserRoles; import google.registry.model.console.UserRoles;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.request.RequestModule; import google.registry.request.RequestModule;
import google.registry.request.auth.AuthResult; import google.registry.request.auth.AuthResult;
import google.registry.testing.CloudTasksHelper;
import google.registry.testing.ConsoleApiParamsUtils; import google.registry.testing.ConsoleApiParamsUtils;
import google.registry.testing.DatabaseHelper; import google.registry.testing.DatabaseHelper;
import google.registry.testing.DeterministicStringGenerator; import google.registry.testing.DeterministicStringGenerator;
import google.registry.testing.FakeResponse; import google.registry.testing.FakeResponse;
import google.registry.tools.IamClient;
import google.registry.ui.server.console.ConsoleUsersAction.UserDeleteData;
import google.registry.util.StringGenerator; import google.registry.util.StringGenerator;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.util.Optional; 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.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.extension.RegisterExtension;
@@ -53,6 +62,10 @@ class ConsoleUsersActionTest {
private final Directory directory = mock(Directory.class); private final Directory directory = mock(Directory.class);
private final Users users = mock(Users.class); private final Users users = mock(Users.class);
private final Insert insert = mock(Insert.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 = private StringGenerator passwordGenerator =
new DeterministicStringGenerator("abcdefghijklmnopqrstuvwxyz"); new DeterministicStringGenerator("abcdefghijklmnopqrstuvwxyz");
@@ -112,7 +125,10 @@ class ConsoleUsersActionTest {
AuthResult authResult = AuthResult.createUser(user); AuthResult authResult = AuthResult.createUser(user);
ConsoleUsersAction action = ConsoleUsersAction action =
createAction(Optional.of(ConsoleApiParamsUtils.createFake(authResult)), Optional.of("GET")); createAction(
Optional.of(ConsoleApiParamsUtils.createFake(authResult)),
Optional.of("GET"),
Optional.empty());
action.run(); action.run();
var response = ((FakeResponse) consoleApiParams.response()); var response = ((FakeResponse) consoleApiParams.response());
assertThat(response.getPayload()) assertThat(response.getPayload())
@@ -134,7 +150,10 @@ class ConsoleUsersActionTest {
AuthResult authResult = AuthResult.createUser(user); AuthResult authResult = AuthResult.createUser(user);
ConsoleUsersAction action = ConsoleUsersAction action =
createAction(Optional.of(ConsoleApiParamsUtils.createFake(authResult)), Optional.of("GET")); createAction(
Optional.of(ConsoleApiParamsUtils.createFake(authResult)),
Optional.of("GET"),
Optional.empty());
action.run(); action.run();
var response = ((FakeResponse) consoleApiParams.response()); var response = ((FakeResponse) consoleApiParams.response());
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN); assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN);
@@ -146,43 +165,141 @@ class ConsoleUsersActionTest {
AuthResult authResult = AuthResult.createUser(user); AuthResult authResult = AuthResult.createUser(user);
ConsoleUsersAction action = ConsoleUsersAction action =
createAction( 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(directory.users()).thenReturn(users);
when(users.insert(any(com.google.api.services.directory.model.User.class))).thenReturn(insert); when(users.insert(any(com.google.api.services.directory.model.User.class))).thenReturn(insert);
action.run(); action.run();
var response = ((FakeResponse) consoleApiParams.response()); var response = ((FakeResponse) consoleApiParams.response());
assertThat(response.getStatus()).isEqualTo(SC_OK); assertThat(response.getStatus()).isEqualTo(SC_CREATED);
assertThat(response.getPayload()) assertThat(response.getPayload())
.contains( .contains(
"{\"password\":\"abcdefghijklmnop\",\"emailAddress\":\"email-1@email.com\",\"role\":\"ACCOUNT_MANAGER\"}"); "{\"password\":\"abcdefghijklmnop\",\"emailAddress\":\"TheRegistrar-user1@email.com\",\"role\":\"ACCOUNT_MANAGER\"}");
} }
@Test @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"); 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); AuthResult authResult = AuthResult.createUser(user);
ConsoleUsersAction action = ConsoleUsersAction action =
createAction( 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(directory.users()).thenReturn(users);
when(users.insert(any(com.google.api.services.directory.model.User.class))).thenReturn(insert); when(users.insert(any(com.google.api.services.directory.model.User.class))).thenReturn(insert);
action.run(); action.run();
var response = ((FakeResponse) consoleApiParams.response()); var response = ((FakeResponse) consoleApiParams.response());
assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); 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( private ConsoleUsersAction createAction(
Optional<ConsoleApiParams> maybeConsoleApiParams, Optional<String> method) Optional<ConsoleApiParams> maybeConsoleApiParams,
Optional<String> method,
Optional<UserDeleteData> userDeleteData)
throws IOException { throws IOException {
consoleApiParams = consoleApiParams =
maybeConsoleApiParams.orElseGet( maybeConsoleApiParams.orElseGet(
() -> ConsoleApiParamsUtils.createFake(AuthResult.NOT_AUTHENTICATED)); () -> ConsoleApiParamsUtils.createFake(AuthResult.NOT_AUTHENTICATED));
when(consoleApiParams.request().getMethod()).thenReturn(method.orElse("GET")); when(consoleApiParams.request().getMethod()).thenReturn(method.orElse("GET"));
return new ConsoleUsersAction( return new ConsoleUsersAction(
consoleApiParams, GSON, directory, passwordGenerator, "TheRegistrar"); consoleApiParams,
GSON,
directory,
iamClient,
"email.com",
Optional.of("someRandomString"),
passwordGenerator,
userDeleteData,
"TheRegistrar");
} }
} }

View File

@@ -1,16 +1,16 @@
SERVICE PATH CLASS METHODS OK MIN USER_POLICY SERVICE PATH CLASS METHODS OK MIN USER_POLICY
FRONTEND /_dr/epp EppTlsAction POST n APP ADMIN FRONTEND /_dr/epp EppTlsAction POST n APP ADMIN
CONSOLE /console-api/domain ConsoleDomainGetAction GET n USER PUBLIC CONSOLE /console-api/domain ConsoleDomainGetAction GET n USER PUBLIC
CONSOLE /console-api/domain-list ConsoleDomainListAction 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/dum-download ConsoleDumDownloadAction GET n USER PUBLIC
CONSOLE /console-api/eppPassword ConsoleEppPasswordAction POST 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/ote ConsoleOteAction GET,POST n USER PUBLIC
CONSOLE /console-api/registrar ConsoleUpdateRegistrarAction 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/registrars RegistrarsAction GET,POST n USER PUBLIC
CONSOLE /console-api/registry-lock ConsoleRegistryLockAction 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/registry-lock-verify ConsoleRegistryLockVerifyAction GET n USER PUBLIC
CONSOLE /console-api/settings/contacts ContactAction GET,POST 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/security SecurityAction POST n USER PUBLIC
CONSOLE /console-api/settings/whois-fields WhoisRegistrarFieldsAction 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/userdata ConsoleUserDataAction GET n USER PUBLIC
CONSOLE /console-api/users ConsoleUsersAction GET,POST n USER PUBLIC CONSOLE /console-api/users ConsoleUsersAction GET,POST,DELETE n USER PUBLIC

View File

@@ -1,82 +1,82 @@
SERVICE PATH CLASS METHODS OK MIN USER_POLICY SERVICE PATH CLASS METHODS OK MIN USER_POLICY
FRONTEND /_dr/epp EppTlsAction POST n APP ADMIN FRONTEND /_dr/epp EppTlsAction POST n APP ADMIN
BACKEND /_dr/admin/createGroups CreateGroupsAction 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/domains ListDomainsAction GET,POST n APP ADMIN
BACKEND /_dr/admin/list/hosts ListHostsAction 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/premiumLists ListPremiumListsAction GET,POST n APP ADMIN
BACKEND /_dr/admin/list/registrars ListRegistrarsAction 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/reservedLists ListReservedListsAction GET,POST n APP ADMIN
BACKEND /_dr/admin/list/tlds ListTldsAction 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/updateUserGroup UpdateUserGroupAction POST n APP ADMIN
BACKEND /_dr/admin/verifyOte VerifyOteAction POST n APP ADMIN BACKEND /_dr/admin/verifyOte VerifyOteAction POST n APP ADMIN
BACKEND /_dr/cron/fanout TldFanoutAction GET y APP ADMIN BACKEND /_dr/cron/fanout TldFanoutAction GET y APP ADMIN
BACKEND /_dr/epptool EppToolAction POST n APP ADMIN BACKEND /_dr/epptool EppToolAction POST n APP ADMIN
BACKEND /_dr/loadtest LoadTestAction POST y APP ADMIN BACKEND /_dr/loadtest LoadTestAction POST y APP ADMIN
BACKEND /_dr/task/brdaCopy BrdaCopyAction 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/bsaDownload BsaDownloadAction GET,POST n APP ADMIN
BACKEND /_dr/task/bsaRefresh BsaRefreshAction 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/bsaValidate BsaValidateAction GET,POST n APP ADMIN
BACKEND /_dr/task/copyDetailReports CopyDetailReportsAction 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/deleteExpiredDomains DeleteExpiredDomainsAction GET n APP ADMIN
BACKEND /_dr/task/deleteLoadTestData DeleteLoadTestDataAction POST n APP ADMIN BACKEND /_dr/task/deleteLoadTestData DeleteLoadTestDataAction POST n APP ADMIN
BACKEND /_dr/task/deleteProberData DeleteProberDataAction 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/dnsRefresh RefreshDnsAction GET y APP ADMIN
BACKEND /_dr/task/executeCannedScript CannedScriptExecutionAction POST,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/expandBillingRecurrences ExpandBillingRecurrencesAction GET n APP ADMIN
BACKEND /_dr/task/exportDomainLists ExportDomainListsAction POST n APP ADMIN BACKEND /_dr/task/exportDomainLists ExportDomainListsAction POST n APP ADMIN
BACKEND /_dr/task/exportPremiumTerms ExportPremiumTermsAction 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/exportReservedTerms ExportReservedTermsAction POST n APP ADMIN
BACKEND /_dr/task/generateInvoices GenerateInvoicesAction 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/generateSpec11 GenerateSpec11ReportAction POST n APP ADMIN
BACKEND /_dr/task/generateZoneFiles GenerateZoneFilesAction 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/icannReportingStaging IcannReportingStagingAction POST n APP ADMIN
BACKEND /_dr/task/icannReportingUpload IcannReportingUploadAction 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/nordnUpload NordnUploadAction POST y APP ADMIN
BACKEND /_dr/task/nordnVerify NordnVerifyAction 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/publishDnsUpdates PublishDnsUpdatesAction POST y APP ADMIN
BACKEND /_dr/task/publishInvoices PublishInvoicesAction POST n APP ADMIN BACKEND /_dr/task/publishInvoices PublishInvoicesAction POST n APP ADMIN
BACKEND /_dr/task/publishSpec11 PublishSpec11ReportAction 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/rdeReport RdeReportAction POST n APP ADMIN
BACKEND /_dr/task/rdeStaging RdeStagingAction GET,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/rdeUpload RdeUploadAction POST n APP ADMIN
BACKEND /_dr/task/readDnsRefreshRequests ReadDnsRefreshRequestsAction POST y APP ADMIN BACKEND /_dr/task/readDnsRefreshRequests ReadDnsRefreshRequestsAction POST y APP ADMIN
BACKEND /_dr/task/refreshDnsForAllDomains RefreshDnsForAllDomainsAction GET n APP ADMIN BACKEND /_dr/task/refreshDnsForAllDomains RefreshDnsForAllDomainsAction GET n APP ADMIN
BACKEND /_dr/task/refreshDnsOnHostRename RefreshDnsOnHostRenameAction POST n APP ADMIN BACKEND /_dr/task/refreshDnsOnHostRename RefreshDnsOnHostRenameAction POST n APP ADMIN
BACKEND /_dr/task/relockDomain RelockDomainAction POST y APP ADMIN BACKEND /_dr/task/relockDomain RelockDomainAction POST y APP ADMIN
BACKEND /_dr/task/resaveAllEppResourcesPipeline ResaveAllEppResourcesPipelineAction GET n APP ADMIN BACKEND /_dr/task/resaveAllEppResourcesPipeline ResaveAllEppResourcesPipelineAction GET n APP ADMIN
BACKEND /_dr/task/resaveEntity ResaveEntityAction POST n APP ADMIN BACKEND /_dr/task/resaveEntity ResaveEntityAction POST n APP ADMIN
BACKEND /_dr/task/sendExpiringCertificateNotificationEmail SendExpiringCertificateNotificationEmailAction GET n APP ADMIN BACKEND /_dr/task/sendExpiringCertificateNotificationEmail SendExpiringCertificateNotificationEmailAction GET n APP ADMIN
BACKEND /_dr/task/syncGroupMembers SyncGroupMembersAction POST n APP ADMIN BACKEND /_dr/task/syncGroupMembers SyncGroupMembersAction POST n APP ADMIN
BACKEND /_dr/task/syncRegistrarsSheet SyncRegistrarsSheetAction 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/tmchCrl TmchCrlAction POST y APP ADMIN
BACKEND /_dr/task/tmchDnl TmchDnlAction 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/tmchSmdrl TmchSmdrlAction POST y APP ADMIN
BACKEND /_dr/task/updateRegistrarRdapBaseUrls UpdateRegistrarRdapBaseUrlsAction GET 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/uploadBsaUnavailableNames UploadBsaUnavailableDomainsAction GET,POST n APP ADMIN
BACKEND /_dr/task/wipeOutContactHistoryPii WipeOutContactHistoryPiiAction GET n APP ADMIN BACKEND /_dr/task/wipeOutContactHistoryPii WipeOutContactHistoryPiiAction GET n APP ADMIN
PUBAPI /_dr/whois WhoisAction POST n APP ADMIN PUBAPI /_dr/whois WhoisAction POST n APP ADMIN
PUBAPI /check CheckApiAction GET n NONE PUBLIC PUBAPI /check CheckApiAction GET n NONE PUBLIC
PUBAPI /rdap/autnum/(*) RdapAutnumAction GET,HEAD n NONE PUBLIC PUBAPI /rdap/autnum/(*) RdapAutnumAction GET,HEAD n NONE PUBLIC
PUBAPI /rdap/domain/(*) RdapDomainAction 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/domains RdapDomainSearchAction GET,HEAD n NONE PUBLIC
PUBAPI /rdap/entities RdapEntitySearchAction 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/entity/(*) RdapEntityAction GET,HEAD n NONE PUBLIC
PUBAPI /rdap/help(*) RdapHelpAction 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/ip/(*) RdapIpAction GET,HEAD n NONE PUBLIC
PUBAPI /rdap/nameserver/(*) RdapNameserverAction GET,HEAD n NONE PUBLIC PUBAPI /rdap/nameserver/(*) RdapNameserverAction GET,HEAD n NONE PUBLIC
PUBAPI /rdap/nameservers RdapNameserverSearchAction GET,HEAD n NONE PUBLIC PUBAPI /rdap/nameservers RdapNameserverSearchAction GET,HEAD n NONE PUBLIC
PUBAPI /whois/(*) WhoisHttpAction GET n NONE PUBLIC PUBAPI /whois/(*) WhoisHttpAction GET n NONE PUBLIC
CONSOLE /console-api/domain ConsoleDomainGetAction GET n USER PUBLIC CONSOLE /console-api/domain ConsoleDomainGetAction GET n USER PUBLIC
CONSOLE /console-api/domain-list ConsoleDomainListAction 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/dum-download ConsoleDumDownloadAction GET n USER PUBLIC
CONSOLE /console-api/eppPassword ConsoleEppPasswordAction POST 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/ote ConsoleOteAction GET,POST n USER PUBLIC
CONSOLE /console-api/registrar ConsoleUpdateRegistrarAction 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/registrars RegistrarsAction GET,POST n USER PUBLIC
CONSOLE /console-api/registry-lock ConsoleRegistryLockAction 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/registry-lock-verify ConsoleRegistryLockVerifyAction GET n USER PUBLIC
CONSOLE /console-api/settings/contacts ContactAction GET,POST 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/security SecurityAction POST n USER PUBLIC
CONSOLE /console-api/settings/whois-fields WhoisRegistrarFieldsAction 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/userdata ConsoleUserDataAction GET n USER PUBLIC
CONSOLE /console-api/users ConsoleUsersAction GET,POST n USER PUBLIC CONSOLE /console-api/users ConsoleUsersAction GET,POST,DELETE n USER PUBLIC