1
0
mirror of https://github.com/google/nomulus synced 2026-02-11 07:11:40 +00:00

Compare commits

...

26 Commits

Author SHA1 Message Date
Lai Jiang
42b508427b Bypass SCRYPT hashing in tests (#2262)
SCRYPT is much computationally heavier than SHA265 (by design), which
resulted in test run time doubling due to most tests initializing canned
data that uses hashing.

Since out tests are not verifying the correctness of a specific hashing
algorithm anyway, this PR makes it so that simple concatenation is used
in tests.

Also moved RegistryEnvironment to the util subproject so it can be called by
PasswordUtils, which makes sense as it is a utility class.
2023-12-21 16:17:37 -05:00
sarahcaseybot
20b5b43501 Add type conversion to TimedTransitionProperty<Money> deserializer to handle JPY currency (#2258)
* Add BigInt conversion to TimedTransitionProperty<Money> deserializer to handle JPY currency

* Remove unnecessary lines in test

* Add eap schedule check

* Don't use raw LinkedHashMap type

* add timezone
2023-12-21 12:59:54 -05:00
Lai Jiang
08285f5de7 Greatly increase the upper limit of proxy instances in production (#2259)
From our investigation, the Monday night WHOIS storm does not cause any
strain to the backend system. The backend latency metrics are all well within
the limits. The latency measured from the proxy matches observed latency
by the prober, and we see that the "used" CPU is 1.5x of "requested" CPU
during the time when the latency is above the threshold.

Making this change hopefully removes the proxy as the bottleneck and
ameliorate the pages.
2023-12-20 15:37:29 -05:00
Pavlo Tkach
fb4c5b457d Prevent reusing ianaId for real registrars (#2257) 2023-12-20 15:20:04 -05:00
Pavlo Tkach
781c212275 Add IcannHttpReporter failed response logging (#2252) 2023-12-18 11:03:33 -05:00
Weimin Yu
c73f7a6bd3 Add the BsaDomainRefresh entity (#2250)
Add the BsaDomainRefresh class which tracks the refresh actions.

The refresh actions checks for changes in the set of registered and
reserved domains, which are called unblockables to BSA.
2023-12-13 16:08:37 -05:00
Lai Jiang
8d793b2349 Do not double-enqueue NordnVerifyAction (#2253)
Currently, a verify action is enqueued every time the upload method
succeeds. Because the upload job is wrapped in a transaction, the
same task will be enqueued again if the transaction retries.

We cannot move the upload method outside the transaction because the
read-upload-write logic needs to be atomic, and the upload part itself
is idempotent (therefore retri-able). We can, however, move the
enqueuing part outside the transaction as we only need to enqueue the
verify task once the transaction succeeds. This should fix the issue
where multiple verify jobs try to hit the same marksdb endpoints,
resulting in 429 (Too Many Requests) errors.
2023-12-12 16:00:35 -05:00
Weimin Yu
55d5f8c6f8 Forbid domain creation with label blocked by BSA (#2236)
* Forbid domain creation with label blocked by BSA

Add a BSA label check in the DomainCreation flow.
2023-12-11 22:14:12 -05:00
Pavlo Tkach
9006312253 Create reusable dialog / bottom sheet component (#2237) 2023-12-08 17:52:57 -05:00
gbrodman
e5e2370923 Debouncedly use a search term in console domain list (#2242) 2023-12-08 15:37:30 -05:00
sarahcaseybot
b3b0efd47e Add a dryrun tag to UpdatePremiumListCommand and early exit command if no new changes to the list (#2246)
* Add a dryrun tag to UpdatePremiumListCommand and early exit command if no new changes to the list

* Change prompt string when no change to list to reflect that there is no actual prompted user input

* Add camelCase and correct flag name
2023-12-08 14:35:05 -05:00
Lai Jiang
e82cbe60a9 Do not log nested transactions in production (#2251)
This might be the cause of the SQL performance degradation that we are
observing during the recent launch. The change went in a month ago but
there hasn't been enough increase in mutating traffic to make it
problematic until the launch.

Note that presubmits should run faster too with this chance, which
serves as an evidence that excessive logging is the culprit.
2023-12-07 19:02:16 -05:00
Weimin Yu
923bc13e3a Start using Tld's bsaEnrollStartTime field (#2239)
* Start using Tld's bsaEnrollStartTime field

    Longer-term change is tracked in b/309175410
2023-12-06 17:11:36 -05:00
Lai Jiang
4893ea307b Check for null error stream (#2249) 2023-12-06 13:30:37 -05:00
Pavlo Tkach
01f868cefc Increase number of service to 5 in cloudbuild-deploy (#2248) 2023-12-06 13:21:17 -05:00
Weimin Yu
1b0919eaff Add the BsaDomainRefresh table (#2247)
Add the BsaDomainRefresh table which tracks the refresh actions.

The refresh actions checks for changes in the set of registered and
reserved domains, which are called unblockables to BSA.
2023-12-06 11:55:42 -05:00
Lai Jiang
92b23bac16 Use the error stream when HTTP response code is non-200 (#2245) 2023-12-06 10:42:19 -05:00
gbrodman
cc9b3f5965 Filter in SQL when updating/deleting alloc tokens (#2244)
This doesn't fix any issues with dead/livelocks when deleting or
updating allocation tokens, but it at least will significantly reduce
the time to load the tokens that we'll want to update/delete.
2023-12-04 19:24:17 -05:00
gbrodman
dd86c56ddc Return the correct renewal fee for anchor tenants in domain checks (#2238)
The code as previously written assumed that creation fees would be the
same as renewal fees -- this is not the case for anchor tenants, where
the renewal fee is always the standard cost for the TLD (instead of any
premium cost). This was already handled properly in the actual billing
implementation, but we didn't tell the user the right renewal cost in
domain checks.

This also removes some warning logs related to nested transactions
2023-12-01 15:37:05 -05:00
Pavlo Tkach
08551f7bc7 Enable static ip for bsa service production (#2240) 2023-12-01 14:25:38 -05:00
Lai Jiang
e7171a326b Use reTransact when loading caches (#2234)
Similar to #2179, but adds a few calls missed in that PR.
2023-11-30 15:13:36 -05:00
gbrodman
c3eae7b76f Add an optional search term for ConsoleDomainListAction (#2225)
It's a case-insensitive query and it can appear anywhere (including
TLDs)
2023-11-30 11:42:50 -05:00
Pavlo Tkach
2687181045 Update console file naming to be camelCase like (#2235) 2023-11-30 11:42:36 -05:00
gbrodman
68750569db Pretty-print reserved list updates in the CLI (#2226)
We shouldn't have to parse through every single entry to see what
changed

Note: we don't do this for premium lists because those can be HUGE and
we don't want/need to load and display every entry. This was an explicit
choice made in https://github.com/google/nomulus/pull/1482
2023-11-30 11:32:12 -05:00
Lai Jiang
028e5cc958 Make read-only transactions more performant (#2233)
Since the replica SQL instance is read-only, any transaction performed
on it should be explicitly read-only, which would allow PostgreSQL to
optimize away (some) use of predicate locks.

Also changed the EPP cache to read from the replica. The foreign key
cache already behaves this way.

See: https://www.postgresql.org/docs/current/transaction-iso.html
2023-11-29 15:55:50 -05:00
Weimin Yu
853e571d01 Add more BSA configs (#2230)
* Add more BSA configs

Added urls for reporting order and domains to BSA.

Also added operational configs.
2023-11-28 16:40:36 -05:00
141 changed files with 5613 additions and 4574 deletions

View File

@@ -36,28 +36,27 @@ import { RegistrarGuard } from './registrar/registrar.guard';
import SecurityComponent from './settings/security/security.component';
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
import { EmptyRegistrar } from './registrar/emptyRegistrar.component';
import { RegistrarSelectorComponent } from './registrar/registrar-selector.component';
import { RegistrarSelectorComponent } from './registrar/registrarSelector.component';
import { GlobalLoaderService } from './shared/services/globalLoader.service';
import { ContactWidgetComponent } from './home/widgets/contact-widget.component';
import { PromotionsWidgetComponent } from './home/widgets/promotions-widget.component';
import { TldsWidgetComponent } from './home/widgets/tlds-widget.component';
import { ResourcesWidgetComponent } from './home/widgets/resources-widget.component';
import { EppWidgetComponent } from './home/widgets/epp-widget.component';
import { BillingWidgetComponent } from './home/widgets/billing-widget.component';
import { DomainsWidgetComponent } from './home/widgets/domains-widget.component';
import { SettingsWidgetComponent } from './home/widgets/settings-widget.component';
import { ContactWidgetComponent } from './home/widgets/contactWidget.component';
import { PromotionsWidgetComponent } from './home/widgets/promotionsWidget.component';
import { TldsWidgetComponent } from './home/widgets/tldsWidget.component';
import { ResourcesWidgetComponent } from './home/widgets/resourcesWidget.component';
import { EppWidgetComponent } from './home/widgets/eppWidget.component';
import { BillingWidgetComponent } from './home/widgets/billingWidget.component';
import { DomainsWidgetComponent } from './home/widgets/domainsWidget.component';
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,

View File

@@ -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">

View File

@@ -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) {

View File

@@ -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) => {

View File

@@ -17,7 +17,7 @@ import { RegistrarService } from 'src/app/registrar/registrar.service';
@Component({
selector: '[app-billing-widget]',
templateUrl: './billing-widget.component.html',
templateUrl: './billingWidget.component.html',
})
export class BillingWidgetComponent {
constructor(public registrarService: RegistrarService) {}

View File

@@ -17,7 +17,7 @@ import { UserDataService } from 'src/app/shared/services/userData.service';
@Component({
selector: '[app-contact-widget]',
templateUrl: './contact-widget.component.html',
templateUrl: './contactWidget.component.html',
})
export class ContactWidgetComponent {
constructor(public userDataService: UserDataService) {}

View File

@@ -18,7 +18,7 @@ import { DomainListComponent } from 'src/app/domains/domainList.component';
@Component({
selector: '[app-domains-widget]',
templateUrl: './domains-widget.component.html',
templateUrl: './domainsWidget.component.html',
})
export class DomainsWidgetComponent {
constructor(private router: Router) {}

View File

@@ -16,7 +16,7 @@ import { Component } from '@angular/core';
@Component({
selector: '[app-epp-widget]',
templateUrl: './epp-widget.component.html',
templateUrl: './eppWidget.component.html',
})
export class EppWidgetComponent {
constructor() {}

View File

@@ -16,7 +16,7 @@ import { Component } from '@angular/core';
@Component({
selector: '[app-promotions-widget]',
templateUrl: './promotions-widget.component.html',
templateUrl: './promotionsWidget.component.html',
})
export class PromotionsWidgetComponent {
constructor() {}

View File

@@ -17,7 +17,7 @@ import { UserDataService } from 'src/app/shared/services/userData.service';
@Component({
selector: '[app-resources-widget]',
templateUrl: './resources-widget.component.html',
templateUrl: './resourcesWidget.component.html',
})
export class ResourcesWidgetComponent {
constructor(public userDataService: UserDataService) {}

View File

@@ -21,7 +21,7 @@ import { SettingsComponent } from 'src/app/settings/settings.component';
@Component({
selector: '[app-settings-widget]',
templateUrl: './settings-widget.component.html',
templateUrl: './settingsWidget.component.html',
})
export class SettingsWidgetComponent {
constructor(private router: Router) {}

View File

@@ -16,7 +16,7 @@ import { Component } from '@angular/core';
@Component({
selector: '[app-tlds-widget]',
templateUrl: './tlds-widget.component.html',
templateUrl: './tldsWidget.component.html',
})
export class TldsWidgetComponent {
constructor() {}

View File

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

View File

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

View File

@@ -14,7 +14,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RegistrarSelectorComponent } from './registrar-selector.component';
import { RegistrarSelectorComponent } from './registrarSelector.component';
describe('RegistrarSelectorComponent', () => {
let component: RegistrarSelectorComponent;

View File

@@ -21,8 +21,8 @@ const MOBILE_LAYOUT_BREAKPOINT = '(max-width: 599px)';
@Component({
selector: 'app-registrar-selector',
templateUrl: './registrar-selector.component.html',
styleUrls: ['./registrar-selector.component.scss'],
templateUrl: './registrarSelector.component.html',
styleUrls: ['./registrarSelector.component.scss'],
})
export class RegistrarSelectorComponent implements OnInit {
protected isMobile: boolean = false;

View File

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

View File

@@ -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) {

View File

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

View File

@@ -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: 'contact-details.component.html',
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 }
);
}
}

View File

@@ -45,7 +45,7 @@ export class ContactService {
return this.backend
.getContacts(this.registrarService.activeRegistrarId)
.pipe(
tap((contacts) => {
tap((contacts = []) => {
this.contacts = contacts;
})
);

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
@use "@angular/material" as mat;
@import "app/registrar/registrar-selector.component.scss";
@import "app/registrar/registrarSelector.component.scss";
html,
body {

View File

@@ -17,15 +17,14 @@ package google.registry.batch;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.batch.BatchModule.PARAM_DRY_RUN;
import static google.registry.config.RegistryEnvironment.PRODUCTION;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.POST;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.RegistryEnvironment.PRODUCTION;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryEnvironment;
import google.registry.flows.poll.PollFlowUtils;
import google.registry.model.EppResource;
import google.registry.model.EppResourceUtils;
@@ -40,6 +39,7 @@ import google.registry.request.Action;
import google.registry.request.Parameter;
import google.registry.request.auth.Auth;
import google.registry.util.Clock;
import google.registry.util.RegistryEnvironment;
import javax.inject.Inject;
/**

View File

@@ -18,13 +18,13 @@ import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.batch.BatchModule.PARAM_DRY_RUN;
import static google.registry.config.RegistryEnvironment.PRODUCTION;
import static google.registry.dns.DnsUtils.requestDomainDnsRefresh;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_DELETE;
import static google.registry.model.tld.Tlds.getTldsOfType;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.POST;
import static google.registry.request.RequestParameters.PARAM_TLDS;
import static google.registry.util.RegistryEnvironment.PRODUCTION;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
@@ -32,7 +32,6 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryEnvironment;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.EppResourceUtils;
import google.registry.model.domain.Domain;
@@ -41,6 +40,7 @@ import google.registry.model.tld.Tld.TldType;
import google.registry.request.Action;
import google.registry.request.Parameter;
import google.registry.request.auth.Auth;
import google.registry.util.RegistryEnvironment;
import java.util.concurrent.atomic.AtomicInteger;
import javax.inject.Inject;
import org.hibernate.CacheMode;

View File

@@ -31,7 +31,6 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import google.registry.beam.billing.ExpandBillingRecurrencesPipeline;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryEnvironment;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingRecurrence;
import google.registry.model.common.Cursor;
@@ -40,6 +39,7 @@ import google.registry.request.Parameter;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.util.Clock;
import google.registry.util.RegistryEnvironment;
import java.io.IOException;
import java.util.Optional;
import javax.inject.Inject;

View File

@@ -27,12 +27,12 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryEnvironment;
import google.registry.request.Action;
import google.registry.request.Parameter;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.util.Clock;
import google.registry.util.RegistryEnvironment;
import javax.inject.Inject;
/**

View File

@@ -28,7 +28,6 @@ import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import google.registry.beam.wipeout.WipeOutContactHistoryPiiPipeline;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryEnvironment;
import google.registry.model.contact.ContactHistory;
import google.registry.request.Action;
import google.registry.request.Action.Service;
@@ -36,6 +35,7 @@ import google.registry.request.Parameter;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.util.Clock;
import google.registry.util.RegistryEnvironment;
import java.io.IOException;
import java.util.Optional;
import javax.inject.Inject;

View File

@@ -14,9 +14,9 @@
package google.registry.beam.common;
import google.registry.config.RegistryEnvironment;
import google.registry.persistence.PersistenceModule.JpaTransactionManagerType;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import google.registry.util.RegistryEnvironment;
import java.util.Objects;
import javax.annotation.Nullable;
import org.apache.beam.sdk.extensions.gcp.options.GcpOptions;

View File

@@ -19,10 +19,10 @@ import static google.registry.beam.common.RegistryPipelineOptions.toRegistryPipe
import com.google.auto.service.AutoService;
import com.google.common.flogger.FluentLogger;
import dagger.Lazy;
import google.registry.config.RegistryEnvironment;
import google.registry.config.SystemPropertySetter;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.persistence.transaction.TransactionManagerFactory;
import google.registry.util.RegistryEnvironment;
import google.registry.util.SystemPropertySetter;
import org.apache.beam.sdk.harness.JvmInitializer;
import org.apache.beam.sdk.options.PipelineOptions;

View File

@@ -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()

View File

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

View File

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

View File

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

View File

@@ -36,6 +36,7 @@ import dagger.Provides;
import google.registry.dns.ReadDnsRefreshRequestsAction;
import google.registry.model.common.DnsRefreshRequest;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.util.RegistryEnvironment;
import google.registry.util.YamlUtils;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
@@ -1397,6 +1398,47 @@ public final class RegistryConfig {
return config.bulkPricingPackageMonitoring.bulkPricingPackageDomainLimitUpgradeEmailBody;
}
@Provides
@Config("bsaGcsBucket")
public static String provideBsaGcsBucket(@Config("projectId") String projectId) {
return projectId + "-bsa";
}
@Provides
@Config("bsaChecksumAlgorithm")
public static String provideBsaChecksumAlgorithm(RegistryConfigSettings config) {
return config.bsa.bsaChecksumAlgorithm;
}
@Provides
@Config("bsaLockLeaseExpiry")
public static Duration provideBsaLockLeaseExpiry(RegistryConfigSettings config) {
return Duration.standardMinutes(config.bsa.bsaLockLeaseExpiryMinutes);
}
/** Returns the desired interval between successive BSA downloads. */
@Provides
@Config("bsaDownloadInterval")
public static Duration provideBsaDownloadInterval(RegistryConfigSettings config) {
return Duration.standardMinutes(config.bsa.bsaDownloadIntervalMinutes);
}
/**
* Returns the maximum period when a BSA download can be skipped due to the checksum-based
* equality check with the previous download.
*/
@Provides
@Config("bsaMaxNopInterval")
public static Duration provideBsaMaxNopInterval(RegistryConfigSettings config) {
return Duration.standardHours(config.bsa.bsaMaxNopIntervalHours);
}
@Provides
@Config("bsaLabelTxnBatchSize")
public static int provideBsaLabelTxnBatchSize(RegistryConfigSettings config) {
return config.bsa.bsaLabelTxnBatchSize;
}
@Provides
@Config("bsaAuthUrl")
public static String provideBsaAuthUrl(RegistryConfigSettings config) {
@@ -1415,6 +1457,27 @@ public final class RegistryConfig {
return ImmutableMap.copyOf(config.bsa.dataUrls);
}
/** Provides the BSA Http endpoint for reporting order processing status. */
@Provides
@Config("bsaOrderStatusUrl")
public static String provideBsaOrderStatusUrls(RegistryConfigSettings config) {
return config.bsa.orderStatusUrl;
}
/** Provides the BSA Http endpoint for reporting new unblockable domains. */
@Provides
@Config("bsaAddUnblockableDomainsUrl")
public static String provideBsaAddUnblockableDomainsUrls(RegistryConfigSettings config) {
return String.format("%s?%s", config.bsa.unblockableDomainsUrl, "action=add");
}
/** Provides the BSA Http endpoint for reporting domains that have become blockable. */
@Provides
@Config("bsaRemoveUnblockableDomainsUrl")
public static String provideBsaRemoveUnblockableDomainsUrls(RegistryConfigSettings config) {
return String.format("%s?%s", config.bsa.unblockableDomainsUrl, "action=remove");
}
private static String formatComments(String text) {
return Splitter.on('\n').omitEmptyStrings().trimResults().splitToList(text).stream()
.map(s -> "# " + s)

View File

@@ -267,8 +267,15 @@ public class RegistryConfigSettings {
/** Configurations for integration with Brand Safety Alliance (BSA) API. */
public static class Bsa {
public String bsaChecksumAlgorithm;
public int bsaLockLeaseExpiryMinutes;
public int bsaDownloadIntervalMinutes;
public int bsaMaxNopIntervalHours;
public int bsaLabelTxnBatchSize;
public String authUrl;
public int authTokenExpirySeconds;
public Map<String, String> dataUrls;
public String orderStatusUrl;
public String unblockableDomainsUrl;
}
}

View File

@@ -617,3 +617,7 @@ bsa:
dataUrls:
"BLOCK": "https://"
"BLOCK_PLUS": "https://"
# Http endpoint for reporting order processing status
orderStatusUrl: "https://"
# Http endpoint for reporting changes in the set of unblockable domains.
unblockableDomainsUrl: "https://"

View File

@@ -14,7 +14,7 @@
package google.registry.dns;
import static google.registry.config.RegistryEnvironment.PRODUCTION;
import static google.registry.util.RegistryEnvironment.PRODUCTION;
import com.google.common.collect.ImmutableSet;
import com.google.monitoring.metrics.DistributionFitter;
@@ -24,7 +24,7 @@ import com.google.monitoring.metrics.FibonacciFitter;
import com.google.monitoring.metrics.IncrementableMetric;
import com.google.monitoring.metrics.LabelDescriptor;
import com.google.monitoring.metrics.MetricRegistryImpl;
import google.registry.config.RegistryEnvironment;
import google.registry.util.RegistryEnvironment;
import javax.inject.Inject;
import org.joda.time.Duration;

View File

@@ -19,6 +19,12 @@
value="production"/>
</system-properties>
<!-- Enable external traffic to go through VPC, required for static ip -->
<vpc-access-connector>
<name>projects/domain-registry/locations/us-central1/connectors/appengine-connector</name>
<egress-setting>all-traffic</egress-setting>
</vpc-access-connector>
<static-files>
<include path="/*.html" expiration="1d"/>
</static-files>

View File

@@ -26,7 +26,6 @@ import com.google.common.net.InetAddresses;
import dagger.Module;
import dagger.Provides;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryEnvironment;
import google.registry.flows.EppException.AuthenticationErrorException;
import google.registry.flows.certs.CertificateChecker;
import google.registry.flows.certs.CertificateChecker.InsecureCertificateException;
@@ -34,6 +33,7 @@ import google.registry.model.registrar.Registrar;
import google.registry.request.Header;
import google.registry.util.CidrAddressBlock;
import google.registry.util.ProxyHttpHeaders;
import google.registry.util.RegistryEnvironment;
import java.net.InetAddress;
import java.security.MessageDigest;
import java.util.Optional;

View File

@@ -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)

View File

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

View File

@@ -18,7 +18,6 @@ import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.flows.domain.DomainFlowUtils.zeroInCurrency;
import static google.registry.flows.domain.token.AllocationTokenFlowUtils.validateTokenForPossiblePremiumName;
import static google.registry.pricing.PricingEngineProxy.getPricesForDomainName;
import static google.registry.util.DomainNameUtils.getTldFromDomainName;
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
import com.google.common.net.InternetDomainName;
@@ -31,11 +30,13 @@ import google.registry.flows.custom.DomainPricingCustomLogic.RenewPriceParameter
import google.registry.flows.custom.DomainPricingCustomLogic.RestorePriceParameters;
import google.registry.flows.custom.DomainPricingCustomLogic.TransferPriceParameters;
import google.registry.flows.custom.DomainPricingCustomLogic.UpdatePriceParameters;
import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
import google.registry.model.billing.BillingRecurrence;
import google.registry.model.domain.fee.BaseFee;
import google.registry.model.domain.fee.BaseFee.FeeType;
import google.registry.model.domain.fee.Fee;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationToken.RegistrationBehavior;
import google.registry.model.domain.token.AllocationToken.TokenBehavior;
import google.registry.model.pricing.PremiumPricingEngine.DomainPrices;
import google.registry.model.tld.Tld;
@@ -132,12 +133,14 @@ public final class DomainPricingLogic {
// recurrence is null if the domain is still available. Billing events are created
// in the process of domain creation.
if (billingRecurrence == null) {
renewCost = getDomainRenewCostWithDiscount(domainPrices, years, allocationToken);
renewCost =
getDomainRenewCostWithDiscount(tld, domainPrices, dateTime, years, allocationToken);
isRenewCostPremiumPrice = domainPrices.isPremium();
} else {
switch (billingRecurrence.getRenewalPriceBehavior()) {
case DEFAULT:
renewCost = getDomainRenewCostWithDiscount(domainPrices, years, allocationToken);
renewCost =
getDomainRenewCostWithDiscount(tld, domainPrices, dateTime, years, allocationToken);
isRenewCostPremiumPrice = domainPrices.isPremium();
break;
// if the renewal price behavior is specified, then the renewal price should be the same
@@ -156,10 +159,7 @@ public final class DomainPricingLogic {
case NONPREMIUM:
renewCost =
getDomainCostWithDiscount(
false,
years,
allocationToken,
Tld.get(getTldFromDomainName(domainName)).getStandardRenewCost(dateTime));
false, years, allocationToken, tld.getStandardRenewCost(dateTime));
isRenewCostPremiumPrice = false;
break;
default:
@@ -257,8 +257,20 @@ public final class DomainPricingLogic {
/** Returns the domain renew cost with allocation-token-related discounts applied. */
private Money getDomainRenewCostWithDiscount(
DomainPrices domainPrices, int years, Optional<AllocationToken> allocationToken)
Tld tld,
DomainPrices domainPrices,
DateTime dateTime,
int years,
Optional<AllocationToken> allocationToken)
throws AllocationTokenInvalidForPremiumNameException {
// Short-circuit if the user sent an anchor-tenant or otherwise NONPREMIUM-renewal token
if (allocationToken.isPresent()) {
AllocationToken token = allocationToken.get();
if (token.getRegistrationBehavior().equals(RegistrationBehavior.ANCHOR_TENANT)
|| token.getRenewalPriceBehavior().equals(RenewalPriceBehavior.NONPREMIUM)) {
return tld.getStandardRenewCost(dateTime).multipliedBy(years);
}
}
return getDomainCostWithDiscount(
domainPrices.isPremium(), years, allocationToken, domainPrices.getRenewCost());
}

View File

@@ -29,12 +29,12 @@ import com.google.common.collect.Iterators;
import com.google.common.flogger.FluentLogger;
import com.google.protobuf.Timestamp;
import google.registry.batch.CloudTasksUtils;
import google.registry.config.RegistryEnvironment;
import google.registry.request.Action;
import google.registry.request.Action.Service;
import google.registry.request.Parameter;
import google.registry.request.auth.Auth;
import google.registry.security.XsrfTokenManager;
import google.registry.util.RegistryEnvironment;
import java.time.Instant;
import java.util.Arrays;
import java.util.Iterator;

View File

@@ -349,7 +349,7 @@ public class EntityYamlUtils {
@Override
public TimedTransitionProperty<Money> deserialize(JsonParser jp, DeserializationContext context)
throws IOException {
SortedMap<String, LinkedHashMap> valueMap = jp.readValueAs(SortedMap.class);
SortedMap<String, LinkedHashMap<String, Object>> valueMap = jp.readValueAs(SortedMap.class);
return TimedTransitionProperty.fromValueMap(
valueMap.keySet().stream()
.collect(
@@ -359,7 +359,7 @@ public class EntityYamlUtils {
key ->
Money.of(
CurrencyUnit.of(valueMap.get(key).get("currency").toString()),
(double) valueMap.get(key).get("amount")))));
new BigDecimal(String.valueOf(valueMap.get(key).get("amount")))))));
}
}

View File

@@ -20,6 +20,7 @@ import static com.google.common.collect.Sets.difference;
import static com.google.common.collect.Sets.union;
import static google.registry.config.RegistryConfig.getEppResourceCachingDuration;
import static google.registry.config.RegistryConfig.getEppResourceMaxCachedEntries;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.CollectionUtils.nullToEmpty;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
@@ -357,13 +358,13 @@ public abstract class EppResource extends UpdateAutoTimestampEntity implements B
@Override
public EppResource load(VKey<? extends EppResource> key) {
return tm().reTransact(() -> tm().loadByKey(key));
return replicaTm().reTransact(() -> replicaTm().loadByKey(key));
}
@Override
public Map<VKey<? extends EppResource>, EppResource> loadAll(
Iterable<? extends VKey<? extends EppResource>> keys) {
return tm().reTransact(() -> tm().loadByKeys(keys));
return replicaTm().reTransact(() -> replicaTm().loadByKeys(keys));
}
};

View File

@@ -165,10 +165,11 @@ public final class ForeignKeyUtils {
* foreign keys should not use this cache.
*
* <p>Note that here the key of the {@link LoadingCache} is of type {@code VKey<? extends
* EppResource>}, but they are not legal {VKey}s to {@link EppResource}s, whose keys are the SQL
* primary keys, i.e. the {@code repoId}s. Instead, their keys are the foreign keys used to query
* the database. We use {@link VKey} here because it is a convenient composite class that contains
* both the resource type and the foreign key, which are needed to for the query and caching.
* EppResource>}, but they are not legal {@link VKey}s to {@link EppResource}s, whose keys are the
* SQL primary keys, i.e. the {@code repoId}s. Instead, their keys are the foreign keys used to
* query the database. We use {@link VKey} here because it is a convenient composite class that
* contains both the resource type and the foreign key, which are needed to for the query and
* caching.
*
* <p>Also note that the value type of this cache is {@link Optional} because the foreign keys in
* question are coming from external commands, and thus don't necessarily represent entities in

View File

@@ -28,7 +28,6 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import google.registry.config.RegistryEnvironment;
import google.registry.model.pricing.StaticPremiumListPricingEngine;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarAddress;
@@ -40,6 +39,7 @@ import google.registry.model.tld.label.PremiumList;
import google.registry.model.tld.label.PremiumListDao;
import google.registry.persistence.VKey;
import google.registry.util.CidrAddressBlock;
import google.registry.util.RegistryEnvironment;
import java.util.Collection;
import java.util.Optional;
import java.util.function.Function;

View File

@@ -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 =

View File

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

View File

@@ -45,7 +45,7 @@ import org.joda.money.Money;
* {@link PremiumList} object in SQL, and caching these entries so that future lookups can be
* quicker.
*/
public class PremiumListDao {
public final class PremiumListDao {
/**
* In-memory cache for premium lists.
@@ -102,7 +102,7 @@ public class PremiumListDao {
/**
* Returns the most recent revision of the PremiumList with the specified name, if it exists.
*
* <p>Note that this does not load <code>PremiumList.labelsToPrices</code>! If you need to check
* <p>Note that this does not load {@code PremiumList.labelsToPrices}! If you need to check
* prices, use {@link #getPremiumPrice}.
*/
public static Optional<PremiumList> getLatestRevision(String premiumListName) {
@@ -169,7 +169,7 @@ public class PremiumListDao {
}
private static Optional<PremiumList> getLatestRevisionUncached(String premiumListName) {
return tm().transact(
return tm().reTransact(
() ->
tm().query(
"FROM PremiumList WHERE name = :name ORDER BY revisionId DESC",
@@ -197,10 +197,10 @@ public class PremiumListDao {
/**
* Loads the price for the given revisionId + label combination. Note that this does a database
* retrieval so it should only be done in a cached context.
* retrieval, so it should only be done in a cached context.
*/
static Optional<BigDecimal> getPriceForLabelUncached(RevisionIdAndLabel revisionIdAndLabel) {
return tm().transact(
return tm().reTransact(
() ->
tm().query(
"SELECT pe.price FROM PremiumEntry pe WHERE pe.revisionId = :revisionId"

View File

@@ -199,7 +199,7 @@ public final class ReservedList
public synchronized ImmutableMap<String, ReservedListEntry> getReservedListEntries() {
if (reservedListMap == null) {
reservedListMap =
tm().transact(
tm().reTransact(
() ->
tm()
.createQueryComposer(ReservedListEntry.class)

View File

@@ -47,7 +47,7 @@ public class ReservedListDao {
* exists.
*/
public static Optional<ReservedList> getLatestRevision(String reservedListName) {
return tm().transact(
return tm().reTransact(
() ->
tm().query(
"FROM ReservedList WHERE revisionId IN "

View File

@@ -65,7 +65,7 @@ public class ClaimsListDao {
* doesn't exist.
*/
private static ClaimsList getUncached() {
return tm().transact(
return tm().reTransact(
() -> {
Long revisionId =
tm().query("SELECT MAX(revisionId) FROM ClaimsList", Long.class)

View File

@@ -267,7 +267,7 @@ public abstract class PersistenceModule {
name -> overrides.put(HIKARI_DS_CLOUD_SQL_INSTANCE, name));
overrides.put(
Environment.ISOLATION, TransactionIsolationLevel.TRANSACTION_REPEATABLE_READ.name());
return new JpaTransactionManagerImpl(create(overrides), clock);
return new JpaTransactionManagerImpl(create(overrides), clock, true);
}
@Provides
@@ -283,7 +283,7 @@ public abstract class PersistenceModule {
name -> overrides.put(HIKARI_DS_CLOUD_SQL_INSTANCE, name));
overrides.put(
Environment.ISOLATION, TransactionIsolationLevel.TRANSACTION_REPEATABLE_READ.name());
return new JpaTransactionManagerImpl(create(overrides), clock);
return new JpaTransactionManagerImpl(create(overrides), clock, true);
}
/** Constructs the {@link EntityManagerFactory} instance. */

View File

@@ -38,6 +38,7 @@ import google.registry.persistence.JpaRetries;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import google.registry.persistence.VKey;
import google.registry.util.Clock;
import google.registry.util.RegistryEnvironment;
import google.registry.util.Retrier;
import google.registry.util.SystemSleeper;
import java.io.Serializable;
@@ -85,13 +86,19 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
// EntityManagerFactory is thread safe.
private final EntityManagerFactory emf;
private final Clock clock;
private final boolean readOnly;
private static final ThreadLocal<TransactionInfo> transactionInfo =
ThreadLocal.withInitial(TransactionInfo::new);
public JpaTransactionManagerImpl(EntityManagerFactory emf, Clock clock) {
public JpaTransactionManagerImpl(EntityManagerFactory emf, Clock clock, boolean readOnly) {
this.emf = emf;
this.clock = clock;
this.readOnly = readOnly;
}
public JpaTransactionManagerImpl(EntityManagerFactory emf, Clock clock) {
this(emf, clock, false);
}
@Override
@@ -158,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);
}
@@ -200,6 +210,10 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
try {
txn.begin();
txnInfo.start(clock);
if (readOnly) {
getEntityManager().createNativeQuery("SET TRANSACTION READ ONLY").executeUpdate();
logger.atInfo().log("Using read-only SQL replica");
}
if (isolationLevel != null && isolationLevel != getDefaultTransactionIsolationLevel()) {
getEntityManager()
.createNativeQuery(

View File

@@ -20,10 +20,10 @@ import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.appengine.api.utils.SystemProperty;
import com.google.appengine.api.utils.SystemProperty.Environment.Value;
import com.google.common.base.Suppliers;
import google.registry.config.RegistryEnvironment;
import google.registry.persistence.DaggerPersistenceComponent;
import google.registry.tools.RegistryToolEnvironment;
import google.registry.util.NonFinalForTesting;
import google.registry.util.RegistryEnvironment;
import java.util.function.Supplier;
/** Factory class to create {@link TransactionManager} instance. */

View File

@@ -40,7 +40,6 @@ import com.google.common.flogger.FluentLogger;
import com.google.common.io.BaseEncoding;
import google.registry.beam.rde.RdePipeline;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryEnvironment;
import google.registry.gcs.GcsUtils;
import google.registry.keyring.api.KeyModule.Key;
import google.registry.model.common.Cursor;
@@ -57,6 +56,7 @@ import google.registry.request.RequestParameters;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.util.Clock;
import google.registry.util.RegistryEnvironment;
import google.registry.xml.ValidationMode;
import java.io.IOException;
import java.util.Optional;

View File

@@ -29,7 +29,6 @@ import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import google.registry.batch.CloudTasksUtils;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryEnvironment;
import google.registry.persistence.PersistenceModule;
import google.registry.reporting.ReportingModule;
import google.registry.request.Action;
@@ -38,6 +37,7 @@ import google.registry.request.Parameter;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.util.Clock;
import google.registry.util.RegistryEnvironment;
import java.io.IOException;
import javax.inject.Inject;
import org.joda.time.Duration;

View File

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

View File

@@ -29,7 +29,6 @@ import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import google.registry.batch.CloudTasksUtils;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryEnvironment;
import google.registry.keyring.api.KeyModule.Key;
import google.registry.reporting.ReportingModule;
import google.registry.request.Action;
@@ -38,6 +37,7 @@ import google.registry.request.Parameter;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.util.Clock;
import google.registry.util.RegistryEnvironment;
import java.io.IOException;
import javax.inject.Inject;
import org.joda.time.Duration;

View File

@@ -28,6 +28,7 @@ import com.google.common.net.MediaType;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URLConnection;
import java.util.Random;
@@ -36,26 +37,37 @@ public final class UrlConnectionUtils {
private UrlConnectionUtils() {}
/** Retrieves the response from the given connection as a byte array. */
public static byte[] getResponseBytes(URLConnection connection) throws IOException {
try (InputStream is = connection.getInputStream()) {
/**
* Retrieves the response from the given connection as a byte array.
*
* <p>Note that in the event the response code is 4XX or 5XX, we use the error stream as any
* payload is included there.
*
* @see HttpURLConnection#getErrorStream()
*/
public static byte[] getResponseBytes(HttpURLConnection connection) throws IOException {
int responseCode = connection.getResponseCode();
try (InputStream is =
responseCode < 400 ? connection.getInputStream() : connection.getErrorStream()) {
return ByteStreams.toByteArray(is);
} catch (NullPointerException e) {
return new byte[] {};
}
}
/** Sets auth on the given connection with the given username/password. */
public static void setBasicAuth(URLConnection connection, String username, String password) {
public static void setBasicAuth(HttpURLConnection connection, String username, String password) {
setBasicAuth(connection, String.format("%s:%s", username, password));
}
/** Sets auth on the given connection with the given string, formatted "username:password". */
public static void setBasicAuth(URLConnection connection, String usernameAndPassword) {
public static void setBasicAuth(HttpURLConnection connection, String usernameAndPassword) {
String token = base64().encode(usernameAndPassword.getBytes(UTF_8));
connection.setRequestProperty(AUTHORIZATION, "Basic " + token);
}
/** Sets the given byte[] payload on the given connection with a particular content type. */
public static void setPayload(URLConnection connection, byte[] bytes, String contentType)
public static void setPayload(HttpURLConnection connection, byte[] bytes, String contentType)
throws IOException {
connection.setRequestProperty(CONTENT_TYPE, contentType);
connection.setDoOutput(true);
@@ -72,7 +84,7 @@ public final class UrlConnectionUtils {
* @see <a href="http://www.ietf.org/rfc/rfc2388.txt">RFC2388 - Returning Values from Forms</a>
*/
public static void setPayloadMultipart(
URLConnection connection,
HttpURLConnection connection,
String name,
String filename,
MediaType contentType,

View File

@@ -20,13 +20,13 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryEnvironment;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.request.auth.AuthModule.IapOidc;
import google.registry.request.auth.AuthModule.RegularOidc;
import google.registry.request.auth.AuthModule.RegularOidcFallback;
import google.registry.request.auth.AuthSettings.AuthLevel;
import google.registry.util.RegistryEnvironment;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.inject.Inject;

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -463,8 +463,7 @@ abstract class CreateOrUpdateTldCommand extends MutatingCommand {
}
}
private void checkReservedListValidityForTld(String tld, Set<String> reservedListNames)
throws UnsupportedEncodingException {
private void checkReservedListValidityForTld(String tld, Set<String> reservedListNames) {
ImmutableList.Builder<String> builder = new ImmutableList.Builder<>();
for (String reservedListName : reservedListNames) {
if (!reservedListName.startsWith("common_") && !reservedListName.startsWith(tld + "_")) {

View File

@@ -30,8 +30,8 @@ import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
import google.registry.config.RegistryEnvironment;
import google.registry.model.registrar.Registrar;
import google.registry.util.RegistryEnvironment;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

View File

@@ -21,8 +21,8 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import google.registry.config.RegistryEnvironment;
import google.registry.config.SystemPropertySetter;
import google.registry.util.RegistryEnvironment;
import google.registry.util.SystemPropertySetter;
/** Enum of production environments, used for the {@code --environment} flag. */
public enum RegistryToolEnvironment {

View File

@@ -22,10 +22,10 @@ import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.MoreFiles;
import google.registry.config.RegistryEnvironment;
import google.registry.model.OteAccountBuilder;
import google.registry.tools.params.PathParameter;
import google.registry.util.Clock;
import google.registry.util.RegistryEnvironment;
import google.registry.util.StringGenerator;
import java.nio.file.Path;
import java.util.ArrayList;

View File

@@ -67,9 +67,12 @@ abstract class UpdateOrDeleteAllocationTokensCommand extends ConfirmingCommand {
checkArgument(!prefix.isEmpty(), "Provided prefix should not be blank");
return tm().transact(
() ->
tm().loadAllOf(AllocationToken.class).stream()
.filter(token -> token.getToken().startsWith(prefix))
.map(AllocationToken::createVKey)
tm().query(
"SELECT token FROM AllocationToken WHERE token LIKE :prefix",
String.class)
.setParameter("prefix", String.format("%s%%", prefix))
.getResultStream()
.map(token -> VKey.create(AllocationToken.class, token))
.collect(toImmutableList()));
}
}

View File

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

View File

@@ -18,8 +18,8 @@ import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
import com.beust.jcommander.Parameters;
import google.registry.config.RegistryEnvironment;
import google.registry.model.registrar.Registrar;
import google.registry.util.RegistryEnvironment;
import javax.annotation.Nullable;
/** Command to update a Registrar. */

View File

@@ -14,6 +14,7 @@
package google.registry.tools;
import static google.registry.util.DiffUtils.prettyPrintEntityDeepDiff;
import static google.registry.util.ListNamingUtils.convertFilePathToName;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -46,17 +47,27 @@ final class UpdateReservedListCommand extends CreateOrUpdateReservedListCommand
.setReservedListMapFromLines(allLines)
.setShouldPublish(shouldPublish);
reservedList = updated.build();
// only call stageEntityChange if there are changes in entries
if (!existingReservedList
.getReservedListEntries()
.equals(reservedList.getReservedListEntries())) {
return String.format(
"Update reserved list for %s?\nOld list: %s\n New list: %s",
name,
outputReservedListEntries(existingReservedList),
outputReservedListEntries(reservedList));
boolean shouldPublishChanged =
existingReservedList.getShouldPublish() != reservedList.getShouldPublish();
boolean reservedListEntriesChanged =
!existingReservedList
.getReservedListEntries()
.equals(reservedList.getReservedListEntries());
if (!shouldPublishChanged && !reservedListEntriesChanged) {
return "No entity changes to apply.";
}
return "No entity changes to apply.";
String result = String.format("Update reserved list for %s?\n", name);
if (shouldPublishChanged) {
result +=
String.format(
"shouldPublish: %s -> %s\n",
existingReservedList.getShouldPublish(), reservedList.getShouldPublish());
}
if (reservedListEntriesChanged) {
result +=
prettyPrintEntityDeepDiff(
existingReservedList.getReservedListEntries(), reservedList.getReservedListEntries());
}
return result;
}
}

View File

@@ -25,10 +25,10 @@ import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import google.registry.config.RegistryEnvironment;
import google.registry.model.tld.Tld;
import google.registry.model.tld.Tld.TldState;
import google.registry.tools.params.StringListParameter;
import google.registry.util.RegistryEnvironment;
import java.util.List;
import java.util.Map;
import java.util.Optional;

View File

@@ -25,9 +25,9 @@ import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
import google.registry.config.RegistryEnvironment;
import google.registry.model.registrar.Registrar;
import google.registry.tools.server.VerifyOteAction;
import google.registry.util.RegistryEnvironment;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;

View File

@@ -19,6 +19,7 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
import com.google.api.client.http.HttpStatusCodes;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ascii;
import com.google.gson.Gson;
import com.google.gson.annotations.Expose;
import google.registry.model.CreateAutoTimestamp;
@@ -33,6 +34,7 @@ import google.registry.ui.server.registrar.JsonGetAction;
import java.util.List;
import java.util.Optional;
import javax.inject.Inject;
import javax.persistence.TypedQuery;
import org.joda.time.DateTime;
/** Returns a (paginated) list of domains for a particular registrar. */
@@ -49,6 +51,8 @@ public class ConsoleDomainListAction implements JsonGetAction {
private static final String DOMAIN_QUERY_TEMPLATE =
"FROM Domain WHERE currentSponsorRegistrarId = :registrarId AND deletionTime >"
+ " :deletedAfterTime AND creationTime <= :createdBeforeTime";
private static final String SEARCH_TERM_QUERY = " AND LOWER(domainName) LIKE :searchTerm";
private static final String ORDER_BY_STATEMENT = " ORDER BY creationTime DESC";
private final AuthResult authResult;
private final Response response;
@@ -58,6 +62,7 @@ public class ConsoleDomainListAction implements JsonGetAction {
private final int pageNumber;
private final int resultsPerPage;
private final Optional<Long> totalResults;
private final Optional<String> searchTerm;
@Inject
public ConsoleDomainListAction(
@@ -68,7 +73,8 @@ public class ConsoleDomainListAction implements JsonGetAction {
@Parameter("checkpointTime") Optional<DateTime> checkpointTime,
@Parameter("pageNumber") Optional<Integer> pageNumber,
@Parameter("resultsPerPage") Optional<Integer> resultsPerPage,
@Parameter("totalResults") Optional<Long> totalResults) {
@Parameter("totalResults") Optional<Long> totalResults,
@Parameter("searchTerm") Optional<String> searchTerm) {
this.authResult = authResult;
this.response = response;
this.gson = gson;
@@ -77,6 +83,7 @@ public class ConsoleDomainListAction implements JsonGetAction {
this.pageNumber = pageNumber.orElse(0);
this.resultsPerPage = resultsPerPage.orElse(DEFAULT_RESULTS_PER_PAGE);
this.totalResults = totalResults;
this.searchTerm = searchTerm;
}
@Override
@@ -110,13 +117,13 @@ public class ConsoleDomainListAction implements JsonGetAction {
long actualTotalResults =
totalResults.orElseGet(
() ->
tm().query("SELECT COUNT(*) " + DOMAIN_QUERY_TEMPLATE, Long.class)
createCountQuery()
.setParameter("registrarId", registrarId)
.setParameter("createdBeforeTime", checkpointTimestamp)
.setParameter("deletedAfterTime", checkpoint)
.getSingleResult());
List<Domain> domains =
tm().query(DOMAIN_QUERY_TEMPLATE + " ORDER BY creationTime DESC", Domain.class)
createDomainQuery()
.setParameter("registrarId", registrarId)
.setParameter("createdBeforeTime", checkpointTimestamp)
.setParameter("deletedAfterTime", checkpoint)
@@ -127,6 +134,26 @@ public class ConsoleDomainListAction implements JsonGetAction {
response.setStatus(HttpStatusCodes.STATUS_CODE_OK);
}
/** Creates the query to get the total number of matching domains, interpolating as necessary. */
private TypedQuery<Long> createCountQuery() {
String queryString = "SELECT COUNT(*) " + DOMAIN_QUERY_TEMPLATE;
if (searchTerm.isPresent() && !searchTerm.get().isEmpty()) {
return tm().query(queryString + SEARCH_TERM_QUERY, Long.class)
.setParameter("searchTerm", String.format("%%%s%%", Ascii.toLowerCase(searchTerm.get())));
}
return tm().query(queryString, Long.class);
}
/** Creates the query to retrieve the matching domains themselves, interpolating as necessary. */
private TypedQuery<Domain> createDomainQuery() {
if (searchTerm.isPresent() && !searchTerm.get().isEmpty()) {
return tm().query(
DOMAIN_QUERY_TEMPLATE + SEARCH_TERM_QUERY + ORDER_BY_STATEMENT, Domain.class)
.setParameter("searchTerm", String.format("%%%s%%", Ascii.toLowerCase(searchTerm.get())));
}
return tm().query(DOMAIN_QUERY_TEMPLATE + ORDER_BY_STATEMENT, Domain.class);
}
private void writeBadRequest(String message) {
response.setPayload(message);
response.setStatus(HttpStatusCodes.STATUS_CODE_BAD_REQUEST);

View File

@@ -15,8 +15,8 @@
package google.registry.ui.server.registrar;
import static com.google.common.base.Preconditions.checkState;
import static google.registry.config.RegistryEnvironment.PRODUCTION;
import static google.registry.ui.server.SoyTemplateUtils.CSS_RENAMING_MAP_SUPPLIER;
import static google.registry.util.RegistryEnvironment.PRODUCTION;
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import com.google.common.base.Ascii;
@@ -24,7 +24,6 @@ import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.template.soy.tofu.SoyTofu;
import google.registry.config.RegistryEnvironment;
import google.registry.model.OteAccountBuilder;
import google.registry.request.Action;
import google.registry.request.Action.Method;
@@ -34,6 +33,7 @@ import google.registry.request.auth.AuthenticatedRegistrarAccessor;
import google.registry.ui.server.SendEmailUtils;
import google.registry.ui.server.SoyTemplateUtils;
import google.registry.ui.soy.registrar.OteSetupConsoleSoyInfo;
import google.registry.util.RegistryEnvironment;
import google.registry.util.StringGenerator;
import java.util.HashMap;
import java.util.Optional;

View File

@@ -26,7 +26,6 @@ import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.template.soy.tofu.SoyTofu;
import google.registry.config.RegistryEnvironment;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.Registrar.State;
import google.registry.model.registrar.RegistrarAddress;
@@ -44,6 +43,7 @@ import google.registry.ui.soy.registrar.ConsoleSoyInfo;
import google.registry.ui.soy.registrar.ConsoleUtilsSoyInfo;
import google.registry.ui.soy.registrar.FormsSoyInfo;
import google.registry.ui.soy.registrar.RegistrarCreateConsoleSoyInfo;
import google.registry.util.RegistryEnvironment;
import google.registry.util.StringGenerator;
import java.util.HashMap;
import java.util.Optional;

View File

@@ -27,7 +27,6 @@ import com.google.common.flogger.FluentLogger;
import com.google.template.soy.data.SoyMapData;
import com.google.template.soy.tofu.SoyTofu;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryEnvironment;
import google.registry.request.Action;
import google.registry.request.Parameter;
import google.registry.request.auth.Auth;
@@ -36,6 +35,7 @@ import google.registry.request.auth.AuthenticatedRegistrarAccessor.RegistrarAcce
import google.registry.request.auth.AuthenticatedRegistrarAccessor.Role;
import google.registry.ui.server.SoyTemplateUtils;
import google.registry.ui.soy.registrar.ConsoleSoyInfo;
import google.registry.util.RegistryEnvironment;
import java.util.HashMap;
import java.util.Optional;
import javax.inject.Inject;

View File

@@ -226,4 +226,10 @@ public final class RegistrarConsoleModule {
public static Optional<Long> provideTotalResults(HttpServletRequest req) {
return extractOptionalParameter(req, "totalResults").map(Long::valueOf);
}
@Provides
@Parameter("searchTerm")
public static Optional<String> provideSearchTerm(HttpServletRequest req) {
return extractOptionalParameter(req, "searchTerm");
}
}

View File

@@ -18,11 +18,11 @@ import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Sets.difference;
import static google.registry.config.RegistryEnvironment.PRODUCTION;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.security.JsonResponseHelper.Status.ERROR;
import static google.registry.security.JsonResponseHelper.Status.SUCCESS;
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
import static google.registry.util.RegistryEnvironment.PRODUCTION;
import com.google.auto.value.AutoValue;
import com.google.common.base.Ascii;
@@ -37,7 +37,6 @@ import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import google.registry.batch.CloudTasksUtils;
import google.registry.config.RegistryEnvironment;
import google.registry.export.sheet.SyncRegistrarsSheetAction;
import google.registry.flows.certs.CertificateChecker;
import google.registry.flows.certs.CertificateChecker.InsecureCertificateException;
@@ -61,6 +60,7 @@ import google.registry.ui.server.RegistrarFormFields;
import google.registry.ui.server.SendEmailUtils;
import google.registry.util.CollectionUtils;
import google.registry.util.DiffUtils;
import google.registry.util.RegistryEnvironment;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;

View File

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

View File

@@ -33,7 +33,6 @@ import static org.joda.time.DateTimeZone.UTC;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableSet;
import google.registry.config.RegistryEnvironment;
import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingEvent;
@@ -47,6 +46,7 @@ import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.SystemPropertyExtension;
import google.registry.util.RegistryEnvironment;
import java.util.Optional;
import java.util.Set;
import org.joda.money.Money;

View File

@@ -25,8 +25,8 @@ import com.google.api.services.dataflow.model.LaunchFlexTemplateParameter;
import com.google.api.services.dataflow.model.LaunchFlexTemplateRequest;
import com.google.common.collect.ImmutableMap;
import google.registry.beam.BeamActionTestBase;
import google.registry.config.RegistryEnvironment;
import google.registry.testing.FakeClock;
import google.registry.util.RegistryEnvironment;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link ResaveAllEppResourcesPipelineAction}. */

View File

@@ -18,10 +18,10 @@ import static com.google.common.truth.Truth.assertThat;
import static google.registry.beam.common.RegistryPipelineOptions.validateRegistryPipelineOptions;
import static org.junit.jupiter.api.Assertions.assertThrows;
import google.registry.config.RegistryEnvironment;
import google.registry.persistence.PersistenceModule.JpaTransactionManagerType;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import google.registry.testing.SystemPropertyExtension;
import google.registry.util.RegistryEnvironment;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More