1
0
mirror of https://github.com/google/nomulus synced 2026-05-21 23:31:51 +00:00

Compare commits

...

7 Commits

Author SHA1 Message Date
Pavlo Tkach
8987fd37c2 Improve console accessibility (#2649) 2025-01-26 00:47:53 +00:00
gbrodman
653e092ad4 Add TLD identifier to premium terms filename and header (#2644)
https://b.corp.google.com/issues/390053672

This makes it easier to identify what file you're looking at, at a
glance
2025-01-24 19:54:35 +00:00
gbrodman
5e97a8b412 Refactor console domain actions to exist in separate files (#2638)
This means that we're not storing everything in one file, otherwise it
quickly becomes unwieldy
2025-01-23 16:46:53 +00:00
Weimin Yu
229fcf3946 UrlConnectionException loses error info (#2648)
It does not get the error message for 400+ status codes.

It fails to get the status code if the response has neither data nor
error.
2025-01-23 16:27:03 +00:00
Lai Jiang
b775e4a178 Pull credentials from fleet for all clusters (#2647)
All clusters have switched to using private APIs.
2025-01-22 16:58:56 +00:00
Pavlo Tkach
e3c386a8a7 Add console bulk delete (#2641)
* Add bulk actions to console

* Add console bulk delete

* Add console bulk delete
2025-01-22 15:54:59 +00:00
Lai Jiang
799f0449ad Only pull credential from the fleet on crash (#2645)
Only crash has the policy controller installed for now.
2025-01-21 18:40:52 +00:00
56 changed files with 816 additions and 329 deletions

View File

@@ -7,6 +7,11 @@
></mat-progress-bar>
</div>
<mat-sidenav-container class="console-app__container">
<mat-sidenav-content class="console-app__content-wrapper">
<div class="console-app__content" role="main">
<router-outlet></router-outlet>
</div>
</mat-sidenav-content>
<mat-sidenav
[mode]="breakpointObserver.isMobileView() ? 'over' : 'side'"
[opened]="!breakpointObserver.isMobileView()"
@@ -15,10 +20,5 @@
>
<app-navigation />
</mat-sidenav>
<mat-sidenav-content class="console-app__content-wrapper">
<div class="console-app__content">
<router-outlet></router-outlet>
</div>
</mat-sidenav-content>
</mat-sidenav-container>
</div>

View File

@@ -20,12 +20,18 @@ import { AppComponent } from './app.component';
import { MaterialModule } from './material.module';
import { BackendService } from './shared/services/backend.service';
import { AppRoutingModule } from './app-routing.module';
import { AppModule } from './app.module';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AppComponent],
imports: [MaterialModule, BrowserAnimationsModule, AppRoutingModule],
imports: [
MaterialModule,
BrowserAnimationsModule,
AppRoutingModule,
AppModule,
],
providers: [
BackendService,
provideHttpClient(),
@@ -36,6 +42,7 @@ describe('AppComponent', () => {
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});

View File

@@ -26,7 +26,11 @@ import { BackendService } from './shared/services/backend.service';
import { provideHttpClient } from '@angular/common/http';
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
import { BillingInfoComponent } from './billingInfo/billingInfo.component';
import { DomainListComponent } from './domains/domainList.component';
import {
DomainListComponent,
ReasonDialogComponent,
ResponseDialogComponent,
} from './domains/domainList.component';
import { RegistryLockComponent } from './domains/registryLock.component';
import { HeaderComponent } from './header/header.component';
import { HomeComponent } from './home/home.component';
@@ -92,6 +96,8 @@ export class SelectedRegistrarModule {}
TldsComponent,
WhoisComponent,
WhoisEditComponent,
ReasonDialogComponent,
ResponseDialogComponent,
],
bootstrap: [AppComponent],
imports: [

View File

@@ -65,16 +65,66 @@
/>
</mat-form-field>
<div
class="console-app__domains-selection"
[elementId]="getElementIdForBulkDelete()"
[ngClass]="{ active: selection.hasValue() }"
>
<div class="console-app__domains-selection-text">
{{ selection.selected.length }} Selected
</div>
<div class="console-app__domains-selection-actions">
<button
mat-flat-button
aria-label="Delete Selected Domains"
(click)="deleteSelectedDomains()"
>
Delete Selected Domains
</button>
</div>
</div>
<mat-table
[dataSource]="dataSource"
class="mat-elevation-z0"
class="console-app__domains-table"
>
<!-- Checkbox Column -->
<ng-container matColumnDef="select">
<mat-header-cell *matHeaderCellDef>
<mat-checkbox
(change)="$event ? toggleAllRows() : null"
[checked]="selection.hasValue() && isAllSelected"
[indeterminate]="selection.hasValue() && !isAllSelected"
[aria-label]="checkboxLabel()"
[elementId]="getElementIdForBulkDelete()"
>
</mat-checkbox>
</mat-header-cell>
<mat-cell *matCellDef="let row">
<mat-checkbox
(click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null"
[checked]="selection.isSelected(row)"
[aria-label]="checkboxLabel(row)"
[elementId]="getElementIdForBulkDelete()"
>
</mat-checkbox>
</mat-cell>
</ng-container>
<ng-container matColumnDef="domainName">
<mat-header-cell *matHeaderCellDef>Domain Name</mat-header-cell>
<mat-cell *matCellDef="let element">{{
element.domainName
}}</mat-cell>
<mat-cell *matCellDef="let element">
<mat-icon
*ngIf="getOperationMessage(element.domainName)"
[matTooltip]="getOperationMessage(element.domainName)"
matTooltipPosition="above"
class="primary-text"
>info</mat-icon
>
<span>{{ element.domainName }}</span>
</mat-cell>
</ng-container>
<ng-container matColumnDef="creationTime">

View File

@@ -12,6 +12,22 @@
}
}
&__domains-selection {
height: 60px;
max-height: 0;
transition: max-height 0.2s linear;
display: flex;
align-items: center;
overflow: hidden;
gap: 20px;
&-text {
font-weight: bold;
}
&.active {
max-height: 60px;
}
}
&-domains__download {
position: absolute;
top: -55px;
@@ -41,6 +57,22 @@
overflow: hidden;
word-break: break-word;
}
.mat-column-select {
max-width: 60px;
padding-left: 15px;
}
.mat-column-domainName {
position: relative;
padding-left: 25px;
mat-icon {
position: absolute;
left: 0;
}
}
mat-cell:has([style*="display: none"]),
mat-header-cell:has([style*="display: none"]) {
display: none;
}
}
&__domains-spinner {

View File

@@ -21,6 +21,7 @@ import { MaterialModule } from '../material.module';
import { BackendService } from '../shared/services/backend.service';
import { DomainListComponent } from './domainList.component';
import { FormsModule } from '@angular/forms';
import { AppModule } from '../app.module';
describe('DomainListComponent', () => {
let component: DomainListComponent;
@@ -29,7 +30,12 @@ describe('DomainListComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DomainListComponent],
imports: [MaterialModule, BrowserAnimationsModule, FormsModule],
imports: [
MaterialModule,
BrowserAnimationsModule,
FormsModule,
AppModule,
],
providers: [
BackendService,
provideHttpClient(),

View File

@@ -12,16 +12,91 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { SelectionModel } from '@angular/cdk/collections';
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
import { Component, ViewChild, effect } from '@angular/core';
import { Component, ViewChild, effect, Inject } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTableDataSource } from '@angular/material/table';
import { Subject, debounceTime } from 'rxjs';
import { Subject, debounceTime, take, filter } from 'rxjs';
import { RegistrarService } from '../registrar/registrar.service';
import { Domain, DomainListService } from './domainList.service';
import { RegistryLockComponent } from './registryLock.component';
import { RegistryLockService } from './registryLock.service';
import {
MAT_DIALOG_DATA,
MatDialog,
MatDialogRef,
} from '@angular/material/dialog';
import { RESTRICTED_ELEMENTS } from '../shared/directives/userLevelVisiblity.directive';
interface DomainResponse {
message: string;
responseCode: string;
}
interface DomainData {
[domain: string]: DomainResponse;
}
@Component({
selector: 'app-response-dialog',
template: `
<h2 mat-dialog-title>{{ data.title }}</h2>
<mat-dialog-content [innerHTML]="data.content" />
<mat-dialog-actions>
<button mat-button (click)="onClose()">Close</button>
</mat-dialog-actions>
`,
})
export class ResponseDialogComponent {
constructor(
public dialogRef: MatDialogRef<ReasonDialogComponent>,
@Inject(MAT_DIALOG_DATA)
public data: { title: string; content: string }
) {}
onClose(): void {
this.dialogRef.close();
}
}
@Component({
selector: 'app-reason-dialog',
template: `
<h2 mat-dialog-title>
Please provide a reason for {{ data.operation }} the domain(s):
</h2>
<mat-dialog-content>
<mat-form-field appearance="outline" style="width:100%">
<textarea matInput [(ngModel)]="reason" rows="4"></textarea>
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button (click)="onCancel()">Cancel</button>
<button mat-button color="warn" (click)="onDelete()" [disabled]="!reason">
Delete
</button>
</mat-dialog-actions>
`,
})
export class ReasonDialogComponent {
reason: string = '';
constructor(
public dialogRef: MatDialogRef<ReasonDialogComponent>,
@Inject(MAT_DIALOG_DATA)
public data: { operation: 'deleting' | 'suspending' }
) {}
onDelete(): void {
this.dialogRef.close(this.reason);
}
onCancel(): void {
this.dialogRef.close();
}
}
@Component({
selector: 'app-domain-list',
@@ -31,8 +106,10 @@ import { RegistryLockService } from './registryLock.service';
export class DomainListComponent {
public static PATH = 'domain-list';
private readonly DEBOUNCE_MS = 500;
isAllSelected = false;
displayedColumns: string[] = [
'select',
'domainName',
'creationTime',
'registrationExpirationTime',
@@ -42,6 +119,7 @@ export class DomainListComponent {
];
dataSource: MatTableDataSource<Domain> = new MatTableDataSource();
selection = new SelectionModel<Domain>(true, [], undefined, this.isChecked());
isLoading = true;
searchTermSubject = new Subject<string>();
@@ -51,13 +129,18 @@ export class DomainListComponent {
resultsPerPage = 50;
totalResults?: number = 0;
reason: string = '';
operationResult: DomainData | undefined;
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
constructor(
protected domainListService: DomainListService,
protected registrarService: RegistrarService,
protected registryLockService: RegistryLockService,
private _snackBar: MatSnackBar
private _snackBar: MatSnackBar,
private dialog: MatDialog
) {
effect(() => {
this.pageNumber = 0;
@@ -134,6 +217,98 @@ export class DomainListComponent {
onPageChange(event: PageEvent) {
this.pageNumber = event.pageIndex;
this.resultsPerPage = event.pageSize;
this.selection.clear();
this.reloadData();
}
toggleAllRows() {
if (this.isAllSelected) {
this.selection.clear();
this.isAllSelected = false;
return;
}
this.selection.select(...this.dataSource.data);
this.isAllSelected = true;
}
checkboxLabel(row?: Domain): string {
if (!row) {
return `${this.isAllSelected ? 'deselect' : 'select'} all`;
}
return `${this.selection.isSelected(row) ? 'deselect' : 'select'} row ${
row.domainName
}`;
}
private isChecked(): ((o1: Domain, o2: Domain) => boolean) | undefined {
return (o1: Domain, o2: Domain) => {
if (!o1.domainName || !o2.domainName) {
return false;
}
return this.isAllSelected || o1.domainName === o2.domainName;
};
}
getElementIdForBulkDelete() {
return RESTRICTED_ELEMENTS.BULK_DELETE;
}
getOperationMessage(domain: string) {
if (this.operationResult && this.operationResult[domain])
return this.operationResult[domain].message;
return '';
}
sendDeleteRequest(reason: string) {
this.isLoading = true;
this.domainListService
.deleteDomains(
this.selection.selected,
reason,
this.registrarService.registrarId()
)
.pipe(take(1))
.subscribe({
next: (result: DomainData) => {
this.isLoading = false;
const successCount = Object.keys(result).filter((domainName) =>
result[domainName].responseCode.toString().startsWith('1')
).length;
const failureCount = Object.keys(result).length - successCount;
this.dialog.open(ResponseDialogComponent, {
data: {
title: 'Domain Deletion Results',
content: `Successfully deleted - ${successCount} domain(s)<br/>Failed to delete - ${failureCount} domain(s)<br/>${
failureCount
? 'Some domains could not be deleted due to ongoing processes or server errors. '
: ''
}Please check the table for more information.`,
},
});
this.selection.clear();
this.operationResult = result;
this.reloadData();
},
error: (err: HttpErrorResponse) =>
this._snackBar.open(err.error || err.message),
});
}
deleteSelectedDomains() {
const dialogRef = this.dialog.open(ReasonDialogComponent, {
data: {
operation: 'deleting',
},
});
dialogRef
.afterClosed()
.pipe(
take(1),
filter((reason) => !!reason)
)
.subscribe(this.sendDeleteRequest.bind(this));
}
}

View File

@@ -48,7 +48,6 @@ export class DomainListService {
private backendService: BackendService,
private registrarService: RegistrarService
) {}
retrieveDomains(
pageNumber?: number,
resultsPerPage?: number,
@@ -71,4 +70,13 @@ export class DomainListService {
})
);
}
deleteDomains(domains: Domain[], reason: string, registrarId: string) {
return this.backendService.bulkDomainAction(
domains.map((d) => d.domainName),
reason,
'DELETE',
registrarId
);
}
}

View File

@@ -12,6 +12,7 @@
<a
[routerLink]="'/home'"
routerLinkActive="active"
aria-label="Google Registry logo"
class="console-app__logo"
>
<svg

View File

@@ -17,6 +17,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HeaderComponent } from './header.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MaterialModule } from '../material.module';
import { ActivatedRoute } from '@angular/router';
import { AppModule, SelectedRegistrarModule } from '../app.module';
import { AppRoutingModule } from '../app-routing.module';
import { BackendService } from '../shared/services/backend.service';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
describe('HeaderComponent', () => {
let component: HeaderComponent;
@@ -24,7 +30,19 @@ describe('HeaderComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MaterialModule, BrowserAnimationsModule],
imports: [
SelectedRegistrarModule,
MaterialModule,
BrowserAnimationsModule,
AppRoutingModule,
AppModule,
],
providers: [
BackendService,
{ provide: ActivatedRoute, useValue: {} as ActivatedRoute },
provideHttpClient(),
provideHttpClientTesting(),
],
declarations: [HeaderComponent],
}).compileComponents();

View File

@@ -16,6 +16,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeComponent } from './home.component';
import { MaterialModule } from '../material.module';
import { AppModule } from '../app.module';
describe('HomeComponent', () => {
let component: HomeComponent;
@@ -23,7 +24,7 @@ describe('HomeComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MaterialModule],
imports: [MaterialModule, AppModule],
declarations: [HomeComponent],
}).compileComponents();

View File

@@ -6,7 +6,9 @@
<mat-tree-node
*matTreeNodeDef="let node"
matTreeNodeToggle
tabindex="0"
(click)="onClick(node)"
(keyup.enter)="onClick(node)"
[class.active]="router.url.includes(node.path)"
[elementId]="getElementId(node)"
>

View File

@@ -1,4 +1,8 @@
<div class="console-app__registrar-view">
<div
class="console-app__registrar-view"
cdkTrapFocus
[cdkTrapFocusAutoCapture]="true"
>
<h1 class="mat-headline-4">Registrars</h1>
<mat-divider></mat-divider>
<div class="console-app__registrar-view-content">

View File

@@ -11,6 +11,7 @@
[ngModelOptions]="{ standalone: true }"
(focus)="onFocus()"
[matAutocomplete]="auto"
spellcheck="false"
/>
<mat-autocomplete
autoActiveFirstOption

View File

@@ -59,6 +59,8 @@
<mat-row
*matRowDef="let row; columns: displayedColumns"
(click)="openDetails(row.registrarId)"
tabindex="0"
(keyup.enter)="openDetails(row.registrarId)"
></mat-row>
</mat-table>

View File

@@ -32,7 +32,9 @@
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row
*matRowDef="let row; columns: displayedColumns"
tabindex="0"
(click)="openDetails(row)"
(keyup.enter)="openDetails(row)"
></mat-row>
</mat-table>
}

View File

@@ -1,4 +1,9 @@
<div class="console-app__contact" *ngIf="contactService.contactInEdit">
<div
class="console-app__contact"
*ngIf="contactService.contactInEdit"
cdkTrapFocus
[cdkTrapFocusAutoCapture]="true"
>
<div class="console-app__contact-controls">
<button
mat-icon-button

View File

@@ -1,4 +1,8 @@
<div class="settings-security__edit-password">
<div
class="settings-security__edit-password"
cdkTrapFocus
[cdkTrapFocusAutoCapture]="true"
>
<p>
<button
mat-icon-button

View File

@@ -42,7 +42,7 @@ describe('SecurityComponent', () => {
fetchSecurityDetailsSpy =
securityServiceSpy.fetchSecurityDetails.and.returnValue(of());
saveSpy = securityServiceSpy.saveChanges;
saveSpy = securityServiceSpy.saveChanges.and.returnValue(of());
await TestBed.configureTestingModule({
declarations: [SecurityEditComponent, SecurityComponent],

View File

@@ -1,4 +1,8 @@
<div class="settings-security__edit">
<div
class="settings-security__edit"
cdkTrapFocus
[cdkTrapFocusAutoCapture]="true"
>
<h1>IP Allowlist</h1>
<p>
Restrict access to EPP production servers to the following IP/IPv6

View File

@@ -10,22 +10,28 @@
<a
mat-tab-link
routerLink="contact"
routerLinkActive="active-link"
routerLinkActive
queryParamsHandling="merge"
#rla="routerLinkActive"
[active]="rla.isActive"
>Contacts</a
>
<a
mat-tab-link
routerLink="whois"
routerLinkActive="active-link"
routerLinkActive
queryParamsHandling="merge"
#rla2="routerLinkActive"
[active]="rla2.isActive"
>WHOIS Info</a
>
<a
mat-tab-link
routerLink="security"
routerLinkActive="active-link"
routerLinkActive
queryParamsHandling="merge"
#rla3="routerLinkActive"
[active]="rla3.isActive"
>Security</a
>
</nav>

View File

@@ -13,14 +13,6 @@
// limitations under the License.
.console-settings {
.mdc-tab {
&.active-link {
border-bottom: 2px solid var(--primary);
.mdc-tab__text-label {
color: var(--primary);
}
}
}
nav {
margin-bottom: 40px;
}

View File

@@ -17,6 +17,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SettingsComponent } from './settings.component';
import { MaterialModule } from '../material.module';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ActivatedRoute } from '@angular/router';
import { AppModule, SelectedRegistrarModule } from '../app.module';
import { BackendService } from '../shared/services/backend.service';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { AppRoutingModule } from '../app-routing.module';
describe('SettingsComponent', () => {
let component: SettingsComponent;
@@ -24,7 +30,19 @@ describe('SettingsComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MaterialModule, BrowserAnimationsModule],
imports: [
SelectedRegistrarModule,
MaterialModule,
BrowserAnimationsModule,
AppRoutingModule,
AppModule,
],
providers: [
BackendService,
{ provide: ActivatedRoute, useValue: {} as ActivatedRoute },
provideHttpClient(),
provideHttpClientTesting(),
],
declarations: [SettingsComponent],
}).compileComponents();

View File

@@ -1,4 +1,9 @@
<div class="console-app__whois-edit" *ngIf="registrarInEdit">
<div
class="console-app__whois-edit"
*ngIf="registrarInEdit"
cdkTrapFocus
[cdkTrapFocusAutoCapture]="true"
>
<button
mat-icon-button
class="console-app__whois-edit-back"

View File

@@ -19,6 +19,7 @@ export enum RESTRICTED_ELEMENTS {
REGISTRAR_ELEMENT,
OTE,
USERS,
BULK_DELETE,
}
export const DISABLED_ELEMENTS_PER_ROLE = {
@@ -26,6 +27,7 @@ export const DISABLED_ELEMENTS_PER_ROLE = {
RESTRICTED_ELEMENTS.REGISTRAR_ELEMENT,
RESTRICTED_ELEMENTS.OTE,
RESTRICTED_ELEMENTS.USERS,
RESTRICTED_ELEMENTS.BULK_DELETE,
],
SUPPORT_LEAD: [RESTRICTED_ELEMENTS.USERS],
SUPPORT_AGENT: [RESTRICTED_ELEMENTS.USERS],

View File

@@ -180,6 +180,23 @@ export class BackendService {
.pipe(catchError((err) => this.errorCatcher<any>(err)));
}
bulkDomainAction(
domainNames: string[],
reason: string,
bulkDomainAction: string,
registrarId: string
) {
return this.http
.post<any>(
`/console-api/bulk-domain?registrarId=${registrarId}&bulkDomainAction=${bulkDomainAction}`,
{
domainList: domainNames,
reason,
}
)
.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)

View File

@@ -1,4 +1,8 @@
<div class="console-app__user-details">
<div
class="console-app__user-details"
cdkTrapFocus
[cdkTrapFocusAutoCapture]="true"
>
@if(isEditing) {
<h1 class="mat-headline-4">Editing {{ userDetails().emailAddress }}</h1>
<mat-divider></mat-divider>

View File

@@ -1,4 +1,4 @@
<app-selected-registrar-wrapper>
<app-selected-registrar-wrapper cdkTrapFocus [cdkTrapFocusAutoCapture]="true">
@if(isLoading) {
<div class="console-app__users-spinner">
<mat-spinner />

View File

@@ -19,6 +19,8 @@
*matRowDef="let row; columns: displayedColumns"
[class.rowSelected]="isRowSelected(row)"
(click)="onClick(row)"
(keyup.enter)="onClick(row)"
tabindex="0"
></mat-row>
</mat-table>
</div>

View File

@@ -82,11 +82,6 @@ $typographyConfig: mat.m2-define-typography-config(
),
);
// Access and define a class with secondary color exposed
.secondary-text {
color: $secondary-color;
}
.text-xl {
font-size: 18px;
}
@@ -115,6 +110,15 @@ mat-row:hover {
--mat-sidenav-container-width: 280px;
}
// Access and define a class with secondary color exposed
.secondary-text {
color: #575757;
}
.primary-text {
color: var(--primary);
}
$theme: mat.define-theme(
(
color: (

View File

@@ -54,7 +54,8 @@ public class ExportPremiumTermsAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
static final MediaType EXPORT_MIME_TYPE = MediaType.PLAIN_TEXT_UTF_8;
static final String PREMIUM_TERMS_FILENAME = "CONFIDENTIAL_premium_terms.txt";
static final String TLD_IDENTIFIER_FORMAT = "# TLD: %s";
static final String PREMIUM_TERMS_FILENAME_FORMAT = "CONFIDENTIAL_premium_terms_%s.txt";
@Inject DriveConnection driveConnection;
@@ -127,7 +128,7 @@ public class ExportPremiumTermsAction implements Runnable {
try {
String fileId =
driveConnection.createOrUpdateFile(
PREMIUM_TERMS_FILENAME,
String.format(PREMIUM_TERMS_FILENAME_FORMAT, tldStr),
EXPORT_MIME_TYPE,
tld.getDriveFolderId(),
getFormattedPremiumTerms(tld).getBytes(UTF_8));
@@ -150,11 +151,9 @@ public class ExportPremiumTermsAction implements Runnable {
.map(PremiumEntry::toString)
.collect(ImmutableSortedSet.toImmutableSortedSet(String::compareTo));
return Joiner.on("\n")
.appendTo(
new StringBuilder(),
Iterables.concat(ImmutableList.of(exportDisclaimer.trim()), premiumTerms))
.append("\n")
.toString();
String tldIdentifier = String.format(TLD_IDENTIFIER_FORMAT, tldStr);
Iterable<String> commentsAndTerms =
Iterables.concat(ImmutableList.of(exportDisclaimer.trim(), tldIdentifier), premiumTerms);
return Joiner.on("\n").join(commentsAndTerms) + "\n";
}
}

View File

@@ -121,6 +121,7 @@ import google.registry.ui.server.console.ConsoleUpdateRegistrarAction;
import google.registry.ui.server.console.ConsoleUserDataAction;
import google.registry.ui.server.console.ConsoleUsersAction;
import google.registry.ui.server.console.RegistrarsAction;
import google.registry.ui.server.console.domains.ConsoleBulkDomainAction;
import google.registry.ui.server.console.settings.ContactAction;
import google.registry.ui.server.console.settings.SecurityAction;
import google.registry.ui.server.console.settings.WhoisRegistrarFieldsAction;
@@ -174,6 +175,8 @@ interface RequestComponent {
CheckApiAction checkApiAction();
ConsoleBulkDomainAction consoleBulkDomainAction();
ConsoleDomainGetAction consoleDomainGetAction();
ConsoleDomainListAction consoleDomainListAction();

View File

@@ -37,6 +37,7 @@ import google.registry.ui.server.console.ConsoleUpdateRegistrarAction;
import google.registry.ui.server.console.ConsoleUserDataAction;
import google.registry.ui.server.console.ConsoleUsersAction;
import google.registry.ui.server.console.RegistrarsAction;
import google.registry.ui.server.console.domains.ConsoleBulkDomainAction;
import google.registry.ui.server.console.settings.ContactAction;
import google.registry.ui.server.console.settings.SecurityAction;
import google.registry.ui.server.console.settings.WhoisRegistrarFieldsAction;
@@ -53,6 +54,8 @@ import google.registry.ui.server.console.settings.WhoisRegistrarFieldsAction;
WhiteboxModule.class,
})
public interface FrontendRequestComponent {
ConsoleBulkDomainAction consoleBulkDomainAction();
ConsoleDomainGetAction consoleDomainGetAction();
ConsoleDomainListAction consoleDomainListAction();

View File

@@ -136,7 +136,7 @@ abstract class EppToolCommand extends ConfirmingCommand implements CommandWithCo
params.put("dryRun", dryRun);
params.put("clientId", command.clientId);
params.put("superuser", superuser);
params.put("xml", URLEncoder.encode(command.xml, UTF_8.toString()));
params.put("xml", URLEncoder.encode(command.xml, UTF_8));
String requestBody =
Joiner.on('&').withKeyValueSeparator("=").join(filterValues(params, Objects::nonNull));
responses.add(

View File

@@ -1,228 +0,0 @@
// 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

@@ -0,0 +1,125 @@
// 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.domains;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
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.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.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 google.registry.ui.server.console.ConsoleApiAction;
import google.registry.ui.server.console.ConsoleApiParams;
import java.util.List;
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 ConsoleDomainActionType.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,
method = Action.Method.POST,
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public class ConsoleBulkDomainAction extends ConsoleApiAction {
public static final String PATH = "/console-api/bulk-domain";
/** All requests must include at least a list of domain names on which to perform the action. */
public record BulkDomainList(@Expose List<String> domainList) {}
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) {
// Temporary flag while testing
if (!user.getUserRoles().isAdmin()) {
consoleApiParams.response().setStatus(SC_FORBIDDEN);
return;
}
JsonElement jsonPayload =
optionalJsonPayload.orElseThrow(
() -> new IllegalArgumentException("Bulk action payload must be present"));
BulkDomainList domainList = consoleApiParams.gson().fromJson(jsonPayload, BulkDomainList.class);
ConsoleDomainActionType actionType =
ConsoleDomainActionType.parseActionType(bulkDomainAction, jsonPayload);
checkPermission(user, registrarId, actionType.getNecessaryPermission());
ImmutableMap<String, ConsoleEppOutput> result =
domainList.domainList.stream()
.collect(
toImmutableMap(d -> d, d -> executeEpp(actionType.getXmlContentsToRun(d), user)));
// Front end should parse situations where only some commands worked
consoleApiParams.response().setPayload(consoleApiParams.gson().toJson(result));
consoleApiParams.response().setStatus(SC_OK);
}
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)));
}
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

@@ -0,0 +1,61 @@
// Copyright 2025 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.domains;
import com.google.common.collect.ImmutableMap;
import com.google.gson.JsonElement;
import google.registry.model.console.ConsolePermission;
/** An action that will run a delete EPP command on the given domain. */
public class ConsoleBulkDomainDeleteActionType implements ConsoleDomainActionType {
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 final String reason;
public ConsoleBulkDomainDeleteActionType(JsonElement jsonElement) {
this.reason = jsonElement.getAsJsonObject().get("reason").getAsString();
}
@Override
public String getXmlContentsToRun(String domainName) {
return ConsoleDomainActionType.fillSubstitutions(
DOMAIN_DELETE_XML, ImmutableMap.of("DOMAIN_NAME", domainName, "REASON", reason));
}
@Override
public ConsolePermission getNecessaryPermission() {
return ConsolePermission.EXECUTE_EPP_COMMANDS;
}
}

View File

@@ -0,0 +1,71 @@
// Copyright 2025 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.domains;
import com.google.common.collect.ImmutableMap;
import com.google.gson.JsonElement;
import google.registry.model.console.ConsolePermission;
/** An action that will suspend the given domain, assigning all 5 server*Prohibited statuses. */
public class ConsoleBulkDomainSuspendActionType implements ConsoleDomainActionType {
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>RegistryConsole</clTRID>
</command>
</epp>""";
private final String reason;
public ConsoleBulkDomainSuspendActionType(JsonElement jsonElement) {
this.reason = jsonElement.getAsJsonObject().get("reason").getAsString();
}
@Override
public String getXmlContentsToRun(String domainName) {
return ConsoleDomainActionType.fillSubstitutions(
DOMAIN_SUSPEND_XML, ImmutableMap.of("DOMAIN_NAME", domainName, "REASON", reason));
}
@Override
public ConsolePermission getNecessaryPermission() {
return ConsolePermission.SUSPEND_DOMAIN;
}
}

View File

@@ -0,0 +1,69 @@
// Copyright 2025 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.domains;
import com.google.common.collect.ImmutableMap;
import com.google.common.escape.Escaper;
import com.google.common.xml.XmlEscapers;
import com.google.gson.JsonElement;
import google.registry.model.console.ConsolePermission;
import java.util.Map;
/**
* A type of EPP action to perform on domain(s), run by the {@link ConsoleBulkDomainAction}.
*
* <p>Each {@link BulkAction} defines the class that implements that action, including the EPP XML
* that will be run and the permission required.
*/
public interface ConsoleDomainActionType {
enum BulkAction {
DELETE(ConsoleBulkDomainDeleteActionType.class),
SUSPEND(ConsoleBulkDomainSuspendActionType.class);
private final Class<? extends ConsoleDomainActionType> actionClass;
BulkAction(Class<? extends ConsoleDomainActionType> actionClass) {
this.actionClass = actionClass;
}
public Class<? extends ConsoleDomainActionType> getActionClass() {
return actionClass;
}
}
Escaper XML_ESCAPER = XmlEscapers.xmlContentEscaper();
static String fillSubstitutions(String xmlTemplate, ImmutableMap<String, String> replacements) {
String xml = xmlTemplate;
for (Map.Entry<String, String> entry : replacements.entrySet()) {
xml = xml.replaceAll("%" + entry.getKey() + "%", XML_ESCAPER.escape(entry.getValue()));
}
return xml;
}
String getXmlContentsToRun(String domainName);
ConsolePermission getNecessaryPermission();
static ConsoleDomainActionType parseActionType(String bulkDomainAction, JsonElement jsonElement) {
BulkAction bulkAction = BulkAction.valueOf(bulkDomainAction);
try {
return bulkAction.getActionClass().getConstructor(JsonElement.class).newInstance(jsonElement);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e); // shouldn't happen
}
}
}

View File

@@ -17,7 +17,6 @@ package google.registry.export;
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.export.ExportPremiumTermsAction.EXPORT_MIME_TYPE;
import static google.registry.export.ExportPremiumTermsAction.PREMIUM_TERMS_FILENAME;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.deleteTld;
import static google.registry.testing.DatabaseHelper.persistResource;
@@ -52,11 +51,10 @@ import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link ExportPremiumTermsAction}. */
public class ExportPremiumTermsActionTest {
private static final String DISCLAIMER_WITH_NEWLINE = "# Premium Terms Export Disclaimer\n";
private static final ImmutableList<String> PREMIUM_NAMES =
ImmutableList.of("2048,USD 549", "0,USD 549");
private static final String EXPECTED_FILE_CONTENT =
DISCLAIMER_WITH_NEWLINE + "0, 549.00\n" + "2048, 549.00\n";
"# Premium Terms Export Disclaimer\n# TLD: tld\n0, 549.00\n" + "2048, 549.00\n";
@RegisterExtension
final JpaIntegrationTestExtension jpa =
@@ -69,7 +67,7 @@ public class ExportPremiumTermsActionTest {
ExportPremiumTermsAction action = new ExportPremiumTermsAction();
action.response = response;
action.driveConnection = driveConnection;
action.exportDisclaimer = DISCLAIMER_WITH_NEWLINE;
action.exportDisclaimer = "# Premium Terms Export Disclaimer\n";
action.tldStr = tld;
action.run();
}
@@ -94,7 +92,7 @@ public class ExportPremiumTermsActionTest {
verify(driveConnection)
.createOrUpdateFile(
PREMIUM_TERMS_FILENAME,
"CONFIDENTIAL_premium_terms_tld.txt",
EXPORT_MIME_TYPE,
"folder_id",
EXPECTED_FILE_CONTENT.getBytes(UTF_8));
@@ -157,7 +155,7 @@ public class ExportPremiumTermsActionTest {
verify(driveConnection)
.createOrUpdateFile(
PREMIUM_TERMS_FILENAME,
"CONFIDENTIAL_premium_terms_tld.txt",
EXPORT_MIME_TYPE,
"bad_folder_id",
EXPECTED_FILE_CONTENT.getBytes(UTF_8));

View File

@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.ui.server.console;
package google.registry.ui.server.console.domains;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL;
@@ -33,7 +33,6 @@ 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;
@@ -50,7 +49,7 @@ 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 google.registry.ui.server.console.ConsoleApiParams;
import java.util.Optional;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
@@ -168,13 +167,13 @@ public class ConsoleBulkDomainActionTest {
@Test
void testFailure_badActionString() {
ConsoleBulkDomainAction action = createAction("bad", null);
ConsoleBulkDomainAction action = createAction("bad", GSON.toJsonTree(ImmutableMap.of()));
action.run();
assertThat(fakeResponse.getStatus()).isEqualTo(SC_BAD_REQUEST);
assertThat(fakeResponse.getPayload())
.isEqualTo(
"No enum constant"
+ " google.registry.ui.server.console.ConsoleBulkDomainAction.BulkAction.bad");
+ " google.registry.ui.server.console.domains.ConsoleDomainActionType.BulkAction.bad");
}
@Test
@@ -188,7 +187,8 @@ public class ConsoleBulkDomainActionTest {
@Test
void testFailure_noPermission() {
JsonElement payload =
GSON.toJsonTree(ImmutableMap.of("domainList", ImmutableList.of("domain.tld")));
GSON.toJsonTree(
ImmutableMap.of("domainList", ImmutableList.of("domain.tld"), "reason", "reason"));
ConsoleBulkDomainAction action =
createAction(
"DELETE",
@@ -205,29 +205,33 @@ public class ConsoleBulkDomainActionTest {
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);
}
// @ptkach - reenable with suspend change
// @Test
// void testFailure_suspend_nonAdmin() {
// ConsoleBulkDomainAction action =
// createAction(
// "SUSPEND",
// GSON.toJsonTree(
// ImmutableMap.of("domainList", ImmutableList.of("example.tld"), "reason",
// "test")),
// user);
// 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())
.setUserRoles(
new UserRoles.Builder().setIsAdmin(true).setGlobalRole(GlobalRole.FTE).build())
.build());
return createAction(action, payload, user);
}

View File

@@ -77,8 +77,10 @@ public class ConsoleScreenshotTest extends WebDriverTestCase {
assertThat(driver.getCurrentUrl()).endsWith("?registrarId=TheRegistrar");
}
@RetryingTest(3)
void dums_mainPage() throws Exception {
server.setGlobalRole(GlobalRole.FTE);
clickSidebarElementByName("Domains");
driver.diffPage("noRegistrarSelected");
selectRegistrar();
@@ -90,20 +92,19 @@ public class ConsoleScreenshotTest extends WebDriverTestCase {
driver.diffPage("actionsButtonClicked");
}
// TODO: Reenable failing test after kokoro issue is resolved
// @RetryingTest(3)
// void settingsPage() throws Exception {
// clickSidebarElementByName("Settings");
// driver.diffPage("noRegistrarSelected");
// selectRegistrar();
// driver.diffPage("registrarSelected_contacts");
// driver.findElement(By.cssSelector("a[routerLink=\"whois\"]")).click();
// Thread.sleep(500);
// driver.diffPage("registrarSelected_whois");
// driver.findElement(By.cssSelector("a[routerLink=\"security\"]")).click();
// Thread.sleep(500);
// driver.diffPage("registrarSelected_security");
// }
@RetryingTest(3)
void settingsPage() throws Exception {
clickSidebarElementByName("Settings");
driver.diffPage("noRegistrarSelected");
selectRegistrar();
driver.diffPage("registrarSelected_contacts");
driver.findElement(By.cssSelector("a[routerLink=\"whois\"]")).click();
Thread.sleep(500);
driver.diffPage("registrarSelected_whois");
driver.findElement(By.cssSelector("a[routerLink=\"security\"]")).click();
Thread.sleep(500);
driver.diffPage("registrarSelected_security");
}
@RetryingTest(3)
void billingInfo() throws Exception {

View File

@@ -1,5 +1,6 @@
SERVICE PATH CLASS METHODS OK MIN USER_POLICY
FRONTEND /_dr/epp EppTlsAction POST n APP ADMIN
CONSOLE /console-api/bulk-domain ConsoleBulkDomainAction POST n USER PUBLIC
CONSOLE /console-api/domain ConsoleDomainGetAction GET n USER PUBLIC
CONSOLE /console-api/domain-list ConsoleDomainListAction GET n USER PUBLIC
CONSOLE /console-api/dum-download ConsoleDumDownloadAction GET n USER PUBLIC
@@ -13,4 +14,4 @@ CONSOLE /console-api/settings/contacts ContactAction GET,
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
CONSOLE /console-api/users ConsoleUsersAction GET,POST,DELETE,PUT n USER PUBLIC

View File

@@ -66,6 +66,7 @@ PUBAPI /rdap/ip/(*) RdapIpAction
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/bulk-domain ConsoleBulkDomainAction POST n USER PUBLIC
CONSOLE /console-api/domain ConsoleDomainGetAction GET n USER PUBLIC
CONSOLE /console-api/domain-list ConsoleDomainListAction GET n USER PUBLIC
CONSOLE /console-api/dum-download ConsoleDumDownloadAction GET n USER PUBLIC
@@ -79,4 +80,4 @@ CONSOLE /console-api/settings/contacts ContactAction
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
CONSOLE /console-api/users ConsoleUsersAction GET,POST,DELETE,PUT n USER PUBLIC

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 72 KiB

View File

@@ -37,13 +37,14 @@ public class UrlConnectionException extends RuntimeException {
@Override
public String getMessage() {
byte[] resultContent;
int responseCode;
int responseCode = 0;
try {
resultContent = ByteStreams.toByteArray(connection.getInputStream());
responseCode = connection.getResponseCode();
} catch (IOException e) {
resultContent = new byte[] {};
responseCode = 0;
resultContent =
ByteStreams.toByteArray(
responseCode < 400 ? connection.getInputStream() : connection.getErrorStream());
} catch (IOException | NullPointerException e) {
resultContent = "-- Response is missing or has malformed content --".getBytes(UTF_8);
}
StringBuilder result =
new StringBuilder(2048 + resultContent.length)