1
0
mirror of https://github.com/google/nomulus synced 2026-05-18 05:41:51 +00:00

Compare commits

...

11 Commits

Author SHA1 Message Date
Pavlo Tkach
fa377733be Allow adding existing users to registrar (#2616) 2024-11-27 22:40:32 +00:00
gbrodman
21950f7d82 Add a bulk-domain-action console endpoint (#2611)
For now it only includes two options (domain deletion and domain
suspension). In the future, as necessary, we can add other actions but
this seems like a relatively simple starting point (actions like bulk
updates are much more conceptually complex).
2024-11-22 20:47:47 +00:00
Ben McIlwain
e66aee0416 Downgrade the tx isolation level of poll message ack flow (#2615)
This might help alleviate DB transaction contention on the PollMessage table. A
lower transaction isolation level is safe because acking a poll message is
idempotent: there are only two things it does, either delete a poll message or
take a recurring one from the past and set it to be a year in the future from
the date in the past. Both of these operations will always yield the same final
result even if executed multiple times simultaneously for some reason.
2024-11-22 19:48:19 +00:00
Ben McIlwain
c7e1fc17d2 Downgrade the tx isolation level of poll message request flow (#2614)
It doesn't need a higher transaction isolation level as it's only loading a given poll
message once, and we want to avoid putting any kind of locks on the PollMessage table
as it seems to be having contention issues. Note that the poll message request flow
is by far the most frequent code that touches the PollMessage table, as there are many
many requests every minute from dozens of registrars, but much fewer poll messages
than that to actually ACK.
2024-11-21 22:49:57 +00:00
gbrodman
0c0b0df36e Skip poll messages on deletions for configured registrars (#2613)
See b/379331882 for more details
2024-11-21 22:16:26 +00:00
Weimin Yu
304f0002b4 Refactor FlowRunner transaction invocation (#2612)
Stop calling `transact` if already in a transaction.
2024-11-21 15:58:26 +00:00
gbrodman
15cf3e1bc0 Add RegistrarUpdateHistory objects for console changes (#2585) 2024-11-19 21:03:48 +00:00
Pavlo Tkach
eeed166310 Add console user role update and minor fixes to delete (#2610) 2024-11-15 18:36:10 +00:00
gbrodman
e54075fea3 Allow for removal of registry lock passwords in User objects (#2609)
This essentially enables the "forgot password" flow
2024-11-14 21:01:17 +00:00
Ben McIlwain
78cc1b2937 Fix 'Domian' typo for Domain (#2608) 2024-11-12 18:41:56 +00:00
Pavlo Tkach
35f95bbbe4 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
2024-11-08 18:20:01 +00:00
68 changed files with 2021 additions and 397 deletions

View File

@@ -166,12 +166,26 @@ export class BackendService {
.pipe(catchError((err) => this.errorCatcher<User[]>(err)));
}
createUser(registrarId: string): Observable<User> {
createUser(registrarId: string, maybeUser: User | null): Observable<User> {
return this.http
.post<User>(`/console-api/users?registrarId=${registrarId}`, {})
.post<User>(`/console-api/users?registrarId=${registrarId}`, maybeUser)
.pipe(catchError((err) => this.errorCatcher<User>(err)));
}
deleteUser(registrarId: string, user: User): Observable<any> {
return this.http
.delete<any>(`/console-api/users?registrarId=${registrarId}`, {
body: JSON.stringify(user),
})
.pipe(catchError((err) => this.errorCatcher<any>(err)));
}
updateUser(registrarId: string, updatedUser: User): Observable<any> {
return this.http
.put<User>(`/console-api/users?registrarId=${registrarId}`, updatedUser)
.pipe(catchError((err) => this.errorCatcher<any>(err)));
}
getUserData(): Observable<UserData> {
return this.http
.get<UserData>('/console-api/userdata')

View File

@@ -27,6 +27,7 @@ export interface UserData {
supportEmail: string;
supportPhoneNumber: string;
technicalDocsUrl: string;
userRoles?: Map<string, string>;
}
@Injectable({

View File

@@ -0,0 +1,125 @@
<div class="console-app__user-details">
@if(isEditing) {
<h1 class="mat-headline-4">Editing {{ userDetails().emailAddress }}</h1>
<mat-divider></mat-divider>
<div class="console-app__user-details-controls">
<button
mat-icon-button
aria-label="Back to view user"
(click)="isEditing = false"
>
<mat-icon>arrow_back</mat-icon>
</button>
</div>
<form (ngSubmit)="saveEdit()">
<p>
<mat-form-field appearance="outline">
<mat-label
>User Role:
<mat-icon
matTooltip="Viewer role doesn't allow making updates; Editor role allows updates, like Contacts delete or SSL certificate change"
>help_outline</mat-icon
></mat-label
>
<mat-select [(ngModel)]="userRole" name="userRole">
<mat-option value="PRIMARY_CONTACT">Editor</mat-option>
<mat-option value="ACCOUNT_MANAGER">Viewer</mat-option>
</mat-select>
</mat-form-field>
</p>
<button
mat-flat-button
color="primary"
aria-label="Save user"
type="submit"
>
Save
</button>
</form>
} @else { @if(isNewUser) {
<h1 class="mat-headline-4">
{{ userDetails().emailAddress + " successfully created" }}
</h1>
} @else {
<h1 class="mat-headline-4">User details</h1>
<mat-divider></mat-divider>
<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-flat-button
color="primary"
aria-label="Edit User"
(click)="userRole = userDetails().role; isEditing = true"
>
<mat-icon>edit</mat-icon>
Edit
</button>
<button
mat-icon-button
aria-label="Delete User"
(click)="deleteUser()"
[disabled]="isLoading"
>
<mat-icon>delete</mat-icon>
</button>
</div>
<div *ngIf="isNewUser" class="console-app__user-details-save-password">
<mat-icon>priority_high</mat-icon>
Please save the password. For your security, we do not store passwords in a
recoverable format.
</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,39 @@
// 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;
}
}
&-save-password {
display: flex;
justify-content: center;
align-items: center;
padding: 15px 10px;
margin-bottom: 20px;
border: 1px solid #ddd;
border-radius: 10px;
}
max-width: 616px;
}
}

View File

@@ -0,0 +1,105 @@
// 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, computed } 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 { UsersService, roleToDescription } from './users.service';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-user-edit',
templateUrl: './userEdit.component.html',
styleUrls: ['./userEdit.component.scss'],
standalone: true,
imports: [
FormsModule,
MaterialModule,
SnackBarModule,
CommonModule,
SelectedRegistrarModule,
],
providers: [],
})
export class UserEditComponent {
isEditing = false;
isPasswordVisible = false;
isNewUser = false;
isLoading = false;
userRole = '';
userDetails = computed(() => {
return this.usersService
.users()
.filter(
(u) => u.emailAddress === this.usersService.currentlyOpenUserEmail()
)[0];
});
constructor(
protected registrarService: RegistrarService,
protected usersService: UsersService,
private _snackBar: MatSnackBar
) {
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()).subscribe({
error: (err) => {
this._snackBar.open(err.error || err.message);
this.isLoading = false;
},
complete: () => {
this.isLoading = false;
this.goBack();
},
});
}
goBack() {
this.usersService.currentlyOpenUserEmail.set('');
}
saveEdit() {
this.isLoading = true;
this.usersService
.updateUser({
role: this.userRole,
emailAddress: this.userDetails().emailAddress,
})
.subscribe({
error: (err) => {
this._snackBar.open(err.error || err.message);
this.isLoading = false;
},
complete: () => {
this.isLoading = false;
this.isEditing = false;
},
});
}
}

View File

@@ -1,43 +1,96 @@
<app-selected-registrar-wrapper>
@if (!isLoading) {
@if(isLoading) {
<div class="console-app__users-spinner">
<mat-spinner />
</div>
} @else if(selectingExistingUser) {
<div class="console-app__users">
<h1 class="mat-headline-4">Add existing user</h1>
<p>
<button
mat-icon-button
aria-label="Back to users list"
(click)="selectingExistingUser = false"
>
<mat-icon>arrow_back</mat-icon>
</button>
</p>
<h1>Select registrar from which to add a new user</h1>
<p>
<mat-form-field appearance="outline">
<mat-label>Registrar</mat-label>
<mat-select
[(ngModel)]="selectedRegistrarId"
name="selectedRegistrarId"
(selectionChange)="onRegistrarSelectionChange($event)"
>
@for (registrar of registrarService.registrars(); track registrar) {
<mat-option [value]="registrar.registrarId">{{
registrar.registrarId
}}</mat-option>
}
</mat-select>
</mat-form-field>
</p>
@if(usersSelection.length) {
<app-users-list
[users]="usersSelection"
(onSelect)="existingUserSelected($event)"
/>
<p class="console-app__users-add-existing">
<button
mat-flat-button
color="primary"
aria-label="Add user"
(click)="submitExistingUser()"
[disabled]="!selectedExistingUser"
>
Add user
</button>
<button
mat-stroked-button
aria-label="Cancel adding existing user"
(click)="selectingExistingUser = false"
>
Cancel
</button>
</p>
}
</div>
} @else if(usersService.currentlyOpenUserEmail()) {
<app-user-edit></app-user-edit>
} @else {
<div class="console-app__users">
<div class="console-app__users-header">
<h1 class="mat-headline-4">Users</h1>
<div class="spacer"></div>
<button
mat-flat-button
(click)="createNewUser()"
aria-label="Create new user"
color="primary"
>
Create a Viewer User
</button>
<div class="console-app__users-header-buttons">
<button
class="console-app__users-header-add"
mat-stroked-button
(click)="addExistingUser()"
aria-label="Create new user"
color="primary"
>
<mat-icon>add</mat-icon>
Add existing user
</button>
<button
mat-flat-button
(click)="createNewUser()"
aria-label="Create new user"
color="primary"
>
Create a Viewer User
</button>
</div>
</div>
<mat-table
[dataSource]="dataSource"
class="mat-elevation-z0"
class="console-app__users-table"
matSort
>
<ng-container
*ngFor="let column of columns"
[matColumnDef]="column.columnDef"
>
<mat-header-cell *matHeaderCellDef>
{{ column.header }}
</mat-header-cell>
<mat-cell
*matCellDef="let row"
[innerHTML]="column.cell(row)"
></mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table>
</div>
} @else {
<div class="console-app__users-spinner">
<mat-spinner />
<app-users-list
[users]="usersService.users()"
(onSelect)="openDetails($event)"
/>
</div>
}
</app-selected-registrar-wrapper>

View File

@@ -13,26 +13,37 @@
// limitations under the License.
.console-app {
&__users {
max-width: 1024px;
overflow-x: auto;
}
&__users-spinner {
align-items: center;
display: flex;
justify-content: center;
}
$min-width: 756px;
$max-width: 1024px;
&__users-table {
min-width: $min-width !important;
max-width: $max-width;
}
&__users-new {
margin-left: 20px;
}
&__users-add-existing {
margin-top: 20px;
> button {
margin-right: 15px;
}
}
&__users-header {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
&-buttons {
display: flex;
flex-wrap: wrap;
button {
margin: 0 15px 15px 0;
}
}
}
}

View File

@@ -14,36 +14,18 @@
import { CommonModule } from '@angular/common';
import { HttpErrorResponse } from '@angular/common/http';
import { Component, effect, ViewChild } from '@angular/core';
import { Component, effect } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { SelectedRegistrarModule } from '../app.module';
import { MaterialModule } from '../material.module';
import { RegistrarService } from '../registrar/registrar.service';
import { SnackBarModule } from '../snackbar.module';
import { UserEditComponent } from './userEdit.component';
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';
};
export const columns = [
{
columnDef: 'emailAddress',
header: 'User email',
cell: (record: User) => `${record.emailAddress || ''}`,
},
{
columnDef: 'role',
header: 'User role',
cell: (record: User) => `${roleToDescription(record.role)}`,
},
];
import { UserDataService } from '../shared/services/userData.service';
import { FormsModule } from '@angular/forms';
import { UsersListComponent } from './usersList.component';
import { MatSelectChange } from '@angular/material/select';
@Component({
selector: 'app-users',
@@ -51,40 +33,45 @@ export const columns = [
styleUrls: ['./users.component.scss'],
standalone: true,
imports: [
FormsModule,
MaterialModule,
SnackBarModule,
CommonModule,
SelectedRegistrarModule,
UsersListComponent,
UserEditComponent,
],
providers: [UsersService],
})
export class UsersComponent {
dataSource: MatTableDataSource<User>;
columns = columns;
displayedColumns = this.columns.map((c) => c.columnDef);
isLoading = false;
@ViewChild(MatSort) sort!: MatSort;
selectingExistingUser = false;
selectedRegistrarId = '';
usersSelection: User[] = [];
selectedExistingUser: User | undefined;
constructor(
protected registrarService: RegistrarService,
protected usersService: UsersService,
private userDataService: UserDataService,
private _snackBar: MatSnackBar
) {
this.dataSource = new MatTableDataSource<User>(usersService.users());
effect(() => {
if (registrarService.registrarId()) {
this.loadUsers();
}
});
effect(() => {
this.dataSource.data = usersService.users();
});
}
ngAfterViewInit() {
this.dataSource.sort = this.sort;
addExistingUser() {
this.selectingExistingUser = true;
this.selectedRegistrarId = '';
this.usersSelection = [];
this.selectedExistingUser = undefined;
}
existingUserSelected(user: User) {
this.selectedExistingUser = user;
}
loadUsers() {
@@ -92,6 +79,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;
@@ -101,22 +89,50 @@ 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,
}
);
},
this.usersService.createOrAddNewUser(null).subscribe({
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error || err.message);
this.isLoading = false;
},
complete: () => {
this.isLoading = false;
},
});
}
openDetails(user: User) {
this.usersService.currentlyOpenUserEmail.set(user.emailAddress);
}
onRegistrarSelectionChange(e: MatSelectChange) {
if (e.value) {
this.usersService.fetchUsersForRegistrar(e.value).subscribe({
error: (err) => {
this._snackBar.open(err.error || err.message);
},
next: (users) => {
this.usersSelection = users;
},
});
}
}
submitExistingUser() {
this.isLoading = true;
if (this.selectedExistingUser) {
this.usersService
.createOrAddNewUser(this.selectedExistingUser)
.subscribe({
error: (err) => {
this._snackBar.open(err.error || err.message);
this.isLoading = false;
},
complete: () => {
this.isLoading = false;
this.selectingExistingUser = false;
this.loadUsers();
},
});
}
}
}

View File

@@ -13,29 +13,43 @@
// limitations under the License.
import { Injectable, signal } from '@angular/core';
import { tap } from 'rxjs';
import { switchMap, 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 === '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<User[]>([]);
currentlyOpenUserEmail = signal<string>('');
isNewUser: boolean = false;
constructor(
private backendService: BackendService,
private registrarService: RegistrarService
) {}
fetchUsersForRegistrar(registrarId: string) {
return this.backendService.getUsers(registrarId);
}
fetchUsers() {
return this.backendService
.getUsers(this.registrarService.registrarId())
@@ -46,13 +60,29 @@ export class UsersService {
);
}
createNewUser() {
createOrAddNewUser(maybeExistingUser: User | null) {
return this.backendService
.createUser(this.registrarService.registrarId())
.createUser(this.registrarService.registrarId(), maybeExistingUser)
.pipe(
tap((newUser: User) => {
this.users.set([...this.users(), newUser]);
if (newUser) {
this.users.set([...this.users(), newUser]);
this.currentlyOpenUserEmail.set(newUser.emailAddress);
this.isNewUser = true;
}
})
);
}
deleteUser(user: User) {
return this.backendService
.deleteUser(this.registrarService.registrarId(), user)
.pipe(switchMap((_) => this.fetchUsers()));
}
updateUser(updatedUser: User) {
return this.backendService
.updateUser(this.registrarService.registrarId(), updatedUser)
.pipe(switchMap((_) => this.fetchUsers()));
}
}

View File

@@ -0,0 +1,24 @@
<div class="console-app__users-table-wrapper">
<mat-table
[dataSource]="dataSource"
class="mat-elevation-z0"
class="console-app__users-table"
matSort
>
<ng-container
*ngFor="let column of columns"
[matColumnDef]="column.columnDef"
>
<mat-header-cell *matHeaderCellDef>
{{ column.header }}
</mat-header-cell>
<mat-cell *matCellDef="let row" [innerHTML]="column.cell(row)"></mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row
*matRowDef="let row; columns: displayedColumns"
[class.rowSelected]="isRowSelected(row)"
(click)="onClick(row)"
></mat-row>
</mat-table>
</div>

View File

@@ -0,0 +1,14 @@
.console-app {
&__users-table {
min-width: 616px;
.rowSelected {
background-color: var(--light-highlight);
font-weight: bold;
}
}
&__users-table-wrapper {
width: 100%;
overflow: auto;
}
}

View File

@@ -0,0 +1,78 @@
// 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,
effect,
EventEmitter,
input,
Output,
ViewChild,
} from '@angular/core';
import { MaterialModule } from '../material.module';
import { User, roleToDescription } from './users.service';
import { MatTableDataSource } from '@angular/material/table';
import { MatSort } from '@angular/material/sort';
export const columns = [
{
columnDef: 'emailAddress',
header: 'User email',
cell: (record: User) => `${record.emailAddress || ''}`,
},
{
columnDef: 'role',
header: 'User role',
cell: (record: User) => `${roleToDescription(record.role)}`,
},
];
@Component({
selector: 'app-users-list',
templateUrl: './usersList.component.html',
styleUrls: ['./usersList.component.scss'],
standalone: true,
imports: [MaterialModule, CommonModule],
providers: [],
})
export class UsersListComponent {
columns = columns;
displayedColumns = this.columns.map((c) => c.columnDef);
dataSource: MatTableDataSource<User>;
selectedRow!: User;
users = input<User[]>([]);
@Output() onSelect = new EventEmitter<User>();
@ViewChild(MatSort) sort!: MatSort;
constructor() {
this.dataSource = new MatTableDataSource<User>(this.users());
effect(() => {
this.dataSource.data = this.users();
});
}
ngAfterViewInit() {
this.dataSource.sort = this.sort;
}
onClick(row: User) {
this.selectedRow = row;
this.onSelect.emit(row);
}
isRowSelected(row: User) {
return row === this.selectedRow;
}
}

View File

@@ -1743,6 +1743,17 @@ public final class RegistryConfig {
CONFIG_SETTINGS.get().registryPolicy.tieredPricingPromotionRegistrarIds);
}
/**
* Set of registrars for which we do not send poll messages on standard domain deletion.
*
* <p>For these registrars we won't send a poll message in order to avoid database contention. See
* b/379331882 for more details.
*/
public static ImmutableSet<String> getNoPollMessageOnDeletionRegistrarIds() {
return ImmutableSet.copyOf(
CONFIG_SETTINGS.get().registryPolicy.noPollMessageOnDeletionRegistrarIds);
}
/**
* Memoizes loading of the {@link RegistryConfigSettings} POJO.
*

View File

@@ -115,6 +115,7 @@ public class RegistryConfigSettings {
public boolean requireSslCertificates;
public double sunriseDomainCreateDiscount;
public Set<String> tieredPricingPromotionRegistrarIds;
public Set<String> noPollMessageOnDeletionRegistrarIds;
}
/** Configuration for Hibernate. */

View File

@@ -220,6 +220,9 @@ registryPolicy:
# In addition, we will return the non-promotional (i.e. incorrect) price on
# domain create requests.
tieredPricingPromotionRegistrarIds: []
# List of registrars for which we won't send poll message on standard domain
# deletions.
noPollMessageOnDeletionRegistrarIds: []
hibernate:
# If set to false, calls to tm().transact() cannot be nested. If set to true,

View File

@@ -11,6 +11,8 @@ registryPolicy:
Line 2 is this 1.
tieredPricingPromotionRegistrarIds:
- NewRegistrar
noPollMessageOnDeletionRegistrarIds:
- NewRegistrar
caching:
singletonCacheRefreshSeconds: 0

View File

@@ -105,7 +105,7 @@ public final class ExtensionManager {
}
private static final ImmutableSet<EppRequestSource> ALLOWED_METADATA_EPP_REQUEST_SOURCES =
ImmutableSet.of(EppRequestSource.TOOL, EppRequestSource.BACKEND);
ImmutableSet.of(EppRequestSource.BACKEND, EppRequestSource.CONSOLE, EppRequestSource.TOOL);
private void checkForRestrictedExtensions(
ImmutableSet<Class<? extends CommandExtension>> suppliedExtensions)

View File

@@ -75,7 +75,8 @@ public class FlowRunner {
flowReporter.recordToLogs();
}
eppMetricBuilder.setCommandNameFromFlow(flowClass.getSimpleName());
if (!isTransactional) {
// We may already be in a transaction, e.g., when invoked by DeleteExpiredDomainsAction.
if (!isTransactional || jpaTransactionManager.inTransaction()) {
return EppOutput.create(flowProvider.get().run());
}
try {

View File

@@ -44,7 +44,9 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
import google.registry.batch.AsyncTaskEnqueuer;
import google.registry.config.RegistryConfig;
import google.registry.flows.EppException;
import google.registry.flows.EppException.AssociationProhibitsOperationException;
import google.registry.flows.ExtensionManager;
@@ -117,6 +119,8 @@ import org.joda.time.Duration;
@ReportingSpec(ActivityReportField.DOMAIN_DELETE)
public final class DomainDeleteFlow implements MutatingFlow {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final ImmutableSet<StatusValue> DISALLOWED_STATUSES = ImmutableSet.of(
StatusValue.CLIENT_DELETE_PROHIBITED,
StatusValue.PENDING_DELETE,
@@ -212,10 +216,17 @@ public final class DomainDeleteFlow implements MutatingFlow {
// superuser (i.e. the registrar didn't request this delete and thus should be notified even if
// it is synchronous).
if (durationUntilDelete.isLongerThan(Duration.ZERO) || isSuperuser) {
PollMessage.OneTime deletePollMessage =
createDeletePollMessage(existingDomain, domainHistoryId, deletionTime);
entitiesToSave.add(deletePollMessage);
builder.setDeletePollMessage(deletePollMessage.createVKey());
if (RegistryConfig.getNoPollMessageOnDeletionRegistrarIds()
.contains(existingDomain.getCurrentSponsorRegistrarId())) {
logger.atInfo().log(
"Skipping poll message on domain deletion for registrar %s due to configuration",
existingDomain.getCurrentSponsorRegistrarId());
} else {
PollMessage.OneTime deletePollMessage =
createDeletePollMessage(existingDomain, domainHistoryId, deletionTime);
entitiesToSave.add(deletePollMessage);
builder.setDeletePollMessage(deletePollMessage.createVKey());
}
}
// Send a second poll message immediately if the domain is being deleted asynchronously by a

View File

@@ -183,7 +183,9 @@ public final class DomainRestoreRequestFlow implements MutatingFlow {
DomainHistory domainHistory = buildDomainHistory(newDomain, now);
entitiesToSave.add(newDomain, domainHistory, autorenewEvent, autorenewPollMessage);
tm().putAll(entitiesToSave.build());
tm().delete(existingDomain.getDeletePollMessage());
if (existingDomain.getDeletePollMessage() != null) {
tm().delete(existingDomain.getDeletePollMessage());
}
requestDomainDnsRefresh(existingDomain.getDomainName());
return responseBuilder
.setExtensions(createResponseExtensions(feesAndCredits, feeUpdate, isExpired))

View File

@@ -37,6 +37,8 @@ import google.registry.model.poll.MessageQueueInfo;
import google.registry.model.poll.PollMessage;
import google.registry.model.poll.PollMessageExternalKeyConverter;
import google.registry.model.poll.PollMessageExternalKeyConverter.PollMessageExternalKeyParseException;
import google.registry.persistence.IsolationLevel;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import google.registry.persistence.VKey;
import java.util.Optional;
import javax.inject.Inject;
@@ -55,6 +57,7 @@ import org.joda.time.DateTime;
* @error {@link PollAckFlow.MissingMessageIdException}
* @error {@link PollAckFlow.NotAuthorizedToAckMessageException}
*/
@IsolationLevel(value = TransactionIsolationLevel.TRANSACTION_READ_COMMITTED)
public final class PollAckFlow implements MutatingFlow {
@Inject ExtensionManager extensionManager;

View File

@@ -32,6 +32,8 @@ import google.registry.model.eppoutput.EppResponse;
import google.registry.model.poll.MessageQueueInfo;
import google.registry.model.poll.PollMessage;
import google.registry.model.poll.PollMessageExternalKeyConverter;
import google.registry.persistence.IsolationLevel;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import java.util.Optional;
import javax.inject.Inject;
import org.joda.time.DateTime;
@@ -47,6 +49,7 @@ import org.joda.time.DateTime;
*
* @error {@link PollRequestFlow.UnexpectedMessageIdException}
*/
@IsolationLevel(value = TransactionIsolationLevel.TRANSACTION_READ_COMMITTED)
public final class PollRequestFlow implements TransactionalFlow {
@Inject ExtensionManager extensionManager;

View File

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

View File

@@ -36,9 +36,9 @@ import javax.annotation.Nullable;
import javax.inject.Inject;
/**
* An authenticam mechanism that verifies the OIDC token.
* An authentication mechanism that verifies the OIDC token.
*
* <p>Currently, two flavors are supported: one that checkes for the OIDC token as a regular bearer
* <p>Currently, two flavors are supported: one that checks for the OIDC token as a regular bearer
* token, and another that checks for the OIDC token passed by IAP. In both cases, the {@link
* AuthResult} with the highest {@link AuthLevel} possible is returned. So, if the email address for
* which the token is minted exists both as a {@link User} and as a service account, the returned

View File

@@ -14,6 +14,7 @@
package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
@@ -41,6 +42,15 @@ public abstract class CreateOrUpdateUserCommand extends ConfirmingCommand {
+ " to remove the field.")
private String registryLockEmailAddress;
@Nullable
@Parameter(
names = "--registry_lock_password",
description =
"Sets the registry lock password for this user, or removes it (allowing the user to"
+ " re-set it). Do not set the password explicitly unless in exceptional"
+ " circumstances.")
private String registryLockPassword;
@Nullable
@Parameter(
names = "--admin",
@@ -94,6 +104,25 @@ public abstract class CreateOrUpdateUserCommand extends ConfirmingCommand {
builder.setRegistryLockEmailAddress(registryLockEmailAddress);
}
}
// Ditto the registry lock password
if (registryLockPassword != null) {
if (registryLockEmailAddress != null) {
// Edge case, make sure we're not removing an email and setting a password at the same time
checkArgument(
!registryLockEmailAddress.isEmpty(),
"Cannot set/remove registry lock password on a user without a registry lock email"
+ " address");
} else {
checkArgument(
user != null && user.getRegistryLockEmailAddress().isPresent(),
"Cannot set/remove registry lock password on a user without a registry lock email"
+ " address");
}
builder.removeRegistryLockPassword();
if (!registryLockPassword.isEmpty()) {
builder.setRegistryLockPassword(registryLockPassword);
}
}
tm().put(builder.build());
}
}

View File

@@ -16,8 +16,12 @@ 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.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.HEAD;
import static google.registry.request.Action.Method.POST;
import static google.registry.request.Action.Method.PUT;
import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
@@ -33,6 +37,7 @@ import google.registry.batch.CloudTasksUtils;
import google.registry.config.RegistryConfig;
import google.registry.export.sheet.SyncRegistrarsSheetAction;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.ConsoleUpdateHistory;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
@@ -75,13 +80,21 @@ 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 if (requestMethod.equals(PUT.toString())) {
putHandler(user);
} else {
postHandler(user);
}
}
}
} catch (ConsolePermissionForbiddenException e) {
@@ -109,10 +122,22 @@ public abstract class ConsoleApiAction implements Runnable {
throw new UnsupportedOperationException("Console API POST handler not implemented");
}
protected void putHandler(User user) {
throw new UnsupportedOperationException("Console API PUT handler not implemented");
}
protected void getHandler(User user) {
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);
@@ -237,6 +262,14 @@ public abstract class ConsoleApiAction implements Runnable {
}
}
protected void finishAndPersistConsoleUpdateHistory(ConsoleUpdateHistory.Builder<?, ?> builder) {
builder.setActingUser(consoleApiParams.authResult().user().get());
builder.setUrl(consoleApiParams.request().getRequestURI());
builder.setMethod(consoleApiParams.request().getMethod());
builder.setModificationTime(tm().getTransactionTime());
tm().put(builder.build());
}
/** Specialized exception class used for failure when a user doesn't have the right permission. */
private static class ConsolePermissionForbiddenException extends RuntimeException {
private ConsolePermissionForbiddenException(String message) {

View File

@@ -14,6 +14,7 @@
package google.registry.ui.server.console;
import com.google.gson.Gson;
import google.registry.request.Response;
import google.registry.request.auth.AuthResult;
import google.registry.security.XsrfTokenManager;
@@ -26,13 +27,16 @@ public record ConsoleApiParams(
Response response,
AuthResult authResult,
SendEmailUtils sendEmailUtils,
XsrfTokenManager xsrfTokenManager) {
XsrfTokenManager xsrfTokenManager,
Gson gson) {
public static ConsoleApiParams create(
HttpServletRequest request,
Response response,
AuthResult authResult,
SendEmailUtils sendEmailUtils,
XsrfTokenManager xsrfTokenManager) {
return new ConsoleApiParams(request, response, authResult, sendEmailUtils, xsrfTokenManager);
XsrfTokenManager xsrfTokenManager,
Gson gson) {
return new ConsoleApiParams(
request, response, authResult, sendEmailUtils, xsrfTokenManager, gson);
}
}

View File

@@ -0,0 +1,228 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.ui.server.console;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.ImmutableMap;
import com.google.common.escape.Escaper;
import com.google.common.xml.XmlEscapers;
import com.google.gson.JsonElement;
import com.google.gson.annotations.Expose;
import google.registry.flows.EppController;
import google.registry.flows.EppRequestSource;
import google.registry.flows.PasswordOnlyTransportCredentials;
import google.registry.flows.StatelessRequestSessionMetadata;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.User;
import google.registry.model.eppcommon.ProtocolDefinition;
import google.registry.model.eppoutput.EppOutput;
import google.registry.model.eppoutput.Result;
import google.registry.request.Action;
import google.registry.request.OptionalJsonPayload;
import google.registry.request.Parameter;
import google.registry.request.auth.Auth;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.inject.Inject;
/**
* Console endpoint to perform the same action to a list of domains.
*
* <p>All requests must include the {@link BulkAction} to perform as well as a {@link
* BulkDomainList} of domains on which to apply the action. The remaining contents of the request
* body depend on the type of action -- some requests may require more data than others.
*/
@Action(
service = Action.GaeService.DEFAULT,
gkeService = Action.GkeService.CONSOLE,
path = ConsoleBulkDomainAction.PATH,
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public class ConsoleBulkDomainAction extends ConsoleApiAction {
public static final String PATH = "/console-api/bulk-domain";
private static Escaper XML_ESCAPER = XmlEscapers.xmlContentEscaper();
public enum BulkAction {
DELETE,
SUSPEND
}
/** All requests must include at least a list of domain names on which to perform the action. */
public record BulkDomainList(@Expose List<String> domainList) {}
public record BulkDomainDeleteRequest(@Expose String reason) {}
public record BulkDomainSuspendRequest(@Expose String reason) {}
private static final String DOMAIN_DELETE_XML =
"""
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<delete>
<domain:delete
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>%DOMAIN_NAME%</domain:name>
</domain:delete>
</delete>
<extension>
<metadata:metadata xmlns:metadata="urn:google:params:xml:ns:metadata-1.0">
<metadata:reason>%REASON%</metadata:reason>
<metadata:requestedByRegistrar>true</metadata:requestedByRegistrar>
</metadata:metadata>
</extension>
<clTRID>RegistryConsole</clTRID>
</command>
</epp>""";
private static final String DOMAIN_SUSPEND_XML =
"""
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<epp
xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<update>
<domain:update
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>%DOMAIN_NAME%</domain:name>
<domain:add>
<domain:status s="serverDeleteProhibited" lang="en"></domain:status>
<domain:status s="serverHold" lang="en"></domain:status>
<domain:status s="serverRenewProhibited" lang="en"></domain:status>
<domain:status s="serverTransferProhibited" lang="en"></domain:status>
<domain:status s="serverUpdateProhibited" lang="en"></domain:status>
</domain:add>
<domain:rem></domain:rem>
</domain:update>
</update>
<extension>
<metadata:metadata
xmlns:metadata="urn:google:params:xml:ns:metadata-1.0">
<metadata:reason>Console suspension: %REASON%</metadata:reason>
<metadata:requestedByRegistrar>false</metadata:requestedByRegistrar>
</metadata:metadata>
</extension>
<clTRID>RegistryTool</clTRID>
</command>
</epp>""";
private final EppController eppController;
private final String registrarId;
private final String bulkDomainAction;
private final Optional<JsonElement> optionalJsonPayload;
@Inject
public ConsoleBulkDomainAction(
ConsoleApiParams consoleApiParams,
EppController eppController,
@Parameter("registrarId") String registrarId,
@Parameter("bulkDomainAction") String bulkDomainAction,
@OptionalJsonPayload Optional<JsonElement> optionalJsonPayload) {
super(consoleApiParams);
this.eppController = eppController;
this.registrarId = registrarId;
this.bulkDomainAction = bulkDomainAction;
this.optionalJsonPayload = optionalJsonPayload;
}
@Override
protected void postHandler(User user) {
BulkAction bulkAction = BulkAction.valueOf(bulkDomainAction);
JsonElement jsonPayload =
optionalJsonPayload.orElseThrow(
() -> new IllegalArgumentException("Bulk action payload must be present"));
BulkDomainList domainList = consoleApiParams.gson().fromJson(jsonPayload, BulkDomainList.class);
checkPermission(user, registrarId, ConsolePermission.EXECUTE_EPP_COMMANDS);
ImmutableMap<String, ConsoleEppOutput> result =
switch (bulkAction) {
case DELETE -> handleBulkDelete(jsonPayload, domainList, user);
case SUSPEND -> handleBulkSuspend(jsonPayload, domainList, user);
};
// Front end should parse situations where only some commands worked
consoleApiParams.response().setPayload(consoleApiParams.gson().toJson(result));
consoleApiParams.response().setStatus(SC_OK);
}
private ImmutableMap<String, ConsoleEppOutput> handleBulkDelete(
JsonElement jsonPayload, BulkDomainList domainList, User user) {
String reason =
consoleApiParams.gson().fromJson(jsonPayload, BulkDomainDeleteRequest.class).reason;
return runCommandOverDomains(
domainList,
DOMAIN_DELETE_XML,
new ImmutableMap.Builder<String, String>().put("REASON", reason),
user);
}
private ImmutableMap<String, ConsoleEppOutput> handleBulkSuspend(
JsonElement jsonPayload, BulkDomainList domainList, User user) {
String reason =
consoleApiParams.gson().fromJson(jsonPayload, BulkDomainSuspendRequest.class).reason;
return runCommandOverDomains(
domainList,
DOMAIN_SUSPEND_XML,
new ImmutableMap.Builder<String, String>().put("REASON", reason),
user);
}
/** Runs the provided XML template and substitutions over a provided list of domains. */
private ImmutableMap<String, ConsoleEppOutput> runCommandOverDomains(
BulkDomainList domainList,
String xmlTemplate,
ImmutableMap.Builder<String, String> replacements,
User user) {
return domainList.domainList.stream()
.collect(
toImmutableMap(
d -> d,
d ->
executeEpp(
fillSubstitutions(xmlTemplate, replacements.put("DOMAIN_NAME", d)), user)));
}
private ConsoleEppOutput executeEpp(String xml, User user) {
return ConsoleEppOutput.fromEppOutput(
eppController.handleEppCommand(
new StatelessRequestSessionMetadata(
registrarId, ProtocolDefinition.getVisibleServiceExtensionUris()),
new PasswordOnlyTransportCredentials(),
EppRequestSource.CONSOLE,
false,
user.getUserRoles().isAdmin(),
xml.getBytes(UTF_8)));
}
/** Fills the provided XML template with the replacement values, including escaping the values. */
private String fillSubstitutions(
String xmlTemplate, ImmutableMap.Builder<String, String> replacements) {
String xml = xmlTemplate;
for (Map.Entry<String, String> entry : replacements.buildKeepingLast().entrySet()) {
xml = xml.replaceAll("%" + entry.getKey() + "%", XML_ESCAPER.escape(entry.getValue()));
}
return xml;
}
public record ConsoleEppOutput(@Expose String message, @Expose int responseCode) {
static ConsoleEppOutput fromEppOutput(EppOutput eppOutput) {
Result result = eppOutput.getResponse().getResult();
return new ConsoleEppOutput(result.getMsg(), result.getCode().code);
}
}
}

View File

@@ -18,7 +18,6 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import com.google.gson.Gson;
import google.registry.model.EppResourceUtils;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.User;
@@ -41,17 +40,14 @@ public class ConsoleDomainGetAction extends ConsoleApiAction {
public static final String PATH = "/console-api/domain";
private final Gson gson;
private final String paramDomain;
@Inject
public ConsoleDomainGetAction(
ConsoleApiParams consoleApiParams,
Gson gson,
@Parameter("consoleDomain") String paramDomain) {
super(consoleApiParams);
this.paramDomain = paramDomain;
this.gson = gson;
}
@Override
@@ -72,6 +68,6 @@ public class ConsoleDomainGetAction extends ConsoleApiAction {
return;
}
consoleApiParams.response().setStatus(SC_OK);
consoleApiParams.response().setPayload(gson.toJson(domain));
consoleApiParams.response().setPayload(consoleApiParams.gson().toJson(domain));
}
}

View File

@@ -21,7 +21,6 @@ import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ascii;
import com.google.gson.Gson;
import com.google.gson.annotations.Expose;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.console.User;
@@ -55,7 +54,6 @@ public class ConsoleDomainListAction extends ConsoleApiAction {
private static final String SEARCH_TERM_QUERY = " AND LOWER(domainName) LIKE :searchTerm";
private static final String ORDER_BY_STATEMENT = " ORDER BY creationTime DESC";
private final Gson gson;
private final String registrarId;
private final Optional<DateTime> checkpointTime;
private final int pageNumber;
@@ -66,7 +64,6 @@ public class ConsoleDomainListAction extends ConsoleApiAction {
@Inject
public ConsoleDomainListAction(
ConsoleApiParams consoleApiParams,
Gson gson,
@Parameter("registrarId") String registrarId,
@Parameter("checkpointTime") Optional<DateTime> checkpointTime,
@Parameter("pageNumber") Optional<Integer> pageNumber,
@@ -74,7 +71,6 @@ public class ConsoleDomainListAction extends ConsoleApiAction {
@Parameter("totalResults") Optional<Long> totalResults,
@Parameter("searchTerm") Optional<String> searchTerm) {
super(consoleApiParams);
this.gson = gson;
this.registrarId = registrarId;
this.checkpointTime = checkpointTime;
this.pageNumber = pageNumber.orElse(0);
@@ -120,7 +116,10 @@ public class ConsoleDomainListAction extends ConsoleApiAction {
consoleApiParams
.response()
.setPayload(gson.toJson(new DomainListResult(domains, checkpoint, actualTotalResults)));
.setPayload(
consoleApiParams
.gson()
.toJson(new DomainListResult(domains, checkpoint, actualTotalResults)));
consoleApiParams.response().setStatus(SC_OK);
}

View File

@@ -27,6 +27,8 @@ import com.google.common.collect.ImmutableSet;
import com.google.gson.annotations.Expose;
import google.registry.flows.EppException.AuthenticationErrorException;
import google.registry.flows.PasswordOnlyTransportCredentials;
import google.registry.model.console.ConsoleUpdateHistory;
import google.registry.model.console.RegistrarUpdateHistory;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.request.Action;
@@ -53,7 +55,6 @@ public class ConsoleEppPasswordAction extends ConsoleApiAction {
private final PasswordOnlyTransportCredentials credentials =
new PasswordOnlyTransportCredentials();
private final AuthenticatedRegistrarAccessor registrarAccessor;
private final Optional<EppPasswordData> eppPasswordChangeRequest;
@Inject
@@ -106,6 +107,14 @@ public class ConsoleEppPasswordAction extends ConsoleApiAction {
Registrar updatedRegistrar =
registrar.asBuilder().setPassword(eppRequestBody.newPassword()).build();
tm().put(updatedRegistrar);
EppPasswordData sanitizedData =
new EppPasswordData(
eppRequestBody.registrarId, "********", "••••••••", "••••••••");
finishAndPersistConsoleUpdateHistory(
new RegistrarUpdateHistory.Builder()
.setType(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE)
.setRegistrar(updatedRegistrar)
.setRequestBody(consoleApiParams.gson().toJson(sanitizedData)));
sendExternalUpdates(
ImmutableMap.of("password", new DiffUtils.DiffPair("********", "••••••••")),
registrar,

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.ConsoleOteAction.OteCreateData;
import google.registry.ui.server.console.ConsoleRegistryLockAction.ConsoleRegistryLockPostInput;
import google.registry.ui.server.console.ConsoleUsersAction.UserData;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Optional;
import org.joda.time.DateTime;
@@ -52,8 +53,10 @@ public final class ConsoleModule {
Response response,
AuthResult authResult,
SendEmailUtils sendEmailUtils,
XsrfTokenManager xsrfTokenManager) {
return ConsoleApiParams.create(request, response, authResult, sendEmailUtils, xsrfTokenManager);
XsrfTokenManager xsrfTokenManager,
Gson gson) {
return ConsoleApiParams.create(
request, response, authResult, sendEmailUtils, xsrfTokenManager, gson);
}
@Provides
@@ -238,6 +241,12 @@ public final class ConsoleModule {
return extractOptionalParameter(req, "searchTerm");
}
@Provides
@Parameter("bulkDomainAction")
public static String provideBulkDomainAction(HttpServletRequest req) {
return extractRequiredParameter(req, "bulkDomainAction");
}
@Provides
@Parameter("eppPasswordChangeRequest")
public static Optional<EppPasswordData> provideEppPasswordChangeRequest(
@@ -245,6 +254,13 @@ public final class ConsoleModule {
return payload.map(s -> gson.fromJson(s, EppPasswordData.class));
}
@Provides
@Parameter("userData")
public static Optional<UserData> provideUserData(
Gson gson, @OptionalJsonPayload Optional<JsonElement> payload) {
return payload.map(s -> gson.fromJson(s, UserData.class));
}
@Provides
@Parameter("oteCreateData")
public static Optional<OteCreateData> provideOteCreateData(

View File

@@ -27,7 +27,6 @@ import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import com.google.common.base.Strings;
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.OteAccountBuilder;
@@ -62,7 +61,6 @@ public class ConsoleOteAction extends ConsoleApiAction {
private static final String STAT_TYPE_DESCRIPTION_PARAM = "description";
private static final String STAT_TYPE_REQUIREMENT_PARAM = "requirement";
private static final String STAT_TYPE_TIMES_PERFORMED_PARAM = "timesPerformed";
private final Gson gson;
private final StringGenerator passwordGenerator;
private final Optional<OteCreateData> oteCreateData;
private final Optional<String> maybeGroupEmailAddress;
@@ -72,14 +70,12 @@ public class ConsoleOteAction extends ConsoleApiAction {
@Inject
public ConsoleOteAction(
ConsoleApiParams consoleApiParams,
Gson gson,
IamClient iamClient,
@Parameter("registrarId") String registrarId, // Get request param
@Config("gSuiteConsoleUserGroupEmailAddress") Optional<String> maybeGroupEmailAddress,
@Named("base58StringGenerator") StringGenerator passwordGenerator,
@Parameter("oteCreateData") Optional<OteCreateData> oteCreateData) {
super(consoleApiParams);
this.gson = gson;
this.passwordGenerator = passwordGenerator;
this.oteCreateData = oteCreateData;
this.maybeGroupEmailAddress = maybeGroupEmailAddress;
@@ -116,8 +112,13 @@ public class ConsoleOteAction extends ConsoleApiAction {
consoleApiParams
.response()
.setPayload(
gson.toJson(
ImmutableMap.builder().putAll(registrarIdToTld).put("password", password).build()));
consoleApiParams
.gson()
.toJson(
ImmutableMap.builder()
.putAll(registrarIdToTld)
.put("password", password)
.build()));
}
@Override
@@ -153,7 +154,7 @@ public class ConsoleOteAction extends ConsoleApiAction {
convertSingleRequirement(statType, oteStats.getCount(statType)))
.collect(toImmutableList());
consoleApiParams.response().setStatus(SC_OK);
consoleApiParams.response().setPayload(gson.toJson(stats));
consoleApiParams.response().setPayload(consoleApiParams.gson().toJson(stats));
});
}

View File

@@ -23,7 +23,6 @@ import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import com.google.common.collect.ImmutableList;
import com.google.gson.Gson;
import com.google.gson.annotations.Expose;
import google.registry.flows.EppException;
import google.registry.flows.domain.DomainFlowUtils;
@@ -72,7 +71,6 @@ public class ConsoleRegistryLockAction extends ConsoleApiAction {
private final DomainLockUtils domainLockUtils;
private final GmailClient gmailClient;
private final Gson gson;
private final Optional<ConsoleRegistryLockPostInput> optionalPostInput;
private final String registrarId;
@@ -81,14 +79,12 @@ public class ConsoleRegistryLockAction extends ConsoleApiAction {
ConsoleApiParams consoleApiParams,
DomainLockUtils domainLockUtils,
GmailClient gmailClient,
Gson gson,
@Parameter("consoleRegistryLockPostInput")
Optional<ConsoleRegistryLockPostInput> optionalPostInput,
@Parameter("registrarId") String registrarId) {
super(consoleApiParams);
this.domainLockUtils = domainLockUtils;
this.gmailClient = gmailClient;
this.gson = gson;
this.optionalPostInput = optionalPostInput;
this.registrarId = registrarId;
}
@@ -96,7 +92,7 @@ public class ConsoleRegistryLockAction extends ConsoleApiAction {
@Override
protected void getHandler(User user) {
checkPermission(user, registrarId, ConsolePermission.REGISTRY_LOCK);
consoleApiParams.response().setPayload(gson.toJson(getLockedDomains()));
consoleApiParams.response().setPayload(consoleApiParams.gson().toJson(getLockedDomains()));
consoleApiParams.response().setStatus(SC_OK);
}

View File

@@ -17,7 +17,6 @@ package google.registry.ui.server.console;
import static google.registry.request.Action.Method.GET;
import com.google.common.base.Ascii;
import com.google.gson.Gson;
import com.google.gson.annotations.Expose;
import google.registry.model.console.User;
import google.registry.model.domain.RegistryLock;
@@ -42,18 +41,15 @@ public class ConsoleRegistryLockVerifyAction extends ConsoleApiAction {
static final String PATH = "/console-api/registry-lock-verify";
private final DomainLockUtils domainLockUtils;
private final Gson gson;
private final String lockVerificationCode;
@Inject
public ConsoleRegistryLockVerifyAction(
ConsoleApiParams consoleApiParams,
DomainLockUtils domainLockUtils,
Gson gson,
@Parameter("lockVerificationCode") String lockVerificationCode) {
super(consoleApiParams);
this.domainLockUtils = domainLockUtils;
this.gson = gson;
this.lockVerificationCode = lockVerificationCode;
}
@@ -68,7 +64,7 @@ public class ConsoleRegistryLockVerifyAction extends ConsoleApiAction {
RegistryLockVerificationResponse lockResponse =
new RegistryLockVerificationResponse(
Ascii.toLowerCase(action.toString()), lock.getDomainName(), lock.getRegistrarId());
consoleApiParams.response().setPayload(gson.toJson(lockResponse));
consoleApiParams.response().setPayload(consoleApiParams.gson().toJson(lockResponse));
consoleApiParams.response().setStatus(HttpServletResponse.SC_OK);
}

View File

@@ -23,6 +23,8 @@ import static org.apache.http.HttpStatus.SC_OK;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.ConsoleUpdateHistory;
import google.registry.model.console.RegistrarUpdateHistory;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.request.Action;
@@ -99,6 +101,11 @@ public class ConsoleUpdateRegistrarAction extends ConsoleApiAction {
.build();
tm().put(updatedRegistrar);
finishAndPersistConsoleUpdateHistory(
new RegistrarUpdateHistory.Builder()
.setType(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE)
.setRegistrar(updatedRegistrar)
.setRequestBody(consoleApiParams.gson().toJson(registrarParam)));
sendExternalUpdatesIfNecessary(
EmailInfo.create(
existingRegistrar.get(),

View File

@@ -82,6 +82,8 @@ public class ConsoleUserDataAction extends ConsoleApiAction {
// auth checks.
"isAdmin", user.getUserRoles().isAdmin(),
"globalRole", user.getUserRoles().getGlobalRole(),
// registrar-specific roles
"userRoles", user.getUserRoles().getRegistrarRoles(),
// Include static contact resources in this call to minimize round trips
"productName", productName,
"supportEmail", supportEmail,

View File

@@ -14,11 +14,15 @@
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 google.registry.request.Action.Method.PUT;
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;
@@ -28,8 +32,10 @@ import com.google.api.services.directory.model.UserName;
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.RegistrarRole;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.persistence.VKey;
@@ -38,11 +44,16 @@ 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.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.IntStream;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Named;
@@ -50,36 +61,40 @@ import javax.inject.Named;
service = Action.GaeService.DEFAULT,
gkeService = GkeService.CONSOLE,
path = ConsoleUsersAction.PATH,
method = {GET, POST},
method = {GET, POST, DELETE, PUT},
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 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<UserData> userData;
private final Optional<String> 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<String> maybeGroupEmailAddress,
@Named("base58StringGenerator") StringGenerator passwordGenerator,
@Parameter("userData") Optional<UserData> userData,
@Parameter("registrarId") String registrarId) {
super(consoleApiParams);
this.gson = gson;
this.registrarId = registrarId;
this.directory = directory;
this.passwordGenerator = passwordGenerator;
}
private static String generateNewEmailAddress(User user, String increment) {
List<String> emailParts = EMAIL_SPLITTER.splitToList(user.getEmailAddress());
return String.format("%s-%s@%s", emailParts.get(0), increment, emailParts.get(1));
this.userData = userData;
this.maybeGroupEmailAddress = maybeGroupEmailAddress;
this.iamClient = iamClient;
this.gSuiteDomainName = gSuiteDomainName;
}
@Override
@@ -87,7 +102,23 @@ public class ConsoleUsersAction extends ConsoleApiAction {
// Temporary flag while testing
if (user.getUserRoles().isAdmin()) {
checkPermission(user, registrarId, ConsolePermission.MANAGE_USERS);
tm().transact(() -> runInTransaction(user));
if (userData.isPresent()) { // Adding existing user to registrar
tm().transact(this::runAppendUserInTransaction);
} else { // Adding new user to registrar
tm().transact(this::runCreateInTransaction);
}
} else {
consoleApiParams.response().setStatus(SC_FORBIDDEN);
}
}
@Override
protected void putHandler(User user) {
// Temporary flag while testing
if (user.getUserRoles().isAdmin()) {
checkPermission(user, registrarId, ConsolePermission.MANAGE_USERS);
tm().transact(this::runUpdateInTransaction);
} else {
consoleApiParams.response().setStatus(SC_FORBIDDEN);
}
@@ -96,40 +127,98 @@ public class ConsoleUsersAction extends ConsoleApiAction {
@Override
protected void getHandler(User user) {
checkPermission(user, registrarId, ConsolePermission.MANAGE_USERS);
List<ImmutableMap> users =
getAllUsers().stream()
.filter(u -> u.getUserRoles().getRegistrarRoles().containsKey(registrarId))
List<UserData> users =
getAllRegistrarUsers(registrarId).stream()
.map(
u ->
ImmutableMap.of(
"emailAddress",
new UserData(
u.getEmailAddress(),
"role",
u.getUserRoles().getRegistrarRoles().get(registrarId)))
u.getUserRoles().getRegistrarRoles().get(registrarId).toString(),
null))
.collect(Collectors.toList());
consoleApiParams.response().setPayload(gson.toJson(users));
consoleApiParams.response().setPayload(consoleApiParams.gson().toJson(users));
consoleApiParams.response().setStatus(SC_OK);
}
private void runInTransaction(User user) throws IOException {
String nextAvailableIncrement =
Stream.of("1", "2", "3")
.filter(
increment ->
tm().loadByKeyIfPresent(
VKey.create(User.class, generateNewEmailAddress(user, increment)))
.isEmpty())
@Override
protected void deleteHandler(User user) {
// Temporary flag while testing
if (user.getUserRoles().isAdmin()) {
checkPermission(user, registrarId, ConsolePermission.MANAGE_USERS);
tm().transact(this::runDeleteInTransaction);
} else {
consoleApiParams.response().setStatus(SC_FORBIDDEN);
}
}
private void runAppendUserInTransaction() {
if (!isModifyingRequestValid(false)) {
return;
}
ImmutableList<User> allRegistrarUsers = getAllRegistrarUsers(registrarId);
if (allRegistrarUsers.size() >= 4)
throw new BadRequestException("Total users amount per registrar is limited to 4");
updateUserRegistrarRoles(
this.userData.get().emailAddress,
registrarId,
RegistrarRole.valueOf(this.userData.get().role),
false);
consoleApiParams.response().setStatus(SC_OK);
}
private void runDeleteInTransaction() throws IOException {
if (!isModifyingRequestValid(true)) {
return;
}
String email = this.userData.get().emailAddress;
User updatedUser =
updateUserRegistrarRoles(
email, registrarId, RegistrarRole.valueOf(this.userData.get().role), true);
// User has no registrars assigned
if (updatedUser.getUserRoles().getRegistrarRoles().size() == 0) {
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()
.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,26 +235,94 @@ 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(
gson.toJson(
ImmutableMap.of(
"password",
newUser.getPassword(),
"emailAddress",
newUser.getPrimaryEmail(),
"role",
ACCOUNT_MANAGER)));
consoleApiParams
.gson()
.toJson(
new UserData(
newUser.getPrimaryEmail(),
ACCOUNT_MANAGER.toString(),
newUser.getPassword())));
}
private ImmutableList<User> getAllUsers() {
private void runUpdateInTransaction() {
if (!isModifyingRequestValid(true)) {
return;
}
updateUserRegistrarRoles(
this.userData.get().emailAddress,
registrarId,
RegistrarRole.valueOf(this.userData.get().role),
false);
consoleApiParams.response().setStatus(SC_OK);
}
private boolean isModifyingRequestValid(boolean verifyAccess) {
if (userData.isEmpty()
|| isNullOrEmpty(userData.get().emailAddress)
|| isNullOrEmpty(userData.get().role)) {
throw new BadRequestException("User data is missing or incomplete");
}
String email = userData.get().emailAddress;
User userToUpdate =
tm().loadByKeyIfPresent(VKey.create(User.class, email))
.orElseThrow(
() -> new BadRequestException(String.format("User %s doesn't exist", email)));
if (verifyAccess && !userToUpdate.getUserRoles().getRegistrarRoles().containsKey(registrarId)) {
setFailedResponse(
String.format("Can't update user not associated with registrarId %s", registrarId),
SC_FORBIDDEN);
return false;
}
return true;
}
private User updateUserRegistrarRoles(
String email, String registrarId, RegistrarRole newRole, boolean isDelete) {
User userToUpdate = tm().loadByKeyIfPresent(VKey.create(User.class, email)).get();
Map<String, RegistrarRole> updatedRegistrarRoles;
if (isDelete) {
updatedRegistrarRoles =
userToUpdate.getUserRoles().getRegistrarRoles().entrySet().stream()
.filter(entry -> !Objects.equals(entry.getKey(), registrarId))
.collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
} else {
updatedRegistrarRoles =
ImmutableMap.<String, RegistrarRole>builder()
.putAll(userToUpdate.getUserRoles().getRegistrarRoles())
.put(registrarId, newRole)
.buildKeepingLast();
}
var updatedUser =
userToUpdate
.asBuilder()
.setUserRoles(
userToUpdate
.getUserRoles()
.asBuilder()
.setRegistrarRoles(updatedRegistrarRoles)
.build())
.build();
tm().put(updatedUser);
return updatedUser;
}
private ImmutableList<User> 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 UserData(
@Expose String emailAddress, @Expose String role, @Expose @Nullable String password) {}
}

View File

@@ -26,8 +26,9 @@ import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
import com.google.gson.Gson;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.ConsoleUpdateHistory;
import google.registry.model.console.RegistrarUpdateHistory;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarBase;
@@ -62,7 +63,6 @@ public class RegistrarsAction extends ConsoleApiAction {
WHERE registrar_id in :registrarIds
""";
static final String PATH = "/console-api/registrars";
private final Gson gson;
private final Optional<Registrar> registrar;
private final StringGenerator passwordGenerator;
private final StringGenerator passcodeGenerator;
@@ -70,12 +70,10 @@ public class RegistrarsAction extends ConsoleApiAction {
@Inject
public RegistrarsAction(
ConsoleApiParams consoleApiParams,
Gson gson,
@Parameter("registrar") Optional<Registrar> registrar,
@Named("base58StringGenerator") StringGenerator passwordGenerator,
@Named("digitOnlyStringGenerator") StringGenerator passcodeGenerator) {
super(consoleApiParams);
this.gson = gson;
this.registrar = registrar;
this.passcodeGenerator = passcodeGenerator;
this.passwordGenerator = passwordGenerator;
@@ -88,7 +86,7 @@ public class RegistrarsAction extends ConsoleApiAction {
Streams.stream(Registrar.loadAll())
.filter(r -> allowedRegistrarTypes.contains(r.getType()))
.collect(ImmutableList.toImmutableList());
consoleApiParams.response().setPayload(gson.toJson(registrars));
consoleApiParams.response().setPayload(consoleApiParams.gson().toJson(registrars));
consoleApiParams.response().setStatus(SC_OK);
} else if (user.getUserRoles().getRegistrarRoles().values().stream()
.anyMatch(role -> role.hasPermission(ConsolePermission.VIEW_REGISTRAR_DETAILS))) {
@@ -106,7 +104,7 @@ public class RegistrarsAction extends ConsoleApiAction {
.setParameter("registrarIds", accessibleRegistrarIds)
.getResultList());
consoleApiParams.response().setPayload(gson.toJson(registrars));
consoleApiParams.response().setPayload(consoleApiParams.gson().toJson(registrars));
consoleApiParams.response().setStatus(SC_OK);
} else {
consoleApiParams.response().setStatus(SC_FORBIDDEN);
@@ -175,6 +173,11 @@ public class RegistrarsAction extends ConsoleApiAction {
"Registrar with registrarId %s already exists",
registrar.getRegistrarId());
tm().putAll(registrar, contact);
finishAndPersistConsoleUpdateHistory(
new RegistrarUpdateHistory.Builder()
.setType(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE)
.setRegistrar(registrar)
.setRequestBody(consoleApiParams.gson().toJson(registrar)));
});
}

View File

@@ -28,7 +28,6 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.google.common.flogger.FluentLogger;
import com.google.gson.Gson;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
@@ -60,18 +59,15 @@ import javax.inject.Inject;
public class ContactAction extends ConsoleApiAction {
static final String PATH = "/console-api/settings/contacts";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final Gson gson;
private final Optional<ImmutableSet<RegistrarPoc>> contacts;
private final String registrarId;
@Inject
public ContactAction(
ConsoleApiParams consoleApiParams,
Gson gson,
@Parameter("registrarId") String registrarId,
@Parameter("contacts") Optional<ImmutableSet<RegistrarPoc>> contacts) {
super(consoleApiParams);
this.gson = gson;
this.registrarId = registrarId;
this.contacts = contacts;
}
@@ -90,7 +86,7 @@ public class ContactAction extends ConsoleApiAction {
.collect(toImmutableList()));
consoleApiParams.response().setStatus(SC_OK);
consoleApiParams.response().setPayload(gson.toJson(am));
consoleApiParams.response().setPayload(consoleApiParams.gson().toJson(am));
}
@Override

View File

@@ -25,6 +25,8 @@ import com.google.common.collect.ImmutableSet;
import google.registry.flows.certs.CertificateChecker;
import google.registry.flows.certs.CertificateChecker.InsecureCertificateException;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.ConsoleUpdateHistory;
import google.registry.model.console.RegistrarUpdateHistory;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.request.Action;
@@ -117,6 +119,11 @@ public class SecurityAction extends ConsoleApiAction {
Registrar updatedRegistrar = updatedRegistrarBuilder.build();
tm().put(updatedRegistrar);
finishAndPersistConsoleUpdateHistory(
new RegistrarUpdateHistory.Builder()
.setType(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE)
.setRegistrar(updatedRegistrar)
.setRequestBody(consoleApiParams.gson().toJson(registrar.get())));
sendExternalUpdatesIfNecessary(
EmailInfo.create(savedRegistrar, updatedRegistrar, ImmutableSet.of(), ImmutableSet.of()));

View File

@@ -22,6 +22,8 @@ import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.ConsoleUpdateHistory;
import google.registry.model.console.RegistrarUpdateHistory;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.request.Action;
@@ -104,6 +106,11 @@ public class WhoisRegistrarFieldsAction extends ConsoleApiAction {
.setEmailAddress(providedRegistrar.getEmailAddress())
.build();
tm().put(newRegistrar);
finishAndPersistConsoleUpdateHistory(
new RegistrarUpdateHistory.Builder()
.setType(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE)
.setRegistrar(newRegistrar)
.setRequestBody(consoleApiParams.gson().toJson(registrar.get())));
sendExternalUpdatesIfNecessary(
EmailInfo.create(
savedRegistrar,

View File

@@ -121,7 +121,7 @@ class ExtensionManagerTest {
void testMetadataExtension_forbiddenWhenNotToolSource() {
ExtensionManager manager =
new TestInstanceBuilder()
.setEppRequestSource(EppRequestSource.CONSOLE)
.setEppRequestSource(EppRequestSource.TLS)
.setDeclaredUris()
.setSuppliedExtensions(MetadataExtension.class)
.build();

View File

@@ -1247,4 +1247,15 @@ class DomainDeleteFlowTest extends ResourceFlowTestCase<DomainDeleteFlow, Domain
clock.advanceOneMilli();
runFlowAssertResponse(loadFile("domain_delete_response_fee_free_grace.xml"));
}
@Test
void testSuccess_skipsPollMessage_whenConfigured() throws Exception {
setUpSuccessfulTest();
domain =
persistResource(
domain.asBuilder().setPersistedCurrentSponsorRegistrarId("NewRegistrar").build());
setRegistrarIdForFlow("NewRegistrar");
runFlowAssertResponse(loadFile("domain_delete_response_pending.xml"));
assertPollMessages();
}
}

View File

@@ -74,6 +74,7 @@ import google.registry.model.reporting.DomainTransactionRecord;
import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.tld.Tld;
import google.registry.persistence.VKey;
import google.registry.testing.DatabaseHelper;
import java.util.Map;
import java.util.Optional;
@@ -103,12 +104,12 @@ class DomainRestoreRequestFlowTest extends ResourceFlowTestCase<DomainRestoreReq
setEppInput("domain_update_restore_request.xml", ImmutableMap.of("DOMAIN", "example.tld"));
}
void persistPendingDeleteDomain() throws Exception {
Domain persistPendingDeleteDomain() throws Exception {
// The domain is now past what had been its expiration date at the time of deletion.
persistPendingDeleteDomain(clock.nowUtc().minusDays(5));
return persistPendingDeleteDomain(clock.nowUtc().minusDays(5));
}
void persistPendingDeleteDomain(DateTime expirationTime) throws Exception {
Domain persistPendingDeleteDomain(DateTime expirationTime) throws Exception {
Domain domain = persistResource(DatabaseHelper.newDomain(getUniqueIdFromCommand()));
HistoryEntry historyEntry =
persistResource(
@@ -118,29 +119,31 @@ class DomainRestoreRequestFlowTest extends ResourceFlowTestCase<DomainRestoreReq
.setRegistrarId(domain.getCurrentSponsorRegistrarId())
.setDomain(domain)
.build());
persistResource(
domain
.asBuilder()
.setRegistrationExpirationTime(expirationTime)
.setDeletionTime(clock.nowUtc().plusDays(35))
.addGracePeriod(
GracePeriod.create(
GracePeriodStatus.REDEMPTION,
domain.getRepoId(),
clock.nowUtc().plusDays(1),
"TheRegistrar",
null))
.setStatusValues(ImmutableSet.of(StatusValue.PENDING_DELETE))
.setDeletePollMessage(
persistResource(
new PollMessage.OneTime.Builder()
.setRegistrarId("TheRegistrar")
.setEventTime(clock.nowUtc().plusDays(5))
.setHistoryEntry(historyEntry)
.build())
.createVKey())
.build());
domain =
persistResource(
domain
.asBuilder()
.setRegistrationExpirationTime(expirationTime)
.setDeletionTime(clock.nowUtc().plusDays(35))
.addGracePeriod(
GracePeriod.create(
GracePeriodStatus.REDEMPTION,
domain.getRepoId(),
clock.nowUtc().plusDays(1),
"TheRegistrar",
null))
.setStatusValues(ImmutableSet.of(StatusValue.PENDING_DELETE))
.setDeletePollMessage(
persistResource(
new PollMessage.OneTime.Builder()
.setRegistrarId("TheRegistrar")
.setEventTime(clock.nowUtc().plusDays(5))
.setHistoryEntry(historyEntry)
.build())
.createVKey())
.build());
clock.advanceOneMilli();
return domain;
}
@Test
@@ -491,6 +494,15 @@ class DomainRestoreRequestFlowTest extends ResourceFlowTestCase<DomainRestoreReq
loadFile("domain_update_restore_request_response_premium.xml"));
}
@Test
void testSuccess_worksWithoutPollMessage() throws Exception {
Domain domain = persistPendingDeleteDomain();
VKey<PollMessage.OneTime> deletePollMessage = domain.getDeletePollMessage();
persistResource(domain.asBuilder().setDeletePollMessage(null).build());
DatabaseHelper.deleteByKey(deletePollMessage);
runFlowAssertResponse(loadFile("generic_success_response.xml"));
}
@Test
void testFailure_doesNotExist() throws Exception {
ResourceDoesNotExistException thrown =

View File

@@ -20,6 +20,7 @@ import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableList;
import google.registry.groups.GmailClient;
import google.registry.model.console.User;
import google.registry.request.RequestModule;
import google.registry.request.auth.AuthResult;
import google.registry.security.XsrfTokenManager;
import google.registry.ui.server.SendEmailUtils;
@@ -45,7 +46,13 @@ public final class ConsoleApiParamsUtils {
xsrfTokenManager.generateToken(
authResult.user().map(User::getEmailAddress).orElse("")))
});
when(request.getRequestURI()).thenReturn("/console/fake-url");
return ConsoleApiParams.create(
request, new FakeResponse(), authResult, sendEmailUtils, xsrfTokenManager);
request,
new FakeResponse(),
authResult,
sendEmailUtils,
xsrfTokenManager,
RequestModule.provideGson());
}
}

View File

@@ -1168,6 +1168,10 @@ public final class DatabaseHelper {
tm().transact(() -> tm().delete(resource));
}
public static void deleteByKey(VKey<?> key) {
tm().transact(() -> tm().delete(key));
}
/** Force the create and update timestamps to get written into the resource. */
public static <R> R cloneAndSetAutoTimestamps(final R resource) {
// We have to separate the read and write operation into different transactions otherwise JPA
@@ -1294,6 +1298,11 @@ public final class DatabaseHelper {
return tm().transact(() -> tm().loadByEntitiesIfPresent(entities));
}
/** Loads the only instance of this particular class, or empty if none exists. */
public static <T> Optional<T> loadSingleton(Class<T> clazz) {
return tm().transact(() -> tm().loadSingleton(clazz));
}
/** Returns whether or not the given entity exists in Cloud SQL. */
public static boolean existsInDb(ImmutableObject object) {
return tm().transact(() -> tm().exists(object));

View File

@@ -91,10 +91,16 @@ public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
runCommandForced(
"--email",
"user@example.test",
"--registrar_roles",
"TheRegistrar=PRIMARY_CONTACT",
"--registry_lock_email_address",
"registrylockemail@otherexample.test");
assertThat(loadExistingUser("user@example.test").getRegistryLockEmailAddress())
.hasValue("registrylockemail@otherexample.test");
"registrylockemail@otherexample.test",
"--registry_lock_password",
"password");
User user = loadExistingUser("user@example.test");
assertThat(user.getRegistryLockEmailAddress()).hasValue("registrylockemail@otherexample.test");
assertThat(user.verifyRegistryLockPassword("password")).isTrue();
assertThat(user.verifyRegistryLockPassword("foobar")).isFalse();
}
@Test
@@ -174,4 +180,18 @@ public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
.hasMessageThat()
.isEqualTo("Provided email this is not valid is not a valid email address");
}
@Test
void testFailure_registryLockPassword_withoutEmail() {
assertThat(
assertThrows(
IllegalArgumentException.class,
() ->
runCommandForced(
"--email", "user@example.test", "--registry_lock_password", "password")))
.hasMessageThat()
.isEqualTo(
"Cannot set/remove registry lock password on a user without a registry lock email"
+ " address");
}
}

View File

@@ -16,6 +16,7 @@ package google.registry.tools;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.DatabaseHelper.loadExistingUser;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.DatabaseHelper.putInDb;
import static org.junit.Assert.assertThrows;
@@ -98,6 +99,44 @@ public class UpdateUserCommandTest extends CommandTestCase<UpdateUserCommand> {
.isEqualTo(GlobalRole.FTE);
}
@Test
void testSuccess_removePassword() throws Exception {
// Empty password value removes the password
persistResource(
loadExistingUser("user@example.test")
.asBuilder()
.setUserRoles(
new UserRoles.Builder()
.setRegistrarRoles(ImmutableMap.of("TheRegistrar", RegistrarRole.TECH_CONTACT))
.build())
.setRegistryLockEmailAddress("registrylock@example.test")
.setRegistryLockPassword("password")
.build());
assertThat(loadExistingUser("user@example.test").hasRegistryLockPassword()).isTrue();
runCommandForced("--email", "user@example.test", "--registry_lock_password", "");
assertThat(loadExistingUser("user@example.test").hasRegistryLockPassword()).isFalse();
}
@Test
void testSuccess_setsPassword() throws Exception {
persistResource(
loadExistingUser("user@example.test")
.asBuilder()
.setUserRoles(
new UserRoles.Builder()
.setRegistrarRoles(ImmutableMap.of("TheRegistrar", RegistrarRole.TECH_CONTACT))
.build())
.setRegistryLockEmailAddress("registrylock@example.test")
.setRegistryLockPassword("password")
.build());
assertThat(loadExistingUser("user@example.test").verifyRegistryLockPassword("password"))
.isTrue();
runCommandForced("--email", "user@example.test", "--registry_lock_password", "foobar");
assertThat(loadExistingUser("user@example.test").verifyRegistryLockPassword("password"))
.isFalse();
assertThat(loadExistingUser("user@example.test").verifyRegistryLockPassword("foobar")).isTrue();
}
@Test
void testFailure_doesntExist() {
assertThat(
@@ -122,4 +161,18 @@ public class UpdateUserCommandTest extends CommandTestCase<UpdateUserCommand> {
.hasMessageThat()
.isEqualTo("Provided email this is not valid is not a valid email address");
}
@Test
void testFailure_setPassword_noEmail() {
assertThat(
assertThrows(
IllegalArgumentException.class,
() ->
runCommandForced(
"--email", "user@example.test", "--registry_lock_password", "foobar")))
.hasMessageThat()
.isEqualTo(
"Cannot set/remove registry lock password on a user without a registry lock email"
+ " address");
}
}

View File

@@ -0,0 +1,242 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.ui.server.console;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL;
import static google.registry.model.common.FeatureFlag.FeatureStatus.INACTIVE;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.loadByEntity;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistDomainWithDependentResources;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import google.registry.flows.DaggerEppTestComponent;
import google.registry.flows.EppController;
import google.registry.flows.EppTestComponent;
import google.registry.model.common.FeatureFlag;
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.model.domain.Domain;
import google.registry.model.eppcommon.StatusValue;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.request.auth.AuthResult;
import google.registry.testing.ConsoleApiParamsUtils;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeResponse;
import google.registry.tools.GsonUtils;
import java.util.Map;
import java.util.Optional;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Tests for {@link ConsoleBulkDomainAction}. */
public class ConsoleBulkDomainActionTest {
private static final Gson GSON = GsonUtils.provideGson();
private final FakeClock clock = new FakeClock(DateTime.parse("2024-05-13T00:00:00.000Z"));
@RegisterExtension
final JpaTestExtensions.JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension();
private EppController eppController;
private FakeResponse fakeResponse;
private Domain domain;
@BeforeEach
void beforeEach() {
persistResource(
new FeatureFlag()
.asBuilder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
.setStatusMap(ImmutableSortedMap.of(START_OF_TIME, INACTIVE))
.build());
eppController =
DaggerEppTestComponent.builder()
.fakesAndMocksModule(EppTestComponent.FakesAndMocksModule.create(clock))
.build()
.startRequest()
.eppController();
createTld("tld");
domain =
persistDomainWithDependentResources(
"example",
"tld",
persistActiveContact("contact1234"),
clock.nowUtc(),
clock.nowUtc().minusMonths(1),
clock.nowUtc().plusMonths(11));
}
@Test
void testSuccess_delete() {
ConsoleBulkDomainAction action =
createAction(
"DELETE",
GSON.toJsonTree(
ImmutableMap.of("domainList", ImmutableList.of("example.tld"), "reason", "test")));
action.run();
assertThat(fakeResponse.getStatus()).isEqualTo(SC_OK);
assertThat(fakeResponse.getPayload())
.isEqualTo(
"{\"example.tld\":{\"message\":\"Command completed"
+ " successfully\",\"responseCode\":1000}}");
assertThat(loadByEntity(domain).getDeletionTime()).isEqualTo(clock.nowUtc());
}
@Test
void testSuccess_suspend() throws Exception {
User adminUser =
persistResource(
new User.Builder()
.setEmailAddress("email@email.com")
.setUserRoles(
new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).setIsAdmin(true).build())
.build());
ConsoleBulkDomainAction action =
createAction(
"SUSPEND",
GSON.toJsonTree(
ImmutableMap.of("domainList", ImmutableList.of("example.tld"), "reason", "test")),
adminUser);
action.run();
assertThat(fakeResponse.getStatus()).isEqualTo(SC_OK);
assertThat(fakeResponse.getPayload())
.isEqualTo(
"{\"example.tld\":{\"message\":\"Command completed"
+ " successfully\",\"responseCode\":1000}}");
assertThat(loadByEntity(domain).getStatusValues())
.containsAtLeast(
StatusValue.SERVER_RENEW_PROHIBITED,
StatusValue.SERVER_TRANSFER_PROHIBITED,
StatusValue.SERVER_UPDATE_PROHIBITED,
StatusValue.SERVER_DELETE_PROHIBITED,
StatusValue.SERVER_HOLD);
}
@Test
void testHalfSuccess_halfNonexistent() throws Exception {
ConsoleBulkDomainAction action =
createAction(
"DELETE",
GSON.toJsonTree(
ImmutableMap.of(
"domainList",
ImmutableList.of("example.tld", "nonexistent.tld"),
"reason",
"test")));
action.run();
assertThat(fakeResponse.getStatus()).isEqualTo(SC_OK);
assertThat(fakeResponse.getPayload())
.isEqualTo(
"{\"example.tld\":{\"message\":\"Command completed"
+ " successfully\",\"responseCode\":1000},\"nonexistent.tld\":{\"message\":\"The"
+ " domain with given ID (nonexistent.tld) doesn\\u0027t"
+ " exist.\",\"responseCode\":2303}}");
assertThat(loadByEntity(domain).getDeletionTime()).isEqualTo(clock.nowUtc());
}
@Test
void testFailure_badActionString() {
ConsoleBulkDomainAction action = createAction("bad", null);
action.run();
assertThat(fakeResponse.getStatus()).isEqualTo(SC_BAD_REQUEST);
assertThat(fakeResponse.getPayload())
.isEqualTo(
"No enum constant"
+ " google.registry.ui.server.console.ConsoleBulkDomainAction.BulkAction.bad");
}
@Test
void testFailure_emptyBody() {
ConsoleBulkDomainAction action = createAction("DELETE", null);
action.run();
assertThat(fakeResponse.getStatus()).isEqualTo(SC_BAD_REQUEST);
assertThat(fakeResponse.getPayload()).isEqualTo("Bulk action payload must be present");
}
@Test
void testFailure_noPermission() {
JsonElement payload =
GSON.toJsonTree(ImmutableMap.of("domainList", ImmutableList.of("domain.tld")));
ConsoleBulkDomainAction action =
createAction(
"DELETE",
payload,
new User.Builder()
.setEmailAddress("foobar@theregistrar.com")
.setUserRoles(
new UserRoles.Builder()
.setRegistrarRoles(
ImmutableMap.of("TheRegistrar", RegistrarRole.ACCOUNT_MANAGER))
.build())
.build());
action.run();
assertThat(fakeResponse.getStatus()).isEqualTo(SC_FORBIDDEN);
}
@Test
void testFailure_suspend_nonAdmin() {
ConsoleBulkDomainAction action =
createAction(
"SUSPEND",
GSON.toJsonTree(
ImmutableMap.of("domainList", ImmutableList.of("example.tld"), "reason", "test")));
action.run();
assertThat(fakeResponse.getStatus()).isEqualTo(SC_OK);
Map<String, ConsoleBulkDomainAction.ConsoleEppOutput> payload =
GSON.fromJson(fakeResponse.getPayload(), new TypeToken<>() {});
assertThat(payload).containsKey("example.tld");
assertThat(payload.get("example.tld").responseCode()).isEqualTo(2004);
assertThat(payload.get("example.tld").message()).contains("cannot be set by clients");
assertThat(loadByEntity(domain)).isEqualTo(domain);
}
private ConsoleBulkDomainAction createAction(String action, JsonElement payload) {
User user =
persistResource(
new User.Builder()
.setEmailAddress("email@email.com")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build())
.build());
return createAction(action, payload, user);
}
private ConsoleBulkDomainAction createAction(String action, JsonElement payload, User user) {
AuthResult authResult = AuthResult.createUser(user);
ConsoleApiParams params = ConsoleApiParamsUtils.createFake(authResult);
when(params.request().getMethod()).thenReturn("POST");
fakeResponse = (FakeResponse) params.response();
return new ConsoleBulkDomainAction(
params, eppController, "TheRegistrar", action, Optional.ofNullable(payload));
}
}

View File

@@ -22,13 +22,11 @@ import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import google.registry.model.console.RegistrarRole;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.request.Action;
import google.registry.request.RequestModule;
import google.registry.request.auth.AuthResult;
import google.registry.testing.ConsoleApiParamsUtils;
import google.registry.testing.DatabaseHelper;
@@ -40,7 +38,6 @@ import org.junit.jupiter.api.extension.RegisterExtension;
/** Tests for {@link google.registry.ui.server.console.ConsoleDomainGetAction}. */
public class ConsoleDomainGetActionTest {
private static final Gson GSON = RequestModule.provideGson();
private ConsoleApiParams consoleApiParams;
@RegisterExtension
@@ -124,6 +121,6 @@ public class ConsoleDomainGetActionTest {
private ConsoleDomainGetAction createAction(AuthResult authResult, String domain) {
consoleApiParams = ConsoleApiParamsUtils.createFake(authResult);
when(consoleApiParams.request().getMethod()).thenReturn(Action.Method.GET.toString());
return new ConsoleDomainGetAction(consoleApiParams, GSON, domain);
return new ConsoleDomainGetAction(consoleApiParams, domain);
}
}

View File

@@ -264,7 +264,6 @@ public class ConsoleDomainListActionTest {
when(consoleApiParams.request().getMethod()).thenReturn(Action.Method.GET.toString());
return new ConsoleDomainListAction(
consoleApiParams,
GSON,
registrarId,
Optional.ofNullable(checkpointTime),
Optional.ofNullable(pageNumber),

View File

@@ -21,7 +21,6 @@ import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableList;
import com.google.gson.Gson;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
@@ -32,7 +31,6 @@ import google.registry.testing.ConsoleApiParamsUtils;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeResponse;
import google.registry.tools.GsonUtils;
import java.io.IOException;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
@@ -41,8 +39,6 @@ import org.junit.jupiter.api.extension.RegisterExtension;
class ConsoleDumDownloadActionTest {
private static final Gson GSON = GsonUtils.provideGson();
private final FakeClock clock = new FakeClock(DateTime.parse("2024-04-15T00:00:00.000Z"));
private ConsoleApiParams consoleApiParams;

View File

@@ -17,6 +17,7 @@ package google.registry.ui.server.console;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.request.auth.AuthenticatedRegistrarAccessor.Role.OWNER;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.loadSingleton;
import static google.registry.testing.DatabaseHelper.persistResource;
import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
@@ -32,6 +33,7 @@ import com.google.common.collect.ImmutableSetMultimap;
import com.google.gson.Gson;
import google.registry.flows.PasswordOnlyTransportCredentials;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.RegistrarUpdateHistory;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.model.registrar.Registrar;
@@ -41,6 +43,7 @@ import google.registry.request.RequestModule;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.AuthenticatedRegistrarAccessor;
import google.registry.testing.ConsoleApiParamsUtils;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.FakeResponse;
import google.registry.tools.GsonUtils;
import google.registry.ui.server.console.ConsoleEppPasswordAction.EppPasswordData;
@@ -139,6 +142,10 @@ class ConsoleEppPasswordActionTest {
() -> {
credentials.validate(loadRegistrar("TheRegistrar"), "randomPassword");
});
assertThat(loadSingleton(RegistrarUpdateHistory.class).get().getRequestBody())
.isEqualTo(
"{\"registrarId\":\"TheRegistrar\",\"oldPassword\":\"********\",\"newPassword\":"
+ "\"••••••••\",\"newPasswordRepeat\":\"••••••••\"}");
}
private ConsoleEppPasswordAction createAction(
@@ -150,6 +157,7 @@ class ConsoleEppPasswordActionTest {
.setEmailAddress("email@email.com")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build())
.build();
DatabaseHelper.putInDb(user);
AuthResult authResult = AuthResult.createUser(user);
consoleApiParams = ConsoleApiParamsUtils.createFake(authResult);

View File

@@ -245,7 +245,6 @@ class ConsoleOteActionTest {
when(consoleApiParams.request().getMethod()).thenReturn(method.toString());
return new ConsoleOteAction(
consoleApiParams,
GSON,
iamClient,
registrarId,
maybeGroupEmailAddress,

View File

@@ -34,7 +34,6 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import google.registry.groups.GmailClient;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.RegistrarRole;
@@ -44,7 +43,6 @@ import google.registry.model.domain.Domain;
import google.registry.model.domain.RegistryLock;
import google.registry.model.eppcommon.StatusValue;
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;
@@ -83,8 +81,6 @@ public class ConsoleRegistryLockActionTest {
https://registrarconsole.tld/console/#/registry-lock-verify?lockVerificationCode=\
123456789ABCDEFGHJKLMNPQRSTUVWXY""";
private static final Gson GSON = RequestModule.provideGson();
private final FakeClock fakeClock = new FakeClock(DateTime.parse("2024-04-18T12:00:00.000Z"));
@RegisterExtension
@@ -128,10 +124,10 @@ public class ConsoleRegistryLockActionTest {
assertThat(response.getPayload())
.isEqualTo(
"""
[{"domainName":"example.test","registrarPocId":"johndoe@theregistrar.com","lockRequestTime":\
{"creationTime":"2024-04-18T12:00:00.000Z"},"unlockRequestTime":"null","lockCompletionTime":\
"2024-04-18T12:00:00.000Z","unlockCompletionTime":"null","isSuperuser":false}]\
""");
[{"domainName":"example.test","registrarPocId":"johndoe@theregistrar.com","lockRequestTime":\
{"creationTime":"2024-04-18T12:00:00.000Z"},"unlockRequestTime":"null","lockCompletionTime":\
"2024-04-18T12:00:00.000Z","unlockCompletionTime":"null","isSuperuser":false}]\
""");
}
@Test
@@ -222,25 +218,25 @@ public class ConsoleRegistryLockActionTest {
assertThat(response.getPayload())
.isEqualTo(
"""
[{"domainName":"adminexample.test","lockRequestTime":{"creationTime":"2024-04-19T12:00:00.001Z"},\
"unlockRequestTime":"null","lockCompletionTime":"2024-04-19T12:00:00.001Z","unlockCompletionTime":\
"null","isSuperuser":true},\
\
{"domainName":"example.test","registrarPocId":"johndoe@theregistrar.com","lockRequestTime":\
{"creationTime":"2024-04-19T12:00:00.001Z"},"unlockRequestTime":"null","lockCompletionTime":\
"2024-04-19T12:00:00.000Z","unlockCompletionTime":"null","isSuperuser":false},\
\
{"domainName":"expiredunlock.test","registrarPocId":"johndoe@theregistrar.com","lockRequestTime":\
{"creationTime":"2024-04-18T12:00:00.000Z"},"unlockRequestTime":"2024-04-18T12:00:00.000Z",\
"lockCompletionTime":"2024-04-18T12:00:00.000Z","unlockCompletionTime":"null","isSuperuser":false},\
\
{"domainName":"incompleteunlock.test","registrarPocId":"johndoe@theregistrar.com","lockRequestTime":\
{"creationTime":"2024-04-19T12:00:00.001Z"},"unlockRequestTime":"2024-04-19T12:00:00.001Z",\
"lockCompletionTime":"2024-04-19T12:00:00.001Z","unlockCompletionTime":"null","isSuperuser":false},\
\
{"domainName":"pending.test","registrarPocId":"johndoe@theregistrar.com","lockRequestTime":\
{"creationTime":"2024-04-19T12:00:00.001Z"},"unlockRequestTime":"null","lockCompletionTime":"null",\
"unlockCompletionTime":"null","isSuperuser":false}]""");
[{"domainName":"adminexample.test","lockRequestTime":{"creationTime":"2024-04-19T12:00:00.001Z"},\
"unlockRequestTime":"null","lockCompletionTime":"2024-04-19T12:00:00.001Z","unlockCompletionTime":\
"null","isSuperuser":true},\
\
{"domainName":"example.test","registrarPocId":"johndoe@theregistrar.com","lockRequestTime":\
{"creationTime":"2024-04-19T12:00:00.001Z"},"unlockRequestTime":"null","lockCompletionTime":\
"2024-04-19T12:00:00.000Z","unlockCompletionTime":"null","isSuperuser":false},\
\
{"domainName":"expiredunlock.test","registrarPocId":"johndoe@theregistrar.com","lockRequestTime":\
{"creationTime":"2024-04-18T12:00:00.000Z"},"unlockRequestTime":"2024-04-18T12:00:00.000Z",\
"lockCompletionTime":"2024-04-18T12:00:00.000Z","unlockCompletionTime":"null","isSuperuser":false},\
\
{"domainName":"incompleteunlock.test","registrarPocId":"johndoe@theregistrar.com","lockRequestTime":\
{"creationTime":"2024-04-19T12:00:00.001Z"},"unlockRequestTime":"2024-04-19T12:00:00.001Z",\
"lockCompletionTime":"2024-04-19T12:00:00.001Z","unlockCompletionTime":"null","isSuperuser":false},\
\
{"domainName":"pending.test","registrarPocId":"johndoe@theregistrar.com","lockRequestTime":\
{"creationTime":"2024-04-19T12:00:00.001Z"},"unlockRequestTime":"null","lockCompletionTime":"null",\
"unlockCompletionTime":"null","isSuperuser":false}]""");
}
@Test
@@ -508,7 +504,7 @@ public class ConsoleRegistryLockActionTest {
new CloudTasksHelper(fakeClock).getTestCloudTasksUtils());
response = (FakeResponse) params.response();
return new ConsoleRegistryLockAction(
params, domainLockUtils, gmailClient, GSON, optionalPostInput, "TheRegistrar");
params, domainLockUtils, gmailClient, optionalPostInput, "TheRegistrar");
}
private ConsoleApiParams createParams() {

View File

@@ -24,7 +24,6 @@ import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STAT
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import google.registry.model.console.RegistrarRole;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
@@ -32,7 +31,6 @@ import google.registry.model.domain.Domain;
import google.registry.model.domain.RegistryLock;
import google.registry.model.eppcommon.StatusValue;
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;
@@ -51,7 +49,6 @@ import org.junit.jupiter.api.extension.RegisterExtension;
public class ConsoleRegistryLockVerifyActionTest {
private static final String DEFAULT_CODE = "123456789ABCDEFGHJKLMNPQRSTUUUUU";
private static final Gson GSON = RequestModule.provideGson();
private final FakeClock fakeClock = new FakeClock();
@RegisterExtension
@@ -214,6 +211,6 @@ public class ConsoleRegistryLockVerifyActionTest {
"adminreg",
new CloudTasksHelper(fakeClock).getTestCloudTasksUtils());
response = (FakeResponse) params.response();
return new ConsoleRegistryLockVerifyAction(params, domainLockUtils, GSON, verificationCode);
return new ConsoleRegistryLockVerifyAction(params, domainLockUtils, verificationCode);
}
}

View File

@@ -15,8 +15,10 @@
package google.registry.ui.server.console;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
import static google.registry.model.registrar.RegistrarPocBase.Type.WHOIS;
import static google.registry.testing.DatabaseHelper.createTlds;
import static google.registry.testing.DatabaseHelper.loadSingleton;
import static google.registry.testing.DatabaseHelper.persistResource;
import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
@@ -29,6 +31,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.gson.Gson;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.RegistrarUpdateHistory;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.model.registrar.Registrar;
@@ -85,10 +88,11 @@ class ConsoleUpdateRegistrarActionTest {
.setRegistryLockAllowed(false)
.build());
user =
new User.Builder()
.setEmailAddress("user@registrarId.com")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build())
.build();
persistResource(
new User.Builder()
.setEmailAddress("user@registrarId.com")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build())
.build());
consoleApiParams = createParams();
}
@@ -104,6 +108,9 @@ class ConsoleUpdateRegistrarActionTest {
assertThat(newRegistrar.getAllowedTlds()).containsExactly("app", "dev");
assertThat(newRegistrar.isRegistryLockAllowed()).isFalse();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK);
assertAboutImmutableObjects()
.that(newRegistrar)
.hasFieldsEqualTo(loadSingleton(RegistrarUpdateHistory.class).get().getRegistrar());
}
@Test

View File

@@ -20,6 +20,7 @@ import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import google.registry.model.console.User;
import google.registry.persistence.transaction.JpaTestExtensions;
@@ -71,6 +72,8 @@ class ConsoleUserDataActionTest {
GSON.fromJson(((FakeResponse) consoleApiParams.response()).getPayload(), Map.class);
assertThat(jsonObject)
.containsExactly(
"userRoles",
ImmutableMap.of(),
"isAdmin",
true,
"technicalDocsUrl",

View File

@@ -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.UserData;
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,243 @@ 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\"}");
"{\"emailAddress\":\"TheRegistrar-user1@email.com\",\"role\":\"ACCOUNT_MANAGER\",\"password\":\"abcdefghijklmnop\"}");
}
@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 UserData("test3@test.com", RegistrarRole.ACCOUNT_MANAGER.toString(), null)));
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 update 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 UserData("email-1@email.com", RegistrarRole.ACCOUNT_MANAGER.toString(), null)));
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 UserData("test2@test.com", RegistrarRole.ACCOUNT_MANAGER.toString(), null)));
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 testSuccess_removesRole() 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.persistResource(
new User.Builder()
.setEmailAddress("test4@test.com")
.setUserRoles(
new UserRoles()
.asBuilder()
.setRegistrarRoles(
ImmutableMap.of(
"TheRegistrar",
RegistrarRole.PRIMARY_CONTACT,
"SomeRegistrar",
RegistrarRole.PRIMARY_CONTACT))
.build())
.build());
ConsoleUsersAction action =
createAction(
Optional.of(ConsoleApiParamsUtils.createFake(authResult)),
Optional.of("DELETE"),
Optional.of(
new UserData("test4@test.com", RegistrarRole.ACCOUNT_MANAGER.toString(), null)));
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);
Optional<User> actualUser =
DatabaseHelper.loadByKeyIfPresent(VKey.create(User.class, "test4@test.com"));
assertThat(actualUser).isPresent();
assertThat(actualUser.get().getUserRoles().getRegistrarRoles().containsKey("TheRegistrar"))
.isFalse();
}
@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");
}
@Test
void testSuccess_updatesUserRole() 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());
assertThat(
DatabaseHelper.loadByKey(VKey.create(User.class, "test2@test.com"))
.getUserRoles()
.getRegistrarRoles()
.get("TheRegistrar"))
.isEqualTo(RegistrarRole.PRIMARY_CONTACT);
ConsoleUsersAction action =
createAction(
Optional.of(ConsoleApiParamsUtils.createFake(authResult)),
Optional.of("PUT"),
Optional.of(
new UserData("test2@test.com", RegistrarRole.ACCOUNT_MANAGER.toString(), null)));
action.cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
action.run();
var response = ((FakeResponse) consoleApiParams.response());
assertThat(response.getStatus()).isEqualTo(SC_OK);
assertThat(
DatabaseHelper.loadByKey(VKey.create(User.class, "test2@test.com"))
.getUserRoles()
.getRegistrarRoles()
.get("TheRegistrar"))
.isEqualTo(RegistrarRole.ACCOUNT_MANAGER);
}
@Test
void testFailure_noPermissionToUpdateUser() 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("PUT"),
Optional.of(
new UserData("test3@test.com", RegistrarRole.ACCOUNT_MANAGER.toString(), null)));
action.run();
var response = ((FakeResponse) consoleApiParams.response());
assertThat(response.getStatus()).isEqualTo(SC_FORBIDDEN);
assertThat(response.getPayload())
.contains("Can't update user not associated with registrarId TheRegistrar");
}
private ConsoleUsersAction createAction(
Optional<ConsoleApiParams> maybeConsoleApiParams, Optional<String> method)
Optional<ConsoleApiParams> maybeConsoleApiParams,
Optional<String> method,
Optional<UserData> userData)
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,
directory,
iamClient,
"email.com",
Optional.of("someRandomString"),
passwordGenerator,
userData,
"TheRegistrar");
}
}

View File

@@ -15,8 +15,10 @@
package google.registry.ui.server.console;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
import static google.registry.testing.DatabaseHelper.loadAllOf;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.loadSingleton;
import static google.registry.testing.DatabaseHelper.persistNewRegistrar;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.SqlHelper.saveRegistrar;
@@ -30,6 +32,7 @@ import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.RegistrarRole;
import google.registry.model.console.RegistrarUpdateHistory;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.model.registrar.Registrar;
@@ -183,6 +186,9 @@ class RegistrarsActionTest {
.findAny()
.isPresent())
.isTrue();
assertAboutImmutableObjects()
.that(r)
.isEqualExceptFields(loadSingleton(RegistrarUpdateHistory.class).get().getRegistrar());
}
@Test
@@ -225,10 +231,8 @@ class RegistrarsActionTest {
}
private User createUser(UserRoles userRoles) {
return new User.Builder()
.setEmailAddress("email@email.com")
.setUserRoles(userRoles)
.build();
return persistResource(
new User.Builder().setEmailAddress("email@email.com").setUserRoles(userRoles).build());
}
private RegistrarsAction createAction(Action.Method method, AuthResult authResult) {
@@ -236,7 +240,7 @@ class RegistrarsActionTest {
when(consoleApiParams.request().getMethod()).thenReturn(method.toString());
if (method.equals(Action.Method.GET)) {
return new RegistrarsAction(
consoleApiParams, GSON, Optional.ofNullable(null), passwordGenerator, passcodeGenerator);
consoleApiParams, Optional.ofNullable(null), passwordGenerator, passcodeGenerator);
} else {
try {
doReturn(new BufferedReader(new StringReader(registrarParamMap.toString())))
@@ -245,7 +249,6 @@ class RegistrarsActionTest {
} catch (IOException e) {
return new RegistrarsAction(
consoleApiParams,
GSON,
Optional.ofNullable(null),
passwordGenerator,
passcodeGenerator);
@@ -254,7 +257,7 @@ class RegistrarsActionTest {
ConsoleModule.provideRegistrar(
GSON, RequestModule.provideJsonBody(consoleApiParams.request(), GSON));
return new RegistrarsAction(
consoleApiParams, GSON, maybeRegistrar, passwordGenerator, passcodeGenerator);
consoleApiParams, maybeRegistrar, passwordGenerator, passcodeGenerator);
}
}
}

View File

@@ -480,10 +480,10 @@ class ContactActionTest {
consoleApiParams = ConsoleApiParamsUtils.createFake(authResult);
when(consoleApiParams.request().getMethod()).thenReturn(method.toString());
if (method.equals(Action.Method.GET)) {
return new ContactAction(consoleApiParams, GSON, registrarId, Optional.empty());
return new ContactAction(consoleApiParams, registrarId, Optional.empty());
} else {
return new ContactAction(
consoleApiParams, GSON, registrarId, Optional.of(ImmutableSet.copyOf(contacts)));
consoleApiParams, registrarId, Optional.of(ImmutableSet.copyOf(contacts)));
}
}
}

View File

@@ -15,8 +15,10 @@
package google.registry.ui.server.console.settings;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
import static google.registry.testing.CertificateSamples.SAMPLE_CERT2;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.loadSingleton;
import static google.registry.testing.SqlHelper.saveRegistrar;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
@@ -28,6 +30,7 @@ import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.ImmutableSortedMap;
import com.google.gson.Gson;
import google.registry.flows.certs.CertificateChecker;
import google.registry.model.console.RegistrarUpdateHistory;
import google.registry.model.registrar.Registrar;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.request.Action;
@@ -98,6 +101,9 @@ class SecurityActionTest {
.isEqualTo("GNd6ZP8/n91t9UTnpxR8aH7aAW4+CpvufYx9ViGbcMY");
assertThat(r.getIpAddressAllowList().get(0).getIp()).isEqualTo("192.168.1.1");
assertThat(r.getIpAddressAllowList().get(0).getNetmask()).isEqualTo(32);
assertAboutImmutableObjects()
.that(loadSingleton(RegistrarUpdateHistory.class).get().getRegistrar())
.hasFieldsEqualTo(r);
}
private SecurityAction createAction(AuthResult authResult, String registrarId)

View File

@@ -16,6 +16,7 @@ package google.registry.ui.server.console.settings;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
import static google.registry.testing.DatabaseHelper.loadSingleton;
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static org.mockito.Mockito.doReturn;
@@ -27,6 +28,7 @@ import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import google.registry.model.console.RegistrarRole;
import google.registry.model.console.RegistrarUpdateHistory;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.model.registrar.Registrar;
@@ -129,6 +131,9 @@ public class WhoisRegistrarFieldsActionTest {
.that(newRegistrar)
.isEqualExceptFields(
oldRegistrar, "whoisServer", "url", "localizedAddress", "phoneNumber", "faxNumber");
assertAboutImmutableObjects()
.that(loadSingleton(RegistrarUpdateHistory.class).get().getRegistrar())
.hasFieldsEqualTo(newRegistrar);
}
@Test

View File

@@ -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
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,PUT n USER PUBLIC

View File

@@ -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,PUT n USER PUBLIC

View File

@@ -56,7 +56,7 @@
</rdeHeader:count>
</rdeHeader:header>
<!-- Domian: example1.test -->
<!-- Domain: example1.test -->
<rdeDom:domain>
<rdeDom:name>example1.test</rdeDom:name>
<rdeDom:roid>Dexample1-TEST</rdeDom:roid>
@@ -74,7 +74,7 @@
<rdeDom:exDate>2015-04-03T22:00:00.0Z</rdeDom:exDate>
</rdeDom:domain>
<!-- Domian: example2.test -->
<!-- Domain: example2.test -->
<rdeDom:domain>
<rdeDom:name>example2.test</rdeDom:name>
<rdeDom:roid>Dexample2-TEST</rdeDom:roid>

View File

@@ -56,7 +56,7 @@
</rdeHeader:count>
</rdeHeader:header>
<!-- Domian: example1.test -->
<!-- Domain: example1.test -->
<rdeDom:domain>
<rdeDom:name>example1.test</rdeDom:name>
<rdeDom:roid>Dexample1-TEST</rdeDom:roid>
@@ -74,7 +74,7 @@
<rdeDom:exDate>2015-04-03T22:00:00.0Z</rdeDom:exDate>
</rdeDom:domain>
<!-- Domian: example2.test -->
<!-- Domain: example2.test -->
<rdeDom:domain>
<rdeDom:name>example2.test</rdeDom:name>
<rdeDom:roid>Dexample2-TEST</rdeDom:roid>