mirror of
https://github.com/google/nomulus
synced 2026-02-13 00:02:04 +00:00
Compare commits
11 Commits
tlds-20231
...
tlds-20231
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08285f5de7 | ||
|
|
fb4c5b457d | ||
|
|
781c212275 | ||
|
|
c73f7a6bd3 | ||
|
|
8d793b2349 | ||
|
|
55d5f8c6f8 | ||
|
|
9006312253 | ||
|
|
e5e2370923 | ||
|
|
b3b0efd47e | ||
|
|
e82cbe60a9 | ||
|
|
923bc13e3a |
@@ -49,15 +49,14 @@ import { SettingsWidgetComponent } from './home/widgets/settingsWidget.component
|
||||
import { UserDataService } from './shared/services/userData.service';
|
||||
import WhoisComponent from './settings/whois/whois.component';
|
||||
import { SnackBarModule } from './snackbar.module';
|
||||
import {
|
||||
RegistrarDetailsComponent,
|
||||
RegistrarDetailsWrapperComponent,
|
||||
} from './registrar/registrarDetails.component';
|
||||
import { RegistrarDetailsComponent } from './registrar/registrarDetails.component';
|
||||
import { DomainListComponent } from './domains/domainList.component';
|
||||
import { DialogBottomSheetWrapper } from './shared/components/dialogBottomSheet.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
DialogBottomSheetWrapper,
|
||||
BillingWidgetComponent,
|
||||
ContactDetailsDialogComponent,
|
||||
ContactWidgetComponent,
|
||||
@@ -70,7 +69,6 @@ import { DomainListComponent } from './domains/domainList.component';
|
||||
PromotionsWidgetComponent,
|
||||
RegistrarComponent,
|
||||
RegistrarDetailsComponent,
|
||||
RegistrarDetailsWrapperComponent,
|
||||
RegistrarSelectorComponent,
|
||||
ResourcesWidgetComponent,
|
||||
SecurityComponent,
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
<div class="console-domains">
|
||||
<mat-form-field>
|
||||
<mat-label>Filter</mat-label>
|
||||
<input matInput (keyup)="applyFilter($event)" #input />
|
||||
<input
|
||||
type="search"
|
||||
matInput
|
||||
[(ngModel)]="searchTerm"
|
||||
(ngModelChange)="sendInput()"
|
||||
#input
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<div *ngIf="isLoading; else domains_content" class="console-domains__loading">
|
||||
|
||||
@@ -18,6 +18,7 @@ import { BackendService } from '../shared/services/backend.service';
|
||||
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import { Domain, DomainListService } from './domainList.service';
|
||||
import { Subject, debounceTime } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-domain-list',
|
||||
@@ -27,6 +28,7 @@ import { Domain, DomainListService } from './domainList.service';
|
||||
})
|
||||
export class DomainListComponent {
|
||||
public static PATH = 'domain-list';
|
||||
private readonly DEBOUNCE_MS = 500;
|
||||
|
||||
displayedColumns: string[] = [
|
||||
'domainName',
|
||||
@@ -38,6 +40,9 @@ export class DomainListComponent {
|
||||
dataSource: MatTableDataSource<Domain> = new MatTableDataSource();
|
||||
isLoading = true;
|
||||
|
||||
searchTermSubject = new Subject<string>();
|
||||
searchTerm?: string;
|
||||
|
||||
pageNumber?: number;
|
||||
resultsPerPage = 50;
|
||||
totalResults?: number;
|
||||
@@ -52,13 +57,28 @@ export class DomainListComponent {
|
||||
|
||||
ngOnInit() {
|
||||
this.dataSource.paginator = this.paginator;
|
||||
// Don't spam the server unnecessarily while the user is typing
|
||||
this.searchTermSubject
|
||||
.pipe(debounceTime(this.DEBOUNCE_MS))
|
||||
.subscribe((searchTermValue) => {
|
||||
this.reloadData();
|
||||
});
|
||||
this.reloadData();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.searchTermSubject.complete();
|
||||
}
|
||||
|
||||
reloadData() {
|
||||
this.isLoading = true;
|
||||
this.domainListService
|
||||
.retrieveDomains(this.pageNumber, this.resultsPerPage, this.totalResults)
|
||||
.retrieveDomains(
|
||||
this.pageNumber,
|
||||
this.resultsPerPage,
|
||||
this.totalResults,
|
||||
this.searchTerm
|
||||
)
|
||||
.subscribe((domainListResult) => {
|
||||
this.dataSource.data = domainListResult.domains;
|
||||
this.totalResults = domainListResult.totalResults;
|
||||
@@ -66,10 +86,8 @@ export class DomainListComponent {
|
||||
});
|
||||
}
|
||||
|
||||
/** TODO: the backend will need to accept a filter string. */
|
||||
applyFilter(event: KeyboardEvent) {
|
||||
// const filterValue = (event.target as HTMLInputElement).value;
|
||||
this.reloadData();
|
||||
sendInput() {
|
||||
this.searchTermSubject.next(this.searchTerm!);
|
||||
}
|
||||
|
||||
onPageChange(event: PageEvent) {
|
||||
|
||||
@@ -47,7 +47,8 @@ export class DomainListService {
|
||||
retrieveDomains(
|
||||
pageNumber?: number,
|
||||
resultsPerPage?: number,
|
||||
totalResults?: number
|
||||
totalResults?: number,
|
||||
searchTerm?: string
|
||||
) {
|
||||
return this.backendService
|
||||
.getDomains(
|
||||
@@ -55,7 +56,8 @@ export class DomainListService {
|
||||
this.checkpointTime,
|
||||
pageNumber,
|
||||
resultsPerPage,
|
||||
totalResults
|
||||
totalResults,
|
||||
searchTerm
|
||||
)
|
||||
.pipe(
|
||||
tap((domainListResult: DomainListResult) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="registrarDetails">
|
||||
<div class="registrarDetails" *ngIf="registrarInEdit">
|
||||
<h3 mat-dialog-title>Edit Registrar: {{ registrarInEdit.registrarId }}</h3>
|
||||
<div mat-dialog-content>
|
||||
<form (ngSubmit)="saveAndClose($event)">
|
||||
<form (ngSubmit)="saveAndClose()">
|
||||
<mat-form-field class="registrarDetails__input">
|
||||
<mat-label>Registry Lock:</mat-label>
|
||||
<mat-select
|
||||
@@ -32,7 +32,7 @@
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button (click)="onCancel($event)">Cancel</button>
|
||||
<button mat-button (click)="this.params?.close()">Cancel</button>
|
||||
<button type="submit" mat-button color="primary">Save</button>
|
||||
</mat-dialog-actions>
|
||||
</form>
|
||||
|
||||
@@ -12,61 +12,38 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Injector } from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
import { Registrar, RegistrarService } from './registrar.service';
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import {
|
||||
MAT_BOTTOM_SHEET_DATA,
|
||||
MatBottomSheet,
|
||||
MatBottomSheetRef,
|
||||
} from '@angular/material/bottom-sheet';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialog,
|
||||
MatDialogRef,
|
||||
} from '@angular/material/dialog';
|
||||
import { MatChipInputEvent } from '@angular/material/chips';
|
||||
import { DialogBottomSheetContent } from '../shared/components/dialogBottomSheet.component';
|
||||
|
||||
const MOBILE_LAYOUT_BREAKPOINT = '(max-width: 599px)';
|
||||
type RegistrarDetailsParams = {
|
||||
close: Function;
|
||||
data: {
|
||||
registrar: Registrar;
|
||||
};
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'app-registrar-details',
|
||||
templateUrl: './registrarDetails.component.html',
|
||||
styleUrls: ['./registrarDetails.component.scss'],
|
||||
})
|
||||
export class RegistrarDetailsComponent {
|
||||
export class RegistrarDetailsComponent implements DialogBottomSheetContent {
|
||||
registrarInEdit!: Registrar;
|
||||
private elementRef:
|
||||
| MatBottomSheetRef<RegistrarDetailsComponent>
|
||||
| MatDialogRef<RegistrarDetailsComponent>;
|
||||
params?: RegistrarDetailsParams;
|
||||
|
||||
constructor(
|
||||
protected registrarService: RegistrarService,
|
||||
private injector: Injector
|
||||
) {
|
||||
// We only inject one, either Dialog or Bottom Sheet data
|
||||
// so one of the injectors is expected to fail
|
||||
try {
|
||||
var params = this.injector.get(MAT_DIALOG_DATA);
|
||||
this.elementRef = this.injector.get(MatDialogRef);
|
||||
} catch (e) {
|
||||
var params = this.injector.get(MAT_BOTTOM_SHEET_DATA);
|
||||
this.elementRef = this.injector.get(MatBottomSheetRef);
|
||||
}
|
||||
this.registrarInEdit = JSON.parse(JSON.stringify(params.registrar));
|
||||
constructor(protected registrarService: RegistrarService) {}
|
||||
|
||||
init(params: RegistrarDetailsParams) {
|
||||
this.params = params;
|
||||
this.registrarInEdit = JSON.parse(
|
||||
JSON.stringify(this.params.data.registrar)
|
||||
);
|
||||
}
|
||||
|
||||
onCancel(e: MouseEvent) {
|
||||
if (this.elementRef instanceof MatBottomSheetRef) {
|
||||
this.elementRef.dismiss();
|
||||
} else if (this.elementRef instanceof MatDialogRef) {
|
||||
this.elementRef.close();
|
||||
}
|
||||
}
|
||||
|
||||
saveAndClose(e: MouseEvent) {
|
||||
// TODO: Implement save call to API
|
||||
this.onCancel(e);
|
||||
saveAndClose() {
|
||||
this.params?.close();
|
||||
}
|
||||
|
||||
addTLD(e: MatChipInputEvent) {
|
||||
@@ -82,24 +59,3 @@ export class RegistrarDetailsComponent {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-registrar-details-wrapper',
|
||||
template: '',
|
||||
})
|
||||
export class RegistrarDetailsWrapperComponent {
|
||||
constructor(
|
||||
private dialog: MatDialog,
|
||||
private bottomSheet: MatBottomSheet,
|
||||
protected breakpointObserver: BreakpointObserver
|
||||
) {}
|
||||
|
||||
open(registrar: Registrar) {
|
||||
const config = { data: { registrar } };
|
||||
if (this.breakpointObserver.isMatched(MOBILE_LAYOUT_BREAKPOINT)) {
|
||||
this.bottomSheet.open(RegistrarDetailsComponent, config);
|
||||
} else {
|
||||
this.dialog.open(RegistrarDetailsComponent, config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
[pageSizeOptions]="[5, 10, 20]"
|
||||
showFirstLastButtons
|
||||
></mat-paginator>
|
||||
<app-registrar-details-wrapper
|
||||
<app-dialog-bottom-sheet-wrapper
|
||||
#registrarDetailsView
|
||||
></app-registrar-details-wrapper>
|
||||
></app-dialog-bottom-sheet-wrapper>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,8 @@ import { Registrar, RegistrarService } from './registrar.service';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { RegistrarDetailsWrapperComponent } from './registrarDetails.component';
|
||||
import { RegistrarDetailsComponent } from './registrarDetails.component';
|
||||
import { DialogBottomSheetWrapper } from '../shared/components/dialogBottomSheet.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-registrar',
|
||||
@@ -82,7 +83,7 @@ export class RegistrarComponent {
|
||||
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
||||
@ViewChild(MatSort) sort!: MatSort;
|
||||
@ViewChild('registrarDetailsView')
|
||||
detailsComponentWrapper!: RegistrarDetailsWrapperComponent;
|
||||
detailsComponentWrapper!: DialogBottomSheetWrapper;
|
||||
|
||||
constructor(protected registrarService: RegistrarService) {
|
||||
this.dataSource = new MatTableDataSource<Registrar>(
|
||||
@@ -97,7 +98,10 @@ export class RegistrarComponent {
|
||||
|
||||
openDetails(event: MouseEvent, registrar: Registrar) {
|
||||
event.stopPropagation();
|
||||
this.detailsComponentWrapper.open(registrar);
|
||||
this.detailsComponentWrapper.open<RegistrarDetailsComponent>(
|
||||
RegistrarDetailsComponent,
|
||||
{ registrar }
|
||||
);
|
||||
}
|
||||
|
||||
applyFilter(event: Event) {
|
||||
|
||||
@@ -41,4 +41,7 @@
|
||||
<mat-icon>add</mat-icon>Create a Contact
|
||||
</button>
|
||||
</div>
|
||||
<app-dialog-bottom-sheet-wrapper
|
||||
#contactDetailsWrapper
|
||||
></app-dialog-bottom-sheet-wrapper>
|
||||
</div>
|
||||
|
||||
@@ -12,21 +12,14 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import {
|
||||
MatDialog,
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogRef,
|
||||
} from '@angular/material/dialog';
|
||||
import {
|
||||
MatBottomSheet,
|
||||
MAT_BOTTOM_SHEET_DATA,
|
||||
MatBottomSheetRef,
|
||||
} from '@angular/material/bottom-sheet';
|
||||
import { Component, ViewChild } from '@angular/core';
|
||||
import { Contact, ContactService } from './contact.service';
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import {
|
||||
DialogBottomSheetContent,
|
||||
DialogBottomSheetWrapper,
|
||||
} from 'src/app/shared/components/dialogBottomSheet.component';
|
||||
|
||||
enum Operations {
|
||||
DELETE,
|
||||
@@ -40,7 +33,13 @@ interface GroupedContacts {
|
||||
contacts: Array<Contact>;
|
||||
}
|
||||
|
||||
let isMobile: boolean;
|
||||
type ContactDetailsParams = {
|
||||
close: Function;
|
||||
data: {
|
||||
contact: Contact;
|
||||
operation: Operations;
|
||||
};
|
||||
};
|
||||
|
||||
const contactTypes: Array<GroupedContacts> = [
|
||||
{ value: 'ADMIN', label: 'Primary contact', contacts: [] },
|
||||
@@ -52,72 +51,46 @@ const contactTypes: Array<GroupedContacts> = [
|
||||
{ value: 'WHOIS', label: 'WHOIS-Inquiry contact', contacts: [] },
|
||||
];
|
||||
|
||||
class ContactDetailsEventsResponder {
|
||||
private ref?: MatDialogRef<any> | MatBottomSheetRef;
|
||||
constructor() {
|
||||
this.onClose = this.onClose.bind(this);
|
||||
}
|
||||
|
||||
setRef(ref: MatDialogRef<any> | MatBottomSheetRef) {
|
||||
this.ref = ref;
|
||||
}
|
||||
|
||||
onClose() {
|
||||
if (this.ref == undefined) {
|
||||
throw "Reference to ContactDetailsDialogComponent hasn't been set. ";
|
||||
}
|
||||
if (this.ref instanceof MatBottomSheetRef) {
|
||||
this.ref.dismiss();
|
||||
} else if (this.ref instanceof MatDialogRef) {
|
||||
this.ref.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-contact-details-dialog',
|
||||
templateUrl: 'contactDetails.component.html',
|
||||
styleUrls: ['./contact.component.scss'],
|
||||
})
|
||||
export class ContactDetailsDialogComponent {
|
||||
contact: Contact;
|
||||
export class ContactDetailsDialogComponent implements DialogBottomSheetContent {
|
||||
contact?: Contact;
|
||||
contactTypes = contactTypes;
|
||||
operation: Operations;
|
||||
contactIndex: number;
|
||||
onCloseCallback: Function;
|
||||
contactIndex?: number;
|
||||
|
||||
params?: ContactDetailsParams;
|
||||
|
||||
constructor(
|
||||
public contactService: ContactService,
|
||||
private _snackBar: MatSnackBar,
|
||||
@Inject(isMobile ? MAT_BOTTOM_SHEET_DATA : MAT_DIALOG_DATA)
|
||||
public data: {
|
||||
onClose: Function;
|
||||
contact: Contact;
|
||||
operation: Operations;
|
||||
}
|
||||
) {
|
||||
this.onCloseCallback = data.onClose;
|
||||
this.contactIndex = contactService.contacts.findIndex(
|
||||
(c) => c === data.contact
|
||||
private _snackBar: MatSnackBar
|
||||
) {}
|
||||
|
||||
init(params: ContactDetailsParams) {
|
||||
this.params = params;
|
||||
this.contactIndex = this.contactService.contacts.findIndex(
|
||||
(c) => c === params.data.contact
|
||||
);
|
||||
this.contact = JSON.parse(JSON.stringify(data.contact));
|
||||
this.operation = data.operation;
|
||||
this.contact = JSON.parse(JSON.stringify(params.data.contact));
|
||||
}
|
||||
|
||||
onClose(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
this.onCloseCallback.call(this);
|
||||
close() {
|
||||
this.params?.close();
|
||||
}
|
||||
|
||||
saveAndClose(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
if (!this.contact || this.contactIndex === undefined) return;
|
||||
if (!(e.target as HTMLFormElement).checkValidity()) {
|
||||
return;
|
||||
}
|
||||
const operation = this.params?.data.operation;
|
||||
let operationObservable;
|
||||
if (this.operation === Operations.ADD) {
|
||||
if (operation === Operations.ADD) {
|
||||
operationObservable = this.contactService.addContact(this.contact);
|
||||
} else if (this.operation === Operations.UPDATE) {
|
||||
} else if (operation === Operations.UPDATE) {
|
||||
operationObservable = this.contactService.updateContact(
|
||||
this.contactIndex,
|
||||
this.contact
|
||||
@@ -127,7 +100,7 @@ export class ContactDetailsDialogComponent {
|
||||
}
|
||||
|
||||
operationObservable.subscribe({
|
||||
complete: this.onCloseCallback.bind(this),
|
||||
complete: () => this.close(),
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error);
|
||||
},
|
||||
@@ -143,11 +116,11 @@ export class ContactDetailsDialogComponent {
|
||||
export default class ContactComponent {
|
||||
public static PATH = 'contact';
|
||||
|
||||
@ViewChild('contactDetailsWrapper')
|
||||
detailsComponentWrapper!: DialogBottomSheetWrapper;
|
||||
|
||||
loading: boolean = false;
|
||||
constructor(
|
||||
private dialog: MatDialog,
|
||||
private bottomSheet: MatBottomSheet,
|
||||
private breakpointObserver: BreakpointObserver,
|
||||
public contactService: ContactService,
|
||||
private _snackBar: MatSnackBar
|
||||
) {
|
||||
@@ -195,20 +168,9 @@ export default class ContactComponent {
|
||||
operation: Operations = Operations.UPDATE
|
||||
) {
|
||||
e.preventDefault();
|
||||
// TODO: handle orientation change
|
||||
isMobile = this.breakpointObserver.isMatched('(max-width: 599px)');
|
||||
const responder = new ContactDetailsEventsResponder();
|
||||
const config = { data: { onClose: responder.onClose, contact, operation } };
|
||||
|
||||
if (isMobile) {
|
||||
const bottomSheetRef = this.bottomSheet.open(
|
||||
ContactDetailsDialogComponent,
|
||||
config
|
||||
);
|
||||
responder.setRef(bottomSheetRef);
|
||||
} else {
|
||||
const dialogRef = this.dialog.open(ContactDetailsDialogComponent, config);
|
||||
responder.setRef(dialogRef);
|
||||
}
|
||||
this.detailsComponentWrapper.open<ContactDetailsDialogComponent>(
|
||||
ContactDetailsDialogComponent,
|
||||
{ contact, operation }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export class ContactService {
|
||||
return this.backend
|
||||
.getContacts(this.registrarService.activeRegistrarId)
|
||||
.pipe(
|
||||
tap((contacts) => {
|
||||
tap((contacts = []) => {
|
||||
this.contacts = contacts;
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<h3 mat-dialog-title>Contact details</h3>
|
||||
<div mat-dialog-content>
|
||||
<div mat-dialog-content *ngIf="contact">
|
||||
<form (ngSubmit)="saveAndClose($event)">
|
||||
<p>
|
||||
<mat-form-field class="contact-details__input">
|
||||
@@ -97,7 +97,7 @@
|
||||
>
|
||||
</section>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button (click)="onClose($event)">Cancel</button>
|
||||
<button mat-button (click)="close()">Cancel</button>
|
||||
<button type="submit" mat-button>Save</button>
|
||||
</mat-dialog-actions>
|
||||
</form>
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright 2023 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 { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import { ComponentType } from '@angular/cdk/portal';
|
||||
import { Component } from '@angular/core';
|
||||
import {
|
||||
MatBottomSheet,
|
||||
MatBottomSheetRef,
|
||||
} from '@angular/material/bottom-sheet';
|
||||
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
|
||||
|
||||
const MOBILE_LAYOUT_BREAKPOINT = '(max-width: 599px)';
|
||||
|
||||
export interface DialogBottomSheetContent {
|
||||
init(data: Object): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps up a child component in an Angular Material Dalog for desktop or a Bottom Sheet
|
||||
* component for mobile depending on a screen resolution, with Breaking Point being 599px.
|
||||
* Child component is required to implement @see DialogBottomSheetContent interface
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-dialog-bottom-sheet-wrapper',
|
||||
template: '',
|
||||
})
|
||||
export class DialogBottomSheetWrapper {
|
||||
private elementRef?: MatBottomSheetRef | MatDialogRef<any>;
|
||||
|
||||
constructor(
|
||||
private dialog: MatDialog,
|
||||
private bottomSheet: MatBottomSheet,
|
||||
protected breakpointObserver: BreakpointObserver
|
||||
) {}
|
||||
|
||||
open<T extends DialogBottomSheetContent>(
|
||||
component: ComponentType<T>,
|
||||
data: any
|
||||
) {
|
||||
const config = { data, close: () => this.close() };
|
||||
if (this.breakpointObserver.isMatched(MOBILE_LAYOUT_BREAKPOINT)) {
|
||||
this.elementRef = this.bottomSheet.open(component);
|
||||
this.elementRef.instance.init(config);
|
||||
} else {
|
||||
this.elementRef = this.dialog.open(component);
|
||||
this.elementRef.componentInstance.init(config);
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.elementRef instanceof MatBottomSheetRef) {
|
||||
this.elementRef.dismiss();
|
||||
} else if (this.elementRef instanceof MatDialogRef) {
|
||||
this.elementRef.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,8 @@ export class BackendService {
|
||||
checkpointTime?: string,
|
||||
pageNumber?: number,
|
||||
resultsPerPage?: number,
|
||||
totalResults?: number
|
||||
totalResults?: number,
|
||||
searchTerm?: string
|
||||
): Observable<DomainListResult> {
|
||||
var url = `/console-api/domain-list?registrarId=${registrarId}`;
|
||||
if (checkpointTime) {
|
||||
@@ -84,6 +85,9 @@ export class BackendService {
|
||||
if (totalResults) {
|
||||
url += `&totalResults=${totalResults}`;
|
||||
}
|
||||
if (searchTerm) {
|
||||
url += `&searchTerm=${searchTerm}`;
|
||||
}
|
||||
return this.http
|
||||
.get<DomainListResult>(url)
|
||||
.pipe(catchError((err) => this.errorCatcher<DomainListResult>(err)));
|
||||
|
||||
@@ -16,8 +16,8 @@ package google.registry.bsa;
|
||||
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static com.google.common.collect.Maps.transformValues;
|
||||
import static google.registry.model.tld.Tld.isEnrolledWithBsa;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableMultimap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
@@ -50,14 +50,6 @@ public class IdnChecker {
|
||||
allTlds = idnToTlds.values().stream().flatMap(ImmutableSet::stream).collect(toImmutableSet());
|
||||
}
|
||||
|
||||
// TODO(11/30/2023): Remove below when new Tld schema is deployed and the `getBsaEnrollStartTime`
|
||||
// method is no longer hardcoded.
|
||||
@VisibleForTesting
|
||||
IdnChecker(ImmutableMap<IdnTableEnum, ImmutableSet<Tld>> idnToTlds) {
|
||||
this.idnToTlds = idnToTlds;
|
||||
allTlds = idnToTlds.values().stream().flatMap(ImmutableSet::stream).collect(toImmutableSet());
|
||||
}
|
||||
|
||||
/** Returns all IDNs in which the {@code label} is valid. */
|
||||
ImmutableSet<IdnTableEnum> getAllValidIdns(String label) {
|
||||
return idnToTlds.keySet().stream()
|
||||
@@ -88,11 +80,6 @@ public class IdnChecker {
|
||||
return Sets.difference(allTlds, getSupportingTlds(idnTables));
|
||||
}
|
||||
|
||||
private static boolean isEnrolledWithBsa(Tld tld, DateTime now) {
|
||||
DateTime enrollTime = tld.getBsaEnrollStartTime();
|
||||
return enrollTime != null && enrollTime.isBefore(now);
|
||||
}
|
||||
|
||||
private static ImmutableMap<IdnTableEnum, ImmutableSet<Tld>> getIdnToTldMap(DateTime now) {
|
||||
ImmutableMultimap.Builder<IdnTableEnum, Tld> idnToTldMap = new ImmutableMultimap.Builder();
|
||||
Tlds.getTldEntitiesOfType(TldType.REAL).stream()
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright 2023 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.bsa.persistence;
|
||||
|
||||
import static google.registry.bsa.persistence.BsaDomainRefresh.Stage.MAKE_DIFF;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import google.registry.model.CreateAutoTimestamp;
|
||||
import google.registry.model.UpdateAutoTimestamp;
|
||||
import google.registry.persistence.VKey;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.EnumType;
|
||||
import javax.persistence.Enumerated;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/**
|
||||
* Records of completed and ongoing refresh actions, which recomputes the set of unblockable domains
|
||||
* and reports changes to BSA.
|
||||
*
|
||||
* <p>The refresh action only handles registered and reserved domain names. Invalid names only
|
||||
* change status when the IDN tables change, and will be handled by a separate tool when it happens.
|
||||
*/
|
||||
@Entity
|
||||
public class BsaDomainRefresh {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
Long jobId;
|
||||
|
||||
@Column(nullable = false)
|
||||
CreateAutoTimestamp creationTime = CreateAutoTimestamp.create(null);
|
||||
|
||||
@Column(nullable = false)
|
||||
UpdateAutoTimestamp updateTime = UpdateAutoTimestamp.create(null);
|
||||
|
||||
@Column(nullable = false)
|
||||
@Enumerated(EnumType.STRING)
|
||||
Stage stage = MAKE_DIFF;
|
||||
|
||||
BsaDomainRefresh() {}
|
||||
|
||||
long getJobId() {
|
||||
return jobId;
|
||||
}
|
||||
|
||||
DateTime getCreationTime() {
|
||||
return creationTime.getTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the starting time of this job as a string, which can be used as folder name on GCS when
|
||||
* storing download data.
|
||||
*/
|
||||
public String getJobName() {
|
||||
return "refresh-" + getCreationTime().toString();
|
||||
}
|
||||
|
||||
public Stage getStage() {
|
||||
return this.stage;
|
||||
}
|
||||
|
||||
BsaDomainRefresh setStage(Stage stage) {
|
||||
this.stage = stage;
|
||||
return this;
|
||||
}
|
||||
|
||||
VKey<BsaDomainRefresh> vKey() {
|
||||
return vKey(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof BsaDomainRefresh)) {
|
||||
return false;
|
||||
}
|
||||
BsaDomainRefresh that = (BsaDomainRefresh) o;
|
||||
return Objects.equal(jobId, that.jobId)
|
||||
&& Objects.equal(creationTime, that.creationTime)
|
||||
&& Objects.equal(updateTime, that.updateTime)
|
||||
&& stage == that.stage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(jobId, creationTime, updateTime, stage);
|
||||
}
|
||||
|
||||
static VKey vKey(BsaDomainRefresh bsaDomainRefresh) {
|
||||
return VKey.create(BsaDomainRefresh.class, bsaDomainRefresh.jobId);
|
||||
}
|
||||
|
||||
enum Stage {
|
||||
MAKE_DIFF,
|
||||
APPLY_DIFF,
|
||||
REPORT_REMOVALS,
|
||||
REPORT_ADDITIONS;
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ import org.joda.time.DateTime;
|
||||
* <p>The label is valid (wrt IDN) in at least one TLD.
|
||||
*/
|
||||
@Entity
|
||||
public final class BsaLabel {
|
||||
final class BsaLabel {
|
||||
|
||||
@Id String label;
|
||||
|
||||
@@ -52,7 +52,7 @@ public final class BsaLabel {
|
||||
}
|
||||
|
||||
/** Returns the label to be blocked. */
|
||||
public String getLabel() {
|
||||
String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright 2023 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.bsa.persistence;
|
||||
|
||||
import static google.registry.config.RegistryConfig.getEppResourceCachingDuration;
|
||||
import static google.registry.config.RegistryConfig.getEppResourceMaxCachedEntries;
|
||||
import static google.registry.model.CacheUtils.newCacheBuilder;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.CacheLoader;
|
||||
import com.github.benmanes.caffeine.cache.LoadingCache;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import google.registry.persistence.VKey;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Helpers for {@link BsaLabel}. */
|
||||
public final class BsaLabelUtils {
|
||||
|
||||
private BsaLabelUtils() {}
|
||||
|
||||
static final CacheLoader<VKey<BsaLabel>, Optional<BsaLabel>> CACHE_LOADER =
|
||||
new CacheLoader<VKey<BsaLabel>, Optional<BsaLabel>>() {
|
||||
|
||||
@Override
|
||||
public Optional<BsaLabel> load(VKey<BsaLabel> key) {
|
||||
return replicaTm().reTransact(() -> replicaTm().loadByKeyIfPresent(key));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<VKey<BsaLabel>, Optional<BsaLabel>> loadAll(
|
||||
Iterable<? extends VKey<BsaLabel>> keys) {
|
||||
// TODO(b/309173359): need this for DomainCheckFlow
|
||||
throw new UnsupportedOperationException(
|
||||
"LoadAll not supported by the BsaLabel cache loader.");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A limited size, limited expiry cache of BSA labels.
|
||||
*
|
||||
* <p>BSA labels are used by the domain creation flow to verify that the requested domain name is
|
||||
* not blocked by the BSA program. Label caching is mainly a defense against two scenarios, the
|
||||
* initial rush and drop-catching, when clients run back-to-back domain creation requests around
|
||||
* the time when a domain becomes available.
|
||||
*
|
||||
* <p>Because of caching and the use of the replica database, new BSA labels installed in the
|
||||
* database will not take effect immediately. A blocked domain may be created due to race
|
||||
* condition. A `refresh` job will detect such domains and report them to BSA as unblockable
|
||||
* domains.
|
||||
*
|
||||
* <p>Since the cached BSA labels have the same usage pattern as the cached EppResources, the
|
||||
* cache configuration for the latter are reused here.
|
||||
*/
|
||||
private static LoadingCache<VKey<BsaLabel>, Optional<BsaLabel>> cacheBsaLabels =
|
||||
createBsaLabelsCache(getEppResourceCachingDuration());
|
||||
|
||||
private static LoadingCache<VKey<BsaLabel>, Optional<BsaLabel>> createBsaLabelsCache(
|
||||
Duration expiry) {
|
||||
return newCacheBuilder(expiry)
|
||||
.maximumSize(getEppResourceMaxCachedEntries())
|
||||
.build(CACHE_LOADER);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void clearCache() {
|
||||
cacheBsaLabels.invalidateAll();
|
||||
}
|
||||
|
||||
/** Checks if the {@code domainLabel} (the leading `part` of a domain name) is blocked by BSA. */
|
||||
public static boolean isLabelBlocked(String domainLabel) {
|
||||
return cacheBsaLabels.get(BsaLabel.vKey(domainLabel)).isPresent();
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ import static google.registry.flows.domain.DomainFlowUtils.verifyClaimsNoticeIfA
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyClaimsPeriodNotEnded;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyLaunchPhaseMatchesRegistryPhase;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyNoCodeMarks;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyNotBlockedByBsa;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyNotReserved;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyPremiumNameIsNotBlocked;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyRegistrarIsActive;
|
||||
@@ -168,6 +169,7 @@ import org.joda.time.Duration;
|
||||
* @error {@link DomainFlowUtils.CurrencyUnitMismatchException}
|
||||
* @error {@link DomainFlowUtils.CurrencyValueScaleException}
|
||||
* @error {@link DomainFlowUtils.DashesInThirdAndFourthException}
|
||||
* @error {@link DomainFlowUtils.DomainLabelBlockedByBsaException}
|
||||
* @error {@link DomainFlowUtils.DomainLabelTooLongException}
|
||||
* @error {@link DomainFlowUtils.DomainReservedException}
|
||||
* @error {@link DomainFlowUtils.DuplicateContactForRoleException}
|
||||
@@ -328,6 +330,7 @@ public final class DomainCreateFlow implements MutatingFlow {
|
||||
.verifySignedMarks(launchCreate.get().getSignedMarks(), domainLabel, now)
|
||||
.getId();
|
||||
}
|
||||
verifyNotBlockedByBsa(domainLabel, tld, now);
|
||||
flowCustomLogic.afterValidation(
|
||||
DomainCreateFlowCustomLogic.AfterValidationParameters.newBuilder()
|
||||
.setDomainName(domainName)
|
||||
|
||||
@@ -25,11 +25,13 @@ import static com.google.common.collect.Iterables.any;
|
||||
import static com.google.common.collect.Sets.difference;
|
||||
import static com.google.common.collect.Sets.intersection;
|
||||
import static com.google.common.collect.Sets.union;
|
||||
import static google.registry.bsa.persistence.BsaLabelUtils.isLabelBlocked;
|
||||
import static google.registry.model.domain.Domain.MAX_REGISTRATION_YEARS;
|
||||
import static google.registry.model.tld.Tld.TldState.GENERAL_AVAILABILITY;
|
||||
import static google.registry.model.tld.Tld.TldState.PREDELEGATION;
|
||||
import static google.registry.model.tld.Tld.TldState.QUIET_PERIOD;
|
||||
import static google.registry.model.tld.Tld.TldState.START_DATE_SUNRISE;
|
||||
import static google.registry.model.tld.Tld.isEnrolledWithBsa;
|
||||
import static google.registry.model.tld.Tlds.findTldForName;
|
||||
import static google.registry.model.tld.Tlds.getTlds;
|
||||
import static google.registry.model.tld.label.ReservationType.ALLOWED_IN_SUNRISE;
|
||||
@@ -259,6 +261,19 @@ public class DomainFlowUtils {
|
||||
return idnTableName.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the {@code domainLabel} is not blocked by any BSA block label for the given
|
||||
* {@code tld} at the specified time.
|
||||
*
|
||||
* @throws DomainLabelBlockedByBsaException
|
||||
*/
|
||||
public static void verifyNotBlockedByBsa(String domainLabel, Tld tld, DateTime now)
|
||||
throws DomainLabelBlockedByBsaException {
|
||||
if (isEnrolledWithBsa(tld, now) && isLabelBlocked(domainLabel)) {
|
||||
throw new DomainLabelBlockedByBsaException();
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns whether a given domain create request is for a valid anchor tenant. */
|
||||
public static boolean isAnchorTenant(
|
||||
InternetDomainName domainName,
|
||||
@@ -1742,4 +1757,12 @@ public class DomainFlowUtils {
|
||||
super("Registrar must be active in order to perform this operation");
|
||||
}
|
||||
}
|
||||
|
||||
/** Domain label is blocked by the Brand Safety Alliance. */
|
||||
static class DomainLabelBlockedByBsaException extends ParameterValuePolicyErrorException {
|
||||
public DomainLabelBlockedByBsaException() {
|
||||
// TODO(b/309174065): finalize the exception message.
|
||||
super("Domain label is blocked by the Brand Safety Alliance");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import static com.google.common.base.Strings.nullToEmpty;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
|
||||
import static com.google.common.collect.Sets.immutableEnumSet;
|
||||
import static com.google.common.collect.Streams.stream;
|
||||
import static com.google.common.io.BaseEncoding.base64;
|
||||
import static google.registry.config.RegistryConfig.getDefaultRegistrarWhoisServer;
|
||||
import static google.registry.model.CacheUtils.memoizeWithShortExpiration;
|
||||
@@ -794,6 +795,24 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
|
||||
}
|
||||
}
|
||||
|
||||
// Making sure there's no registrar with the same ianaId already in the system
|
||||
private static boolean isNotADuplicateIanaId(
|
||||
Iterable<Registrar> registrars, Registrar newInstance) {
|
||||
// Return early if newly build registrar is not type REAL or ianaId is
|
||||
// reserved by ICANN - https://www.iana.org/assignments/registrar-ids/registrar-ids.xhtml
|
||||
if (!Type.REAL.equals(newInstance.type)
|
||||
|| ImmutableSet.of(1L, 8L).contains(newInstance.ianaIdentifier)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return stream(registrars)
|
||||
.filter(registrar -> Type.REAL.equals(registrar.getType()))
|
||||
.filter(registrar -> !Objects.equals(newInstance.registrarId, registrar.getRegistrarId()))
|
||||
.noneMatch(
|
||||
registrar ->
|
||||
Objects.equals(newInstance.ianaIdentifier, registrar.getIanaIdentifier()));
|
||||
}
|
||||
|
||||
public Builder setContactsRequireSyncing(boolean contactsRequireSyncing) {
|
||||
getInstance().contactsRequireSyncing = contactsRequireSyncing;
|
||||
return this;
|
||||
@@ -912,6 +931,15 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
|
||||
"Supplied IANA ID is not valid for %s registrar type: %s",
|
||||
getInstance().type, getInstance().ianaIdentifier));
|
||||
|
||||
// We do not allow creating Real registrars with IANA ID that's already in the system
|
||||
// b/315007360 - for more details
|
||||
checkArgument(
|
||||
isNotADuplicateIanaId(loadAllCached(), getInstance()),
|
||||
String.format(
|
||||
"Rejected attempt to create a registrar with ianaId that's already in the system -"
|
||||
+ " %s",
|
||||
getInstance().ianaIdentifier));
|
||||
|
||||
// In order to grant access to real TLDs, the registrar must have a corresponding billing
|
||||
// account ID for that TLD's billing currency.
|
||||
ImmutableSet<String> nonBillableTlds =
|
||||
|
||||
@@ -256,6 +256,11 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
return VKey.create(Tld.class, tldStr);
|
||||
}
|
||||
|
||||
/** Checks if {@code tld} is enrolled with BSA. */
|
||||
public static boolean isEnrolledWithBsa(Tld tld, DateTime now) {
|
||||
return tld.getBsaEnrollStartTime().orElse(END_OF_TIME).isBefore(now);
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the pricing engine that this TLD uses.
|
||||
*
|
||||
@@ -550,9 +555,15 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
@JsonSerialize(using = SortedEnumSetSerializer.class)
|
||||
Set<IdnTableEnum> idnTables;
|
||||
|
||||
// TODO(11/30/2023): uncomment below two lines
|
||||
// /** The start time of this TLD's enrollment in the BSA program, if applicable. */
|
||||
// @JsonIgnore @Nullable DateTime bsaEnrollStartTime;
|
||||
/**
|
||||
* The start time of this TLD's enrollment in the BSA program, if applicable.
|
||||
*
|
||||
* <p>This property is excluded from source-based configuration and is managed directly in the
|
||||
* database.
|
||||
*/
|
||||
// TODO(b/309175410): implement setup and cleanup procedure for joining or leaving BSA, and see
|
||||
// if it can be integrated with the ConfigTldCommand.
|
||||
@JsonIgnore @Nullable DateTime bsaEnrollStartTime;
|
||||
|
||||
public String getTldStr() {
|
||||
return tldStr;
|
||||
@@ -574,12 +585,9 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
}
|
||||
|
||||
/** Returns the time when this TLD was enrolled in the Brand Safety Alliance (BSA) program. */
|
||||
@JsonIgnore // Annotation can be removed once we add the field and annotate it.
|
||||
@Nullable
|
||||
public DateTime getBsaEnrollStartTime() {
|
||||
// TODO(11/30/2023): uncomment below.
|
||||
// return this.bsaEnrollStartTime;
|
||||
return null;
|
||||
@JsonIgnore
|
||||
public Optional<DateTime> getBsaEnrollStartTime() {
|
||||
return Optional.ofNullable(this.bsaEnrollStartTime);
|
||||
}
|
||||
|
||||
/** Retrieve whether invoicing is enabled. */
|
||||
@@ -1101,10 +1109,9 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setBsaEnrollStartTime(DateTime enrollTime) {
|
||||
public Builder setBsaEnrollStartTime(Optional<DateTime> enrollTime) {
|
||||
// TODO(b/309175133): forbid if enrolled with BSA
|
||||
// TODO(11/30/2023): uncomment below line
|
||||
// getInstance().bsaEnrollStartTime = enrollTime;
|
||||
getInstance().bsaEnrollStartTime = enrollTime.orElse(null);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Streams;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.flogger.StackSize;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.persistence.JpaRetries;
|
||||
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
|
||||
@@ -164,7 +165,10 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
if (!getHibernateAllowNestedTransactions()) {
|
||||
throw new IllegalStateException(NESTED_TRANSACTION_MESSAGE);
|
||||
}
|
||||
logger.atWarning().withStackTrace(StackSize.MEDIUM).log(NESTED_TRANSACTION_MESSAGE);
|
||||
if (RegistryEnvironment.get() != RegistryEnvironment.PRODUCTION
|
||||
&& RegistryEnvironment.get() != RegistryEnvironment.UNITTEST) {
|
||||
logger.atWarning().withStackTrace(StackSize.MEDIUM).log(NESTED_TRANSACTION_MESSAGE);
|
||||
}
|
||||
// This prevents inner transaction from retrying, thus avoiding a cascade retry effect.
|
||||
return transactNoRetry(work, isolationLevel);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
|
||||
package google.registry.reporting.icann;
|
||||
|
||||
import static com.google.api.client.http.HttpStatusCodes.STATUS_CODE_BAD_REQUEST;
|
||||
import static com.google.api.client.http.HttpStatusCodes.STATUS_CODE_OK;
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.net.MediaType.CSV_UTF_8;
|
||||
@@ -38,6 +37,7 @@ import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.List;
|
||||
import javax.inject.Inject;
|
||||
@@ -90,30 +90,31 @@ public class IcannHttpReporter {
|
||||
UrlConnectionUtils.setPayload(connection, reportBytes, CSV_UTF_8.toString());
|
||||
connection.setInstanceFollowRedirects(false);
|
||||
|
||||
int responseCode;
|
||||
byte[] content;
|
||||
int responseCode = 0;
|
||||
byte[] content = null;
|
||||
try {
|
||||
responseCode = connection.getResponseCode();
|
||||
// Only responses with a 200 or 400 status have a body. For everything else, we can return
|
||||
// false early.
|
||||
if (responseCode != STATUS_CODE_OK && responseCode != STATUS_CODE_BAD_REQUEST) {
|
||||
logger.atWarning().log("Connection to ICANN server failed", connection);
|
||||
content = UrlConnectionUtils.getResponseBytes(connection);
|
||||
if (responseCode != STATUS_CODE_OK) {
|
||||
XjcIirdeaResult result = parseResult(content);
|
||||
logger.atWarning().log(
|
||||
"PUT rejected, status code %s:\n%s\n%s",
|
||||
result.getCode().getValue(), result.getMsg(), result.getDescription());
|
||||
return false;
|
||||
}
|
||||
content = UrlConnectionUtils.getResponseBytes(connection);
|
||||
} catch (IOException e) {
|
||||
logger.atWarning().withCause(e).log(
|
||||
"Connection to ICANN server failed with responseCode %s and connection %s",
|
||||
responseCode == 0 ? "not available" : responseCode, connection);
|
||||
return false;
|
||||
} catch (XmlException e) {
|
||||
logger.atWarning().withCause(e).log(
|
||||
"Failed to parse ICANN response with responseCode %s and content %s",
|
||||
responseCode, new String(content, StandardCharsets.UTF_8));
|
||||
return false;
|
||||
} finally {
|
||||
connection.disconnect();
|
||||
}
|
||||
// We know that an HTTP 200 response can only contain a result code of
|
||||
// 1000 (i. e. success), there is no need to parse it.
|
||||
// See: https://tools.ietf.org/html/draft-lozano-icann-registry-interfaces-13#page-16
|
||||
if (responseCode != STATUS_CODE_OK) {
|
||||
XjcIirdeaResult result = parseResult(content);
|
||||
logger.atWarning().log(
|
||||
"PUT rejected, status code %s:\n%s\n%s",
|
||||
result.getCode().getValue(), result.getMsg(), result.getDescription());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -164,4 +165,5 @@ public class IcannHttpReporter {
|
||||
reportType));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ import java.net.URL;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import javax.inject.Inject;
|
||||
import org.joda.time.Duration;
|
||||
@@ -126,55 +127,62 @@ public final class NordnUploadAction implements Runnable {
|
||||
phase.equals(PARAM_LORDN_PHASE_SUNRISE) || phase.equals(PARAM_LORDN_PHASE_CLAIMS),
|
||||
"Invalid phase specified to NordnUploadAction: %s.",
|
||||
phase);
|
||||
tm().transact(
|
||||
() -> {
|
||||
// Note here that we load all domains pending Nordn in one batch, which should not
|
||||
// be a problem for the rate of domain registration that we see. If we anticipate
|
||||
// a peak in claims during TLD launch (sunrise is NOT first-come-first-serve, so
|
||||
// there should be no expectation of a peak during it), we can consider temporarily
|
||||
// increasing the frequency of Nordn upload to reduce the size of each batch.
|
||||
//
|
||||
// We did not further divide the domains into smaller batches because the
|
||||
// read-upload-write operation per small batch needs to be inside a single
|
||||
// transaction to prevent race conditions, and running several uploads in rapid
|
||||
// sucession will likely overwhelm the MarksDB upload server, which recommands a
|
||||
// maximum upload frequency of every 3 hours.
|
||||
//
|
||||
// See:
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-regext-tmch-func-spec-01#section-5.2.3.3
|
||||
List<Domain> domains =
|
||||
tm().createQueryComposer(Domain.class)
|
||||
.where("lordnPhase", EQ, LordnPhase.valueOf(Ascii.toUpperCase(phase)))
|
||||
.where("tld", EQ, tld)
|
||||
.orderBy("creationTime")
|
||||
.list();
|
||||
if (domains.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
StringBuilder csv = new StringBuilder();
|
||||
ImmutableList.Builder<Domain> newDomains = new ImmutableList.Builder<>();
|
||||
Optional<URL> uploadUrl =
|
||||
tm().transact(
|
||||
() -> {
|
||||
// Note here that we load all domains pending Nordn in one batch, which should not
|
||||
// be a problem for the rate of domain registration that we see. If we anticipate
|
||||
// a peak in claims during TLD launch (sunrise is NOT first-come-first-serve, so
|
||||
// there should be no expectation of a peak during it), we can consider
|
||||
// temporarily increasing the frequency of Nordn upload to reduce the size of each
|
||||
// batch.
|
||||
//
|
||||
// We did not further divide the domains into smaller batches because the
|
||||
// read-upload-write operation per small batch needs to be inside a single
|
||||
// transaction to prevent race conditions, and running several uploads in rapid
|
||||
// succession will likely overwhelm the MarksDB upload server, which recommends a
|
||||
// maximum upload frequency of every 3 hours.
|
||||
//
|
||||
// See:
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-regext-tmch-func-spec-01#section-5.2.3.3
|
||||
List<Domain> domains =
|
||||
tm().createQueryComposer(Domain.class)
|
||||
.where("lordnPhase", EQ, LordnPhase.valueOf(Ascii.toUpperCase(phase)))
|
||||
.where("tld", EQ, tld)
|
||||
.orderBy("creationTime")
|
||||
.list();
|
||||
if (domains.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
StringBuilder csv = new StringBuilder();
|
||||
ImmutableList.Builder<Domain> newDomains = new ImmutableList.Builder<>();
|
||||
|
||||
domains.forEach(
|
||||
domain -> {
|
||||
if (phase.equals(PARAM_LORDN_PHASE_SUNRISE)) {
|
||||
csv.append(getCsvLineForSunriseDomain(domain)).append('\n');
|
||||
} else {
|
||||
csv.append(getCsvLineForClaimsDomain(domain)).append('\n');
|
||||
}
|
||||
Domain newDomain = domain.asBuilder().setLordnPhase(LordnPhase.NONE).build();
|
||||
newDomains.add(newDomain);
|
||||
});
|
||||
String columns =
|
||||
phase.equals(PARAM_LORDN_PHASE_SUNRISE) ? COLUMNS_SUNRISE : COLUMNS_CLAIMS;
|
||||
String header =
|
||||
String.format("1,%s,%d\n%s\n", clock.nowUtc(), domains.size(), columns);
|
||||
try {
|
||||
uploadCsvToLordn(String.format("/LORDN/%s/%s", tld, phase), header + csv);
|
||||
} catch (IOException | GeneralSecurityException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
tm().updateAll(newDomains.build());
|
||||
});
|
||||
domains.forEach(
|
||||
domain -> {
|
||||
if (phase.equals(PARAM_LORDN_PHASE_SUNRISE)) {
|
||||
csv.append(getCsvLineForSunriseDomain(domain)).append('\n');
|
||||
} else {
|
||||
csv.append(getCsvLineForClaimsDomain(domain)).append('\n');
|
||||
}
|
||||
Domain newDomain =
|
||||
domain.asBuilder().setLordnPhase(LordnPhase.NONE).build();
|
||||
newDomains.add(newDomain);
|
||||
});
|
||||
String columns =
|
||||
phase.equals(PARAM_LORDN_PHASE_SUNRISE) ? COLUMNS_SUNRISE : COLUMNS_CLAIMS;
|
||||
String header =
|
||||
String.format("1,%s,%d\n%s\n", clock.nowUtc(), domains.size(), columns);
|
||||
try {
|
||||
URL url =
|
||||
uploadCsvToLordn(String.format("/LORDN/%s/%s", tld, phase), header + csv);
|
||||
tm().updateAll(newDomains.build());
|
||||
return Optional.of(url);
|
||||
} catch (IOException | GeneralSecurityException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
uploadUrl.ifPresent(
|
||||
url -> cloudTasksUtils.enqueue(NordnVerifyAction.QUEUE, makeVerifyTask(url)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,7 +194,7 @@ public final class NordnUploadAction implements Runnable {
|
||||
* @see <a href="http://tools.ietf.org/html/draft-lozano-tmch-func-spec-08#section-6.3">TMCH
|
||||
* functional specifications - LORDN File</a>
|
||||
*/
|
||||
private void uploadCsvToLordn(String urlPath, String csvData)
|
||||
private URL uploadCsvToLordn(String urlPath, String csvData)
|
||||
throws IOException, GeneralSecurityException {
|
||||
String url = tmchMarksdbUrl + urlPath;
|
||||
logger.atInfo().log(
|
||||
@@ -222,7 +230,7 @@ public final class NordnUploadAction implements Runnable {
|
||||
actionLogId),
|
||||
connection);
|
||||
}
|
||||
cloudTasksUtils.enqueue(NordnVerifyAction.QUEUE, makeVerifyTask(new URL(location)));
|
||||
return new URL(location);
|
||||
} catch (IOException e) {
|
||||
throw new IOException(String.format("Error connecting to MarksDB at URL %s", url), e);
|
||||
} finally {
|
||||
|
||||
@@ -72,9 +72,9 @@ public class ConfigureTldCommand extends MutatingCommand {
|
||||
boolean breakglass;
|
||||
|
||||
@Parameter(
|
||||
names = {"-d", "--dryrun"},
|
||||
names = {"-d", "--dry_run"},
|
||||
description = "Does not execute the entity mutation")
|
||||
boolean dryrun;
|
||||
boolean dryRun;
|
||||
|
||||
@Inject ObjectMapper mapper;
|
||||
|
||||
@@ -122,6 +122,13 @@ public class ConfigureTldCommand extends MutatingCommand {
|
||||
checkPremiumList(newTld);
|
||||
checkDnsWriters(newTld);
|
||||
checkCurrency(newTld);
|
||||
// bsaEnrollStartTime only exists in DB. Need to carry it over to the updated copy. See Tld.java
|
||||
// for more information.
|
||||
Optional<DateTime> bsaEnrollTime =
|
||||
Optional.ofNullable(oldTld).flatMap(Tld::getBsaEnrollStartTime);
|
||||
if (bsaEnrollTime.isPresent()) {
|
||||
newTld = newTld.asBuilder().setBsaEnrollStartTime(bsaEnrollTime).build();
|
||||
}
|
||||
// Set the new TLD to breakglass mode if breakglass flag was used
|
||||
if (breakglass) {
|
||||
newTld = newTld.asBuilder().setBreakglassMode(true).build();
|
||||
@@ -131,7 +138,7 @@ public class ConfigureTldCommand extends MutatingCommand {
|
||||
|
||||
@Override
|
||||
protected boolean dontRunCommand() {
|
||||
if (dryrun) {
|
||||
if (dryRun) {
|
||||
return true;
|
||||
}
|
||||
if (!newDiff) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static google.registry.util.ListNamingUtils.convertFilePathToName;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.beust.jcommander.Parameter;
|
||||
import com.beust.jcommander.Parameters;
|
||||
import com.google.common.base.Strings;
|
||||
import google.registry.model.tld.label.PremiumList;
|
||||
@@ -29,6 +30,14 @@ import java.nio.file.Files;
|
||||
@Parameters(separators = " =", commandDescription = "Update a PremiumList in Database.")
|
||||
class UpdatePremiumListCommand extends CreateOrUpdatePremiumListCommand {
|
||||
|
||||
@Parameter(
|
||||
names = {"-d", "--dry_run"},
|
||||
description = "Does not execute the entity mutation")
|
||||
boolean dryRun;
|
||||
|
||||
// indicates if there is a new change made by this command
|
||||
private boolean newChange = false;
|
||||
|
||||
@Override
|
||||
protected String prompt() throws Exception {
|
||||
name = Strings.isNullOrEmpty(name) ? convertFilePathToName(inputFile) : name;
|
||||
@@ -43,8 +52,23 @@ class UpdatePremiumListCommand extends CreateOrUpdatePremiumListCommand {
|
||||
checkArgument(!inputData.isEmpty(), "New premium list data cannot be empty");
|
||||
currency = existingList.getCurrency();
|
||||
PremiumList updatedPremiumList = PremiumListUtils.parseToPremiumList(name, currency, inputData);
|
||||
return String.format(
|
||||
"Update premium list for %s?\n Old List: %s\n New List: %s",
|
||||
name, existingList, updatedPremiumList);
|
||||
if (!existingList
|
||||
.getLabelsToPrices()
|
||||
.entrySet()
|
||||
.equals(updatedPremiumList.getLabelsToPrices().entrySet())) {
|
||||
newChange = true;
|
||||
return String.format(
|
||||
"Update premium list for %s?\n Old List: %s\n New List: %s",
|
||||
name, existingList, updatedPremiumList);
|
||||
} else {
|
||||
return String.format(
|
||||
"This update contains no changes to the premium list for %s.\n List Contents: %s",
|
||||
name, existingList);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean dontRunCommand() {
|
||||
return dryRun || !newChange;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
|
||||
<mapping-file>META-INF/orm.xml</mapping-file>
|
||||
|
||||
<class>google.registry.bsa.persistence.BsaDomainRefresh</class>
|
||||
<class>google.registry.bsa.persistence.BsaDownload</class>
|
||||
<class>google.registry.bsa.persistence.BsaLabel</class>
|
||||
<class>google.registry.bsa.persistence.BsaDomainInUse</class>
|
||||
|
||||
@@ -15,38 +15,65 @@
|
||||
package google.registry.bsa;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.DatabaseHelper.createTld;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static google.registry.tldconfig.idn.IdnTableEnum.EXTENDED_LATIN;
|
||||
import static google.registry.tldconfig.idn.IdnTableEnum.JA;
|
||||
import static google.registry.tldconfig.idn.IdnTableEnum.UNCONFUSABLE_LATIN;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.model.tld.Tld;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationWithCoverageExtension;
|
||||
import google.registry.testing.FakeClock;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
/** Unit tests for {@link IdnChecker}. */
|
||||
public class IdnCheckerTest {
|
||||
|
||||
@Mock Tld jaonly;
|
||||
@Mock Tld jandelatin;
|
||||
@Mock Tld strictlatin;
|
||||
FakeClock fakeClock = new FakeClock();
|
||||
|
||||
@RegisterExtension
|
||||
final JpaIntegrationWithCoverageExtension jpa =
|
||||
new JpaTestExtensions.Builder().withClock(fakeClock).buildIntegrationWithCoverageExtension();
|
||||
|
||||
Tld jaonly;
|
||||
Tld jandelatin;
|
||||
Tld strictlatin;
|
||||
IdnChecker idnChecker;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
idnChecker =
|
||||
new IdnChecker(
|
||||
ImmutableMap.of(
|
||||
JA,
|
||||
ImmutableSet.of(jandelatin, jaonly),
|
||||
EXTENDED_LATIN,
|
||||
ImmutableSet.of(jandelatin),
|
||||
UNCONFUSABLE_LATIN,
|
||||
ImmutableSet.of(strictlatin)));
|
||||
jaonly = createTld("jaonly");
|
||||
jandelatin = createTld("jandelatin");
|
||||
strictlatin = createTld("strictlatin");
|
||||
|
||||
jaonly =
|
||||
persistResource(
|
||||
jaonly
|
||||
.asBuilder()
|
||||
.setBsaEnrollStartTime(Optional.of(fakeClock.nowUtc()))
|
||||
.setIdnTables(ImmutableSet.of(JA))
|
||||
.build());
|
||||
jandelatin =
|
||||
persistResource(
|
||||
jandelatin
|
||||
.asBuilder()
|
||||
.setBsaEnrollStartTime(Optional.of(fakeClock.nowUtc()))
|
||||
.setIdnTables(ImmutableSet.of(JA, EXTENDED_LATIN))
|
||||
.build());
|
||||
strictlatin =
|
||||
persistResource(
|
||||
strictlatin
|
||||
.asBuilder()
|
||||
.setBsaEnrollStartTime(Optional.of(fakeClock.nowUtc()))
|
||||
.setIdnTables(ImmutableSet.of(UNCONFUSABLE_LATIN))
|
||||
.build());
|
||||
fakeClock.advanceOneMilli();
|
||||
idnChecker = new IdnChecker(fakeClock);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright 2023 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.bsa.persistence;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.bsa.persistence.BsaDomainRefresh.Stage.MAKE_DIFF;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
|
||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationWithCoverageExtension;
|
||||
import google.registry.testing.FakeClock;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
/** Unit test for {@link BsaDomainRefresh}. */
|
||||
public class BsaDomainRefreshTest {
|
||||
|
||||
protected FakeClock fakeClock = new FakeClock(DateTime.now(UTC));
|
||||
|
||||
@RegisterExtension
|
||||
final JpaIntegrationWithCoverageExtension jpa =
|
||||
new JpaTestExtensions.Builder().withClock(fakeClock).buildIntegrationWithCoverageExtension();
|
||||
|
||||
@Test
|
||||
void saveJob() {
|
||||
BsaDomainRefresh persisted =
|
||||
tm().transact(() -> tm().getEntityManager().merge(new BsaDomainRefresh()));
|
||||
assertThat(persisted.jobId).isNotNull();
|
||||
assertThat(persisted.creationTime.getTimestamp()).isEqualTo(fakeClock.nowUtc());
|
||||
assertThat(persisted.stage).isEqualTo(MAKE_DIFF);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadJobByKey() {
|
||||
BsaDomainRefresh persisted =
|
||||
tm().transact(() -> tm().getEntityManager().merge(new BsaDomainRefresh()));
|
||||
assertThat(tm().transact(() -> tm().loadByKey(BsaDomainRefresh.vKey(persisted))))
|
||||
.isEqualTo(persisted);
|
||||
}
|
||||
}
|
||||
@@ -41,4 +41,15 @@ public class BsaLabelTest {
|
||||
assertThat(persisted.getLabel()).isEqualTo("label");
|
||||
assertThat(persisted.creationTime).isEqualTo(fakeClock.nowUtc());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isLabelBlocked_no() {
|
||||
assertThat(tm().transact(() -> BsaLabelUtils.isLabelBlocked("abc"))).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isLabelBlocked_yes() {
|
||||
tm().transact(() -> tm().put(new BsaLabel("abc", fakeClock.nowUtc())));
|
||||
assertThat(tm().transact(() -> BsaLabelUtils.isLabelBlocked("abc"))).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright 2023 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.bsa.persistence;
|
||||
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Testing utils for users of {@link BsaLabel}. */
|
||||
public final class BsaLabelTestingUtils {
|
||||
|
||||
private BsaLabelTestingUtils() {}
|
||||
|
||||
public static void persistBsaLabel(String domainLabel, DateTime creationTime) {
|
||||
tm().transact(() -> tm().put(new BsaLabel(domainLabel, creationTime)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright 2023 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.bsa.persistence;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.bsa.persistence.BsaLabelTestingUtils.persistBsaLabel;
|
||||
import static google.registry.bsa.persistence.BsaLabelUtils.isLabelBlocked;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.setJpaTm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.setReplicaJpaTm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
import static org.joda.time.Duration.millis;
|
||||
import static org.joda.time.Duration.standardMinutes;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationWithCoverageExtension;
|
||||
import google.registry.persistence.transaction.JpaTransactionManager;
|
||||
import google.registry.testing.FakeClock;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
/** Unit tests for {@link BsaLabelUtils}. */
|
||||
public class BsaLabelUtilsTest {
|
||||
|
||||
protected FakeClock fakeClock = new FakeClock(DateTime.now(UTC));
|
||||
|
||||
@RegisterExtension
|
||||
final JpaIntegrationWithCoverageExtension jpa =
|
||||
new JpaTestExtensions.Builder().withClock(fakeClock).buildIntegrationWithCoverageExtension();
|
||||
|
||||
@Test
|
||||
void isLabelBlocked_yes() {
|
||||
persistBsaLabel("abc", fakeClock.nowUtc());
|
||||
assertThat(isLabelBlocked("abc")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isLabelBlocked_no() {
|
||||
assertThat(isLabelBlocked("abc")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isLabelBlocked_isCacheUsed_withReplica() throws Throwable {
|
||||
JpaTransactionManager primaryTmSave = tm();
|
||||
JpaTransactionManager replicaTmSave = replicaTm();
|
||||
|
||||
JpaTransactionManager primaryTm = mock(JpaTransactionManager.class);
|
||||
JpaTransactionManager replicaTm = mock(JpaTransactionManager.class);
|
||||
setJpaTm(() -> primaryTm);
|
||||
setReplicaJpaTm(() -> replicaTm);
|
||||
when(replicaTm.loadByKey(any())).thenReturn(new BsaLabel("abc", fakeClock.nowUtc()));
|
||||
try {
|
||||
assertThat(isLabelBlocked("abc")).isTrue();
|
||||
assertThat(isLabelBlocked("abc")).isTrue();
|
||||
verify(replicaTm, times(1)).loadByKey(any());
|
||||
verify(primaryTm, never()).loadByKey(any());
|
||||
} catch (Throwable e) {
|
||||
setJpaTm(() -> primaryTmSave);
|
||||
setReplicaJpaTm(() -> replicaTmSave);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void isLabelBlocked_isCacheUsed_withOneMinuteExpiry() throws Throwable {
|
||||
JpaTransactionManager replicaTmSave = replicaTm();
|
||||
JpaTransactionManager replicaTm = mock(JpaTransactionManager.class);
|
||||
setReplicaJpaTm(() -> replicaTm);
|
||||
when(replicaTm.loadByKey(any())).thenReturn(new BsaLabel("abc", fakeClock.nowUtc()));
|
||||
try {
|
||||
assertThat(isLabelBlocked("abc")).isTrue();
|
||||
/**
|
||||
* If test fails, check and fix cache expiry in the config file. Do not increase the duration
|
||||
* on the line below without proper discussion.
|
||||
*/
|
||||
fakeClock.advanceBy(standardMinutes(1).plus(millis(1)));
|
||||
assertThat(isLabelBlocked("abc")).isTrue();
|
||||
verify(replicaTm, times(2)).loadByKey(any());
|
||||
} catch (Throwable e) {
|
||||
setReplicaJpaTm(() -> replicaTmSave);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.io.BaseEncoding.base16;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth8.assertThat;
|
||||
import static google.registry.bsa.persistence.BsaLabelTestingUtils.persistBsaLabel;
|
||||
import static google.registry.flows.FlowTestCase.UserPrivileges.SUPERUSER;
|
||||
import static google.registry.model.billing.BillingBase.Flag.ANCHOR_TENANT;
|
||||
import static google.registry.model.billing.BillingBase.Flag.RESERVED;
|
||||
@@ -30,6 +31,7 @@ import static google.registry.model.domain.token.AllocationToken.TokenType.BULK_
|
||||
import static google.registry.model.domain.token.AllocationToken.TokenType.DEFAULT_PROMO;
|
||||
import static google.registry.model.domain.token.AllocationToken.TokenType.SINGLE_USE;
|
||||
import static google.registry.model.domain.token.AllocationToken.TokenType.UNLIMITED_USE;
|
||||
import static google.registry.model.eppcommon.EppXmlTransformer.marshal;
|
||||
import static google.registry.model.eppcommon.StatusValue.PENDING_DELETE;
|
||||
import static google.registry.model.eppcommon.StatusValue.SERVER_HOLD;
|
||||
import static google.registry.model.tld.Tld.TldState.GENERAL_AVAILABILITY;
|
||||
@@ -96,6 +98,7 @@ import google.registry.flows.domain.DomainFlowUtils.ClaimsPeriodEndedException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.CurrencyUnitMismatchException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.CurrencyValueScaleException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.DashesInThirdAndFourthException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.DomainLabelBlockedByBsaException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.DomainLabelTooLongException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.DomainNameExistsAsTldException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.DomainReservedException;
|
||||
@@ -165,6 +168,9 @@ import google.registry.model.domain.secdns.DomainDsData;
|
||||
import google.registry.model.domain.token.AllocationToken;
|
||||
import google.registry.model.domain.token.AllocationToken.RegistrationBehavior;
|
||||
import google.registry.model.domain.token.AllocationToken.TokenStatus;
|
||||
import google.registry.model.eppcommon.Trid;
|
||||
import google.registry.model.eppoutput.EppOutput;
|
||||
import google.registry.model.eppoutput.EppResponse;
|
||||
import google.registry.model.poll.PendingActionNotificationResponse.DomainPendingActionNotificationResponse;
|
||||
import google.registry.model.poll.PollMessage;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
@@ -183,7 +189,9 @@ import google.registry.tmch.LordnTaskUtils.LordnPhase;
|
||||
import google.registry.tmch.SmdrlCsvParser;
|
||||
import google.registry.tmch.TmchData;
|
||||
import google.registry.tmch.TmchTestData;
|
||||
import google.registry.xml.ValidationMode;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Nullable;
|
||||
@@ -2562,6 +2570,53 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_bsaLabelMatch_notEnrolled() throws Exception {
|
||||
persistResource(Tld.get("tld").asBuilder().setBsaEnrollStartTime(Optional.empty()).build());
|
||||
persistBsaLabel("example", clock.nowUtc());
|
||||
persistContactsAndHosts();
|
||||
doSuccessfulTest();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_bsaLabelMatch_notEnrolledYet() throws Exception {
|
||||
persistResource(
|
||||
Tld.get("tld")
|
||||
.asBuilder()
|
||||
.setBsaEnrollStartTime(Optional.of(clock.nowUtc().plusSeconds(1)))
|
||||
.build());
|
||||
persistBsaLabel("example", clock.nowUtc());
|
||||
persistContactsAndHosts();
|
||||
doSuccessfulTest();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_blockedByBsa() throws Exception {
|
||||
persistResource(
|
||||
Tld.get("tld")
|
||||
.asBuilder()
|
||||
.setBsaEnrollStartTime(Optional.of(clock.nowUtc().minusSeconds(1)))
|
||||
.build());
|
||||
persistBsaLabel("example", clock.nowUtc());
|
||||
persistContactsAndHosts();
|
||||
EppException thrown = assertThrows(DomainLabelBlockedByBsaException.class, this::runFlow);
|
||||
assertAboutEppExceptions()
|
||||
.that(thrown)
|
||||
.marshalsToXml()
|
||||
.and()
|
||||
.hasMessage("Domain label is blocked by the Brand Safety Alliance");
|
||||
byte[] responseXmlBytes =
|
||||
marshal(
|
||||
EppOutput.create(
|
||||
new EppResponse.Builder()
|
||||
.setTrid(Trid.create(null, "server-trid"))
|
||||
.setResult(thrown.getResult())
|
||||
.build()),
|
||||
ValidationMode.STRICT);
|
||||
assertThat(new String(responseXmlBytes, StandardCharsets.UTF_8))
|
||||
.isEqualTo(loadFile("domain_create_blocked_by_bsa.xml"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_uppercase() {
|
||||
doFailingDomainNameTest("Example.tld", BadDomainNameCharacterException.class);
|
||||
|
||||
@@ -201,6 +201,23 @@ class RegistrarTest extends EntityTestCase {
|
||||
() -> new Registrar.Builder().setRegistrarId("abcdefghijklmnopq"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_duplicateIanaId() {
|
||||
persistResource(
|
||||
registrar.asBuilder().setRegistrarId("registrar1").setIanaIdentifier(10L).build());
|
||||
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
registrar.asBuilder().setRegistrarId("registrar2").setIanaIdentifier(10L).build());
|
||||
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.contains(
|
||||
"Rejected attempt to create a registrar with ianaId that's already in the system");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetCertificateHash_alsoSetsHash() {
|
||||
registrar = registrar.asBuilder().setClientCertificate(null, fakeClock.nowUtc()).build();
|
||||
|
||||
@@ -17,6 +17,7 @@ package google.registry.schema.integration;
|
||||
import static com.google.common.truth.Truth.assert_;
|
||||
|
||||
import google.registry.bsa.persistence.BsaDomainInUseTest;
|
||||
import google.registry.bsa.persistence.BsaDomainRefreshTest;
|
||||
import google.registry.bsa.persistence.BsaDownloadTest;
|
||||
import google.registry.bsa.persistence.BsaLabelTest;
|
||||
import google.registry.model.billing.BillingBaseTest;
|
||||
@@ -86,6 +87,7 @@ import org.junit.runner.RunWith;
|
||||
AllocationTokenTest.class,
|
||||
BillingBaseTest.class,
|
||||
BsaDomainInUseTest.class,
|
||||
BsaDomainRefreshTest.class,
|
||||
BsaDownloadTest.class,
|
||||
BsaLabelTest.class,
|
||||
BulkPricingPackageTest.class,
|
||||
|
||||
@@ -778,7 +778,7 @@ public final class DatabaseHelper {
|
||||
|
||||
/** Persists and returns a {@link Registrar} with the specified registrarId. */
|
||||
public static Registrar persistNewRegistrar(String registrarId) {
|
||||
return persistNewRegistrar(registrarId, registrarId + " name", Registrar.Type.REAL, 100L);
|
||||
return persistNewRegistrar(registrarId, registrarId + " name", Registrar.Type.REAL, 8L);
|
||||
}
|
||||
|
||||
/** Persists and returns a list of {@link Registrar}s with the specified registrarIds. */
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
package google.registry.tools;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth8.assertThat;
|
||||
import static google.registry.model.EntityYamlUtils.createObjectMapper;
|
||||
import static google.registry.model.domain.token.AllocationToken.TokenType.DEFAULT_PROMO;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.testing.DatabaseHelper.createTld;
|
||||
import static google.registry.testing.DatabaseHelper.persistPremiumList;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
@@ -46,6 +46,7 @@ import google.registry.model.tld.Tld.TldNotFoundException;
|
||||
import google.registry.model.tld.label.PremiumList;
|
||||
import google.registry.model.tld.label.PremiumListDao;
|
||||
import java.io.File;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Logger;
|
||||
import org.joda.money.Money;
|
||||
import org.joda.time.DateTime;
|
||||
@@ -101,20 +102,19 @@ public class ConfigureTldCommandTest extends CommandTestCase<ConfigureTldCommand
|
||||
assertThat(updatedTld.getCreateBillingCost()).isEqualTo(Money.of(USD, 25));
|
||||
testTldConfiguredSuccessfully(updatedTld, "tld.yaml");
|
||||
assertThat(updatedTld.getBreakglassMode()).isFalse();
|
||||
assertThat(tld.getBsaEnrollStartTime()).isNull();
|
||||
assertThat(tld.getBsaEnrollStartTime()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_updateTld_bsaTimeUnaffected() throws Exception {
|
||||
void testSuccess_updateTld_existingBsaTimeCarriedOver() throws Exception {
|
||||
Tld tld = createTld("tld");
|
||||
DateTime bsaStartTime = DateTime.now(DateTimeZone.UTC);
|
||||
tm().transact(() -> tm().put(tld.asBuilder().setBsaEnrollStartTime(bsaStartTime).build()));
|
||||
persistResource(tld.asBuilder().setBsaEnrollStartTime(Optional.of(bsaStartTime)).build());
|
||||
File tldFile = tmpDir.resolve("tld.yaml").toFile();
|
||||
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "tld.yaml"));
|
||||
runCommandForced("--input=" + tldFile);
|
||||
// TODO(11/30/2023): uncomment below two lines
|
||||
// Tld updatedTld = Tld.get("tld");
|
||||
// assertThat(tld.getBsaEnrollStartTime()).isEqualTo(bsaStartTime);
|
||||
Tld updatedTld = Tld.get("tld");
|
||||
assertThat(updatedTld.getBsaEnrollStartTime()).hasValue(bsaStartTime);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -593,7 +593,7 @@ public class ConfigureTldCommandTest extends CommandTestCase<ConfigureTldCommand
|
||||
void testSuccess_dryRunOnCreate_noChanges() throws Exception {
|
||||
File tldFile = tmpDir.resolve("tld.yaml").toFile();
|
||||
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "tld.yaml"));
|
||||
runCommandForced("--input=" + tldFile, "--dryrun");
|
||||
runCommandForced("--input=" + tldFile, "--dry_run");
|
||||
assertThrows(TldNotFoundException.class, () -> Tld.get("tld"));
|
||||
}
|
||||
|
||||
|
||||
@@ -63,10 +63,19 @@ class UpdatePremiumListCommandTest<C extends UpdatePremiumListCommand>
|
||||
UpdatePremiumListCommand command = new UpdatePremiumListCommand();
|
||||
command.inputFile = Paths.get(tmpFile.getPath());
|
||||
command.name = TLD_TEST;
|
||||
command.prompt();
|
||||
assertThat(command.prompt()).contains("Update premium list for prime?");
|
||||
}
|
||||
|
||||
@Test
|
||||
void commandPrompt_successStageNoChange() throws Exception {
|
||||
File tmpFile = tmpDir.resolve(String.format("%s.txt", TLD_TEST)).toFile();
|
||||
UpdatePremiumListCommand command = new UpdatePremiumListCommand();
|
||||
command.inputFile = Paths.get(tmpFile.getPath());
|
||||
command.name = TLD_TEST;
|
||||
assertThat(command.prompt())
|
||||
.contains("This update contains no changes to the premium list for prime.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void commandRun_successUpdateList() throws Exception {
|
||||
File tmpFile = tmpDir.resolve(String.format("%s.txt", TLD_TEST)).toFile();
|
||||
@@ -83,6 +92,18 @@ class UpdatePremiumListCommandTest<C extends UpdatePremiumListCommand>
|
||||
.containsExactly(PremiumEntry.create(0L, new BigDecimal("9999.00"), "eth"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void commandRun_successNoChange() throws Exception {
|
||||
File tmpFile = tmpDir.resolve(String.format("%s.txt", TLD_TEST)).toFile();
|
||||
|
||||
UpdatePremiumListCommand command = new UpdatePremiumListCommand();
|
||||
command.inputFile = Paths.get(tmpFile.getPath());
|
||||
runCommandForced("--name=" + TLD_TEST, "--input=" + command.inputFile);
|
||||
|
||||
assertThat(PremiumListDao.loadAllPremiumEntries(TLD_TEST))
|
||||
.containsExactly(PremiumEntry.create(0L, new BigDecimal("9090.00"), "doge"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void commandRun_successUpdateList_whenExistingListIsEmpty() throws Exception {
|
||||
File existingPremiumFile = tmpDir.resolve(TLD_TEST + ".txt").toFile();
|
||||
@@ -169,4 +190,19 @@ class UpdatePremiumListCommandTest<C extends UpdatePremiumListCommand>
|
||||
.hasMessageThat()
|
||||
.isEqualTo("Could not update premium list random3 because it doesn't exist");
|
||||
}
|
||||
|
||||
@Test
|
||||
void commandDryRun_noChangesMade() throws Exception {
|
||||
File tmpFile = tmpDir.resolve(String.format("%s.txt", TLD_TEST)).toFile();
|
||||
String newPremiumListData = "eth,USD 9999";
|
||||
Files.asCharSink(tmpFile, UTF_8).write(newPremiumListData);
|
||||
|
||||
UpdatePremiumListCommand command = new UpdatePremiumListCommand();
|
||||
command.inputFile = Paths.get(tmpFile.getPath());
|
||||
runCommandForced("--name=" + TLD_TEST, "--input=" + command.inputFile, "--dry_run");
|
||||
|
||||
assertThat(PremiumListDao.loadAllPremiumEntries(TLD_TEST))
|
||||
.comparingElementsUsing(immutableObjectCorrespondence("revisionId"))
|
||||
.containsExactly(PremiumEntry.create(0L, new BigDecimal("9090.00"), "doge"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<epp xmlns:domain="urn:ietf:params:xml:ns:domain-1.0" xmlns:contact="urn:ietf:params:xml:ns:contact-1.0" xmlns:fee="urn:ietf:params:xml:ns:fee-0.6" xmlns="urn:ietf:params:xml:ns:epp-1.0" xmlns:rgp="urn:ietf:params:xml:ns:rgp-1.0" xmlns:bulkToken="urn:google:params:xml:ns:bulkToken-1.0" xmlns:fee11="urn:ietf:params:xml:ns:fee-0.11" xmlns:fee12="urn:ietf:params:xml:ns:fee-0.12" xmlns:launch="urn:ietf:params:xml:ns:launch-1.0" xmlns:secDNS="urn:ietf:params:xml:ns:secDNS-1.1" xmlns:host="urn:ietf:params:xml:ns:host-1.0">
|
||||
<response>
|
||||
<result code="2306">
|
||||
<msg>Domain label is blocked by the Brand Safety Alliance</msg>
|
||||
</result>
|
||||
<trID>
|
||||
<svTRID>server-trid</svTRID>
|
||||
</trID>
|
||||
</response>
|
||||
</epp>
|
||||
@@ -93,6 +93,14 @@
|
||||
primary key (label, tld)
|
||||
);
|
||||
|
||||
create table "BsaDomainRefresh" (
|
||||
job_id bigserial not null,
|
||||
creation_time timestamptz not null,
|
||||
stage text not null,
|
||||
update_timestamp timestamptz,
|
||||
primary key (job_id)
|
||||
);
|
||||
|
||||
create table "BsaDownload" (
|
||||
job_id bigserial not null,
|
||||
block_list_checksums text not null,
|
||||
@@ -728,6 +736,7 @@
|
||||
auto_renew_grace_period_length interval not null,
|
||||
automatic_transfer_length interval not null,
|
||||
breakglass_mode boolean not null,
|
||||
bsa_enroll_start_time timestamptz,
|
||||
claims_period_end timestamptz not null,
|
||||
create_billing_cost_amount numeric(19, 2),
|
||||
create_billing_cost_currency text,
|
||||
|
||||
@@ -384,6 +384,7 @@ An EPP flow that creates a new domain resource.
|
||||
* The requested fees cannot be provided in the requested currency.
|
||||
* Non-IDN domain names cannot contain hyphens in the third or fourth
|
||||
position.
|
||||
* Domain label is blocked by the Brand Safety Alliance.
|
||||
* Domain labels cannot be longer than 63 characters.
|
||||
* More than one contact for a given role is not allowed.
|
||||
* No part of a domain name can be empty.
|
||||
|
||||
@@ -46,5 +46,5 @@ spec:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: proxy-deployment
|
||||
maxReplicas: 10
|
||||
minReplicas: 1
|
||||
maxReplicas: 50
|
||||
minReplicas: 10
|
||||
|
||||
Reference in New Issue
Block a user