mirror of
https://github.com/google/nomulus
synced 2026-02-07 21:41:03 +00:00
Compare commits
56 Commits
nomulus-20
...
nomulus-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42b508427b | ||
|
|
20b5b43501 | ||
|
|
08285f5de7 | ||
|
|
fb4c5b457d | ||
|
|
781c212275 | ||
|
|
c73f7a6bd3 | ||
|
|
8d793b2349 | ||
|
|
55d5f8c6f8 | ||
|
|
9006312253 | ||
|
|
e5e2370923 | ||
|
|
b3b0efd47e | ||
|
|
e82cbe60a9 | ||
|
|
923bc13e3a | ||
|
|
4893ea307b | ||
|
|
01f868cefc | ||
|
|
1b0919eaff | ||
|
|
92b23bac16 | ||
|
|
cc9b3f5965 | ||
|
|
dd86c56ddc | ||
|
|
08551f7bc7 | ||
|
|
e7171a326b | ||
|
|
c3eae7b76f | ||
|
|
2687181045 | ||
|
|
68750569db | ||
|
|
028e5cc958 | ||
|
|
853e571d01 | ||
|
|
9b79f5af2c | ||
|
|
4195871541 | ||
|
|
504d7ccaac | ||
|
|
36a8908712 | ||
|
|
e42c11051e | ||
|
|
85b588b51f | ||
|
|
572b7101cb | ||
|
|
445825957d | ||
|
|
7ab76f3573 | ||
|
|
9e3c58989a | ||
|
|
cf9c1ec7c3 | ||
|
|
69ea87be31 | ||
|
|
779d0c9d37 | ||
|
|
2855944214 | ||
|
|
992d1c1349 | ||
|
|
f50290ce1d | ||
|
|
e647d4e215 | ||
|
|
08471242df | ||
|
|
cd23fea698 | ||
|
|
ba54208dad | ||
|
|
b5e131ecba | ||
|
|
87e99f59bc | ||
|
|
30accea383 | ||
|
|
72e0101746 | ||
|
|
3090df9a78 | ||
|
|
7332b1fa38 | ||
|
|
9330e3a50d | ||
|
|
1d6b119340 | ||
|
|
8158f761c8 | ||
|
|
08838e091f |
@@ -347,6 +347,7 @@ subprojects {
|
||||
|
||||
def services = [':services:default',
|
||||
':services:backend',
|
||||
':services:bsa',
|
||||
':services:tools',
|
||||
':services:pubapi']
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import ContactComponent from './settings/contact/contact.component';
|
||||
import WhoisComponent from './settings/whois/whois.component';
|
||||
import SecurityComponent from './settings/security/security.component';
|
||||
import UsersComponent from './settings/users/users.component';
|
||||
import { DomainListComponent } from './domains/domainList.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||
@@ -35,6 +36,11 @@ const routes: Routes = [
|
||||
{ path: 'empty-registrar', component: EmptyRegistrar },
|
||||
{ path: 'home', component: HomeComponent, canActivate: [RegistrarGuard] },
|
||||
{ path: 'tlds', component: TldsComponent, canActivate: [RegistrarGuard] },
|
||||
{
|
||||
path: DomainListComponent.PATH,
|
||||
component: DomainListComponent,
|
||||
canActivate: [RegistrarGuard],
|
||||
},
|
||||
{
|
||||
path: SettingsComponent.PATH,
|
||||
component: SettingsComponent,
|
||||
|
||||
@@ -36,30 +36,31 @@ 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,
|
||||
DomainListComponent,
|
||||
DomainsWidgetComponent,
|
||||
EmptyRegistrar,
|
||||
EppWidgetComponent,
|
||||
@@ -68,7 +69,6 @@ import {
|
||||
PromotionsWidgetComponent,
|
||||
RegistrarComponent,
|
||||
RegistrarDetailsComponent,
|
||||
RegistrarDetailsWrapperComponent,
|
||||
RegistrarSelectorComponent,
|
||||
ResourcesWidgetComponent,
|
||||
SecurityComponent,
|
||||
|
||||
60
console-webapp/src/app/domains/domainList.component.html
Normal file
60
console-webapp/src/app/domains/domainList.component.html
Normal file
@@ -0,0 +1,60 @@
|
||||
<div class="console-domains">
|
||||
<mat-form-field>
|
||||
<mat-label>Filter</mat-label>
|
||||
<input
|
||||
type="search"
|
||||
matInput
|
||||
[(ngModel)]="searchTerm"
|
||||
(ngModelChange)="sendInput()"
|
||||
#input
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<div *ngIf="isLoading; else domains_content" class="console-domains__loading">
|
||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||
</div>
|
||||
<ng-template #domains_content>
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="domainName">
|
||||
<th mat-header-cell *matHeaderCellDef>Domain Name</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.domainName }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="creationTime">
|
||||
<th mat-header-cell *matHeaderCellDef>Creation Time</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
{{ element.creationTime.creationTime }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="registrationExpirationTime">
|
||||
<th mat-header-cell *matHeaderCellDef>Expiration Time</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
{{ element.registrationExpirationTime }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="statuses">
|
||||
<th mat-header-cell *matHeaderCellDef>Statuses</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.statuses }}</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
|
||||
<!-- Row shown when there is no matching data. -->
|
||||
<tr class="mat-row" *matNoDataRow>
|
||||
<td class="mat-cell" colspan="4">No domains found</td>
|
||||
</tr>
|
||||
</table>
|
||||
<mat-paginator
|
||||
[length]="totalResults"
|
||||
[pageIndex]="pageNumber"
|
||||
[pageSize]="resultsPerPage"
|
||||
[pageSizeOptions]="[10, 25, 50, 100, 500]"
|
||||
(page)="onPageChange($event)"
|
||||
aria-label="Select page of domain results"
|
||||
showFirstLastButtons
|
||||
></mat-paginator>
|
||||
</ng-template>
|
||||
</div>
|
||||
36
console-webapp/src/app/domains/domainList.component.spec.ts
Normal file
36
console-webapp/src/app/domains/domainList.component.spec.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// 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 { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DomainListComponent } from './domainList.component';
|
||||
|
||||
describe('DomainListComponent', () => {
|
||||
let component: DomainListComponent;
|
||||
let fixture: ComponentFixture<DomainListComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [DomainListComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(DomainListComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
98
console-webapp/src/app/domains/domainList.component.ts
Normal file
98
console-webapp/src/app/domains/domainList.component.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
// 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 { Component, ViewChild } from '@angular/core';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
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',
|
||||
templateUrl: './domainList.component.html',
|
||||
styleUrls: ['./domainList.component.scss'],
|
||||
providers: [DomainListService],
|
||||
})
|
||||
export class DomainListComponent {
|
||||
public static PATH = 'domain-list';
|
||||
private readonly DEBOUNCE_MS = 500;
|
||||
|
||||
displayedColumns: string[] = [
|
||||
'domainName',
|
||||
'creationTime',
|
||||
'registrationExpirationTime',
|
||||
'statuses',
|
||||
];
|
||||
|
||||
dataSource: MatTableDataSource<Domain> = new MatTableDataSource();
|
||||
isLoading = true;
|
||||
|
||||
searchTermSubject = new Subject<string>();
|
||||
searchTerm?: string;
|
||||
|
||||
pageNumber?: number;
|
||||
resultsPerPage = 50;
|
||||
totalResults?: number;
|
||||
|
||||
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
|
||||
|
||||
constructor(
|
||||
private backendService: BackendService,
|
||||
private domainListService: DomainListService,
|
||||
private registrarService: RegistrarService
|
||||
) {}
|
||||
|
||||
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,
|
||||
this.searchTerm
|
||||
)
|
||||
.subscribe((domainListResult) => {
|
||||
this.dataSource.data = domainListResult.domains;
|
||||
this.totalResults = domainListResult.totalResults;
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
sendInput() {
|
||||
this.searchTermSubject.next(this.searchTerm!);
|
||||
}
|
||||
|
||||
onPageChange(event: PageEvent) {
|
||||
this.pageNumber = event.pageIndex;
|
||||
this.resultsPerPage = event.pageSize;
|
||||
this.reloadData();
|
||||
}
|
||||
}
|
||||
68
console-webapp/src/app/domains/domainList.service.ts
Normal file
68
console-webapp/src/app/domains/domainList.service.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
// 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 { Injectable } from '@angular/core';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import { tap } from 'rxjs';
|
||||
|
||||
export interface CreateAutoTimestamp {
|
||||
creationTime: string;
|
||||
}
|
||||
|
||||
export interface Domain {
|
||||
creationTime: CreateAutoTimestamp;
|
||||
currentSponsorRegistrarId: string;
|
||||
domainName: string;
|
||||
registrationExpirationTime: string;
|
||||
statuses: string[];
|
||||
}
|
||||
|
||||
export interface DomainListResult {
|
||||
checkpointTime: string;
|
||||
domains: Domain[];
|
||||
totalResults: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class DomainListService {
|
||||
checkpointTime?: string;
|
||||
|
||||
constructor(
|
||||
private backendService: BackendService,
|
||||
private registrarService: RegistrarService
|
||||
) {}
|
||||
|
||||
retrieveDomains(
|
||||
pageNumber?: number,
|
||||
resultsPerPage?: number,
|
||||
totalResults?: number,
|
||||
searchTerm?: string
|
||||
) {
|
||||
return this.backendService
|
||||
.getDomains(
|
||||
this.registrarService.activeRegistrarId,
|
||||
this.checkpointTime,
|
||||
pageNumber,
|
||||
resultsPerPage,
|
||||
totalResults,
|
||||
searchTerm
|
||||
)
|
||||
.pipe(
|
||||
tap((domainListResult: DomainListResult) => {
|
||||
this.checkpointTime = domainListResult.checkpointTime;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
@@ -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) {}
|
||||
@@ -13,7 +13,12 @@
|
||||
Create a Domain
|
||||
</button>
|
||||
<p class="secondary-text">Register a new domain name</p>
|
||||
<button mat-button color="primary" class="console-app__widget-link">
|
||||
<button
|
||||
mat-button
|
||||
color="primary"
|
||||
class="console-app__widget-link"
|
||||
(click)="openDomainsPage()"
|
||||
>
|
||||
View DUMs
|
||||
</button>
|
||||
<p class="secondary-text">
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { DomainListComponent } from 'src/app/domains/domainList.component';
|
||||
|
||||
@Component({
|
||||
selector: '[app-domains-widget]',
|
||||
templateUrl: './domainsWidget.component.html',
|
||||
})
|
||||
export class DomainsWidgetComponent {
|
||||
constructor(private router: Router) {}
|
||||
|
||||
openDomainsPage() {
|
||||
this.router.navigate([DomainListComponent.PATH]);
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
@@ -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() {}
|
||||
@@ -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) {}
|
||||
@@ -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) {}
|
||||
@@ -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() {}
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="registrarDetails">
|
||||
<div class="registrarDetails" *ngIf="registrarInEdit">
|
||||
<h3 mat-dialog-title>Edit Registrar: {{ registrarInEdit.registrarId }}</h3>
|
||||
<div mat-dialog-content>
|
||||
<form (ngSubmit)="saveAndClose($event)">
|
||||
<form (ngSubmit)="saveAndClose()">
|
||||
<mat-form-field class="registrarDetails__input">
|
||||
<mat-label>Registry Lock:</mat-label>
|
||||
<mat-select
|
||||
@@ -32,7 +32,7 @@
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button (click)="onCancel($event)">Cancel</button>
|
||||
<button mat-button (click)="this.params?.close()">Cancel</button>
|
||||
<button type="submit" mat-button color="primary">Save</button>
|
||||
</mat-dialog-actions>
|
||||
</form>
|
||||
|
||||
@@ -12,61 +12,38 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Injector } from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
import { Registrar, RegistrarService } from './registrar.service';
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import {
|
||||
MAT_BOTTOM_SHEET_DATA,
|
||||
MatBottomSheet,
|
||||
MatBottomSheetRef,
|
||||
} from '@angular/material/bottom-sheet';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialog,
|
||||
MatDialogRef,
|
||||
} from '@angular/material/dialog';
|
||||
import { MatChipInputEvent } from '@angular/material/chips';
|
||||
import { DialogBottomSheetContent } from '../shared/components/dialogBottomSheet.component';
|
||||
|
||||
const MOBILE_LAYOUT_BREAKPOINT = '(max-width: 599px)';
|
||||
type RegistrarDetailsParams = {
|
||||
close: Function;
|
||||
data: {
|
||||
registrar: Registrar;
|
||||
};
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'app-registrar-details',
|
||||
templateUrl: './registrarDetails.component.html',
|
||||
styleUrls: ['./registrarDetails.component.scss'],
|
||||
})
|
||||
export class RegistrarDetailsComponent {
|
||||
export class RegistrarDetailsComponent implements DialogBottomSheetContent {
|
||||
registrarInEdit!: Registrar;
|
||||
private elementRef:
|
||||
| MatBottomSheetRef<RegistrarDetailsComponent>
|
||||
| MatDialogRef<RegistrarDetailsComponent>;
|
||||
params?: RegistrarDetailsParams;
|
||||
|
||||
constructor(
|
||||
protected registrarService: RegistrarService,
|
||||
private injector: Injector
|
||||
) {
|
||||
// We only inject one, either Dialog or Bottom Sheet data
|
||||
// so one of the injectors is expected to fail
|
||||
try {
|
||||
var params = this.injector.get(MAT_DIALOG_DATA);
|
||||
this.elementRef = this.injector.get(MatDialogRef);
|
||||
} catch (e) {
|
||||
var params = this.injector.get(MAT_BOTTOM_SHEET_DATA);
|
||||
this.elementRef = this.injector.get(MatBottomSheetRef);
|
||||
}
|
||||
this.registrarInEdit = JSON.parse(JSON.stringify(params.registrar));
|
||||
constructor(protected registrarService: RegistrarService) {}
|
||||
|
||||
init(params: RegistrarDetailsParams) {
|
||||
this.params = params;
|
||||
this.registrarInEdit = JSON.parse(
|
||||
JSON.stringify(this.params.data.registrar)
|
||||
);
|
||||
}
|
||||
|
||||
onCancel(e: MouseEvent) {
|
||||
if (this.elementRef instanceof MatBottomSheetRef) {
|
||||
this.elementRef.dismiss();
|
||||
} else if (this.elementRef instanceof MatDialogRef) {
|
||||
this.elementRef.close();
|
||||
}
|
||||
}
|
||||
|
||||
saveAndClose(e: MouseEvent) {
|
||||
// TODO: Implement save call to API
|
||||
this.onCancel(e);
|
||||
saveAndClose() {
|
||||
this.params?.close();
|
||||
}
|
||||
|
||||
addTLD(e: MatChipInputEvent) {
|
||||
@@ -82,24 +59,3 @@ export class RegistrarDetailsComponent {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-registrar-details-wrapper',
|
||||
template: '',
|
||||
})
|
||||
export class RegistrarDetailsWrapperComponent {
|
||||
constructor(
|
||||
private dialog: MatDialog,
|
||||
private bottomSheet: MatBottomSheet,
|
||||
protected breakpointObserver: BreakpointObserver
|
||||
) {}
|
||||
|
||||
open(registrar: Registrar) {
|
||||
const config = { data: { registrar } };
|
||||
if (this.breakpointObserver.isMatched(MOBILE_LAYOUT_BREAKPOINT)) {
|
||||
this.bottomSheet.open(RegistrarDetailsComponent, config);
|
||||
} else {
|
||||
this.dialog.open(RegistrarDetailsComponent, config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -48,7 +48,7 @@
|
||||
[pageSizeOptions]="[5, 10, 20]"
|
||||
showFirstLastButtons
|
||||
></mat-paginator>
|
||||
<app-registrar-details-wrapper
|
||||
<app-dialog-bottom-sheet-wrapper
|
||||
#registrarDetailsView
|
||||
></app-registrar-details-wrapper>
|
||||
></app-dialog-bottom-sheet-wrapper>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,8 @@ import { Registrar, RegistrarService } from './registrar.service';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { RegistrarDetailsWrapperComponent } from './registrarDetails.component';
|
||||
import { RegistrarDetailsComponent } from './registrarDetails.component';
|
||||
import { DialogBottomSheetWrapper } from '../shared/components/dialogBottomSheet.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-registrar',
|
||||
@@ -82,7 +83,7 @@ export class RegistrarComponent {
|
||||
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
||||
@ViewChild(MatSort) sort!: MatSort;
|
||||
@ViewChild('registrarDetailsView')
|
||||
detailsComponentWrapper!: RegistrarDetailsWrapperComponent;
|
||||
detailsComponentWrapper!: DialogBottomSheetWrapper;
|
||||
|
||||
constructor(protected registrarService: RegistrarService) {
|
||||
this.dataSource = new MatTableDataSource<Registrar>(
|
||||
@@ -97,7 +98,10 @@ export class RegistrarComponent {
|
||||
|
||||
openDetails(event: MouseEvent, registrar: Registrar) {
|
||||
event.stopPropagation();
|
||||
this.detailsComponentWrapper.open(registrar);
|
||||
this.detailsComponentWrapper.open<RegistrarDetailsComponent>(
|
||||
RegistrarDetailsComponent,
|
||||
{ registrar }
|
||||
);
|
||||
}
|
||||
|
||||
applyFilter(event: Event) {
|
||||
|
||||
@@ -41,4 +41,7 @@
|
||||
<mat-icon>add</mat-icon>Create a Contact
|
||||
</button>
|
||||
</div>
|
||||
<app-dialog-bottom-sheet-wrapper
|
||||
#contactDetailsWrapper
|
||||
></app-dialog-bottom-sheet-wrapper>
|
||||
</div>
|
||||
|
||||
@@ -12,21 +12,14 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import {
|
||||
MatDialog,
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogRef,
|
||||
} from '@angular/material/dialog';
|
||||
import {
|
||||
MatBottomSheet,
|
||||
MAT_BOTTOM_SHEET_DATA,
|
||||
MatBottomSheetRef,
|
||||
} from '@angular/material/bottom-sheet';
|
||||
import { Component, ViewChild } from '@angular/core';
|
||||
import { Contact, ContactService } from './contact.service';
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import {
|
||||
DialogBottomSheetContent,
|
||||
DialogBottomSheetWrapper,
|
||||
} from 'src/app/shared/components/dialogBottomSheet.component';
|
||||
|
||||
enum Operations {
|
||||
DELETE,
|
||||
@@ -40,7 +33,13 @@ interface GroupedContacts {
|
||||
contacts: Array<Contact>;
|
||||
}
|
||||
|
||||
let isMobile: boolean;
|
||||
type ContactDetailsParams = {
|
||||
close: Function;
|
||||
data: {
|
||||
contact: Contact;
|
||||
operation: Operations;
|
||||
};
|
||||
};
|
||||
|
||||
const contactTypes: Array<GroupedContacts> = [
|
||||
{ value: 'ADMIN', label: 'Primary contact', contacts: [] },
|
||||
@@ -52,72 +51,46 @@ const contactTypes: Array<GroupedContacts> = [
|
||||
{ value: 'WHOIS', label: 'WHOIS-Inquiry contact', contacts: [] },
|
||||
];
|
||||
|
||||
class ContactDetailsEventsResponder {
|
||||
private ref?: MatDialogRef<any> | MatBottomSheetRef;
|
||||
constructor() {
|
||||
this.onClose = this.onClose.bind(this);
|
||||
}
|
||||
|
||||
setRef(ref: MatDialogRef<any> | MatBottomSheetRef) {
|
||||
this.ref = ref;
|
||||
}
|
||||
|
||||
onClose() {
|
||||
if (this.ref == undefined) {
|
||||
throw "Reference to ContactDetailsDialogComponent hasn't been set. ";
|
||||
}
|
||||
if (this.ref instanceof MatBottomSheetRef) {
|
||||
this.ref.dismiss();
|
||||
} else if (this.ref instanceof MatDialogRef) {
|
||||
this.ref.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-contact-details-dialog',
|
||||
templateUrl: '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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export class ContactService {
|
||||
return this.backend
|
||||
.getContacts(this.registrarService.activeRegistrarId)
|
||||
.pipe(
|
||||
tap((contacts) => {
|
||||
tap((contacts = []) => {
|
||||
this.contacts = contacts;
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<h3 mat-dialog-title>Contact details</h3>
|
||||
<div mat-dialog-content>
|
||||
<div mat-dialog-content *ngIf="contact">
|
||||
<form (ngSubmit)="saveAndClose($event)">
|
||||
<p>
|
||||
<mat-form-field class="contact-details__input">
|
||||
@@ -97,7 +97,7 @@
|
||||
>
|
||||
</section>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button (click)="onClose($event)">Cancel</button>
|
||||
<button mat-button (click)="close()">Cancel</button>
|
||||
<button type="submit" mat-button>Save</button>
|
||||
</mat-dialog-actions>
|
||||
</form>
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import { ComponentType } from '@angular/cdk/portal';
|
||||
import { Component } from '@angular/core';
|
||||
import {
|
||||
MatBottomSheet,
|
||||
MatBottomSheetRef,
|
||||
} from '@angular/material/bottom-sheet';
|
||||
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
|
||||
|
||||
const MOBILE_LAYOUT_BREAKPOINT = '(max-width: 599px)';
|
||||
|
||||
export interface DialogBottomSheetContent {
|
||||
init(data: Object): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps up a child component in an Angular Material Dalog for desktop or a Bottom Sheet
|
||||
* component for mobile depending on a screen resolution, with Breaking Point being 599px.
|
||||
* Child component is required to implement @see DialogBottomSheetContent interface
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-dialog-bottom-sheet-wrapper',
|
||||
template: '',
|
||||
})
|
||||
export class DialogBottomSheetWrapper {
|
||||
private elementRef?: MatBottomSheetRef | MatDialogRef<any>;
|
||||
|
||||
constructor(
|
||||
private dialog: MatDialog,
|
||||
private bottomSheet: MatBottomSheet,
|
||||
protected breakpointObserver: BreakpointObserver
|
||||
) {}
|
||||
|
||||
open<T extends DialogBottomSheetContent>(
|
||||
component: ComponentType<T>,
|
||||
data: any
|
||||
) {
|
||||
const config = { data, close: () => this.close() };
|
||||
if (this.breakpointObserver.isMatched(MOBILE_LAYOUT_BREAKPOINT)) {
|
||||
this.elementRef = this.bottomSheet.open(component);
|
||||
this.elementRef.instance.init(config);
|
||||
} else {
|
||||
this.elementRef = this.dialog.open(component);
|
||||
this.elementRef.componentInstance.init(config);
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.elementRef instanceof MatBottomSheetRef) {
|
||||
this.elementRef.dismiss();
|
||||
} else if (this.elementRef instanceof MatDialogRef) {
|
||||
this.elementRef.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import { Contact } from '../../settings/contact/contact.service';
|
||||
import { Registrar } from '../../registrar/registrar.service';
|
||||
import { UserData } from './userData.service';
|
||||
import { WhoisRegistrarFields } from 'src/app/settings/whois/whois.service';
|
||||
import { DomainListResult } from 'src/app/domains/domainList.service';
|
||||
|
||||
@Injectable()
|
||||
export class BackendService {
|
||||
@@ -63,6 +64,35 @@ export class BackendService {
|
||||
);
|
||||
}
|
||||
|
||||
getDomains(
|
||||
registrarId: string,
|
||||
checkpointTime?: string,
|
||||
pageNumber?: number,
|
||||
resultsPerPage?: number,
|
||||
totalResults?: number,
|
||||
searchTerm?: string
|
||||
): Observable<DomainListResult> {
|
||||
var url = `/console-api/domain-list?registrarId=${registrarId}`;
|
||||
if (checkpointTime) {
|
||||
url += `&checkpointTime=${checkpointTime}`;
|
||||
}
|
||||
if (pageNumber) {
|
||||
url += `&pageNumber=${pageNumber}`;
|
||||
}
|
||||
if (resultsPerPage) {
|
||||
url += `&resultsPerPage=${resultsPerPage}`;
|
||||
}
|
||||
if (totalResults) {
|
||||
url += `&totalResults=${totalResults}`;
|
||||
}
|
||||
if (searchTerm) {
|
||||
url += `&searchTerm=${searchTerm}`;
|
||||
}
|
||||
return this.http
|
||||
.get<DomainListResult>(url)
|
||||
.pipe(catchError((err) => this.errorCatcher<DomainListResult>(err)));
|
||||
}
|
||||
|
||||
getRegistrars(): Observable<Registrar[]> {
|
||||
return this.http
|
||||
.get<Registrar[]>('/console-api/registrars')
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
// Copyright 2021 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.batch;
|
||||
|
||||
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
||||
|
||||
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.persistence.PersistenceModule.SchemaManagerConnection;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.util.Retrier;
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.function.Supplier;
|
||||
import javax.inject.Inject;
|
||||
|
||||
/**
|
||||
* Wipes out all Cloud SQL data in a Nomulus GCP environment.
|
||||
*
|
||||
* <p>This class is created for the QA environment, where migration testing with production data
|
||||
* will happen. A regularly scheduled wipeout is a prerequisite to using production data there.
|
||||
*/
|
||||
@Action(
|
||||
service = Action.Service.BACKEND,
|
||||
path = "/_dr/task/wipeOutCloudSql",
|
||||
auth = Auth.AUTH_API_ADMIN)
|
||||
public class WipeOutCloudSqlAction implements Runnable {
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
private static final ImmutableSet<RegistryEnvironment> FORBIDDEN_ENVIRONMENTS =
|
||||
ImmutableSet.of(RegistryEnvironment.PRODUCTION, RegistryEnvironment.SANDBOX);
|
||||
|
||||
private final Supplier<Connection> connectionSupplier;
|
||||
private final Response response;
|
||||
private final Retrier retrier;
|
||||
|
||||
@Inject
|
||||
WipeOutCloudSqlAction(
|
||||
@SchemaManagerConnection Supplier<Connection> connectionSupplier,
|
||||
Response response,
|
||||
Retrier retrier) {
|
||||
this.connectionSupplier = connectionSupplier;
|
||||
this.response = response;
|
||||
this.retrier = retrier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
response.setContentType(PLAIN_TEXT_UTF_8);
|
||||
|
||||
if (FORBIDDEN_ENVIRONMENTS.contains(RegistryEnvironment.get())) {
|
||||
response.setStatus(SC_FORBIDDEN);
|
||||
response.setPayload("Wipeout is not allowed in " + RegistryEnvironment.get());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
retrier.callWithRetry(
|
||||
() -> {
|
||||
try (Connection conn = connectionSupplier.get()) {
|
||||
dropAllTables(conn, listTables(conn));
|
||||
dropAllSequences(conn, listSequences(conn));
|
||||
}
|
||||
return null;
|
||||
},
|
||||
e -> !(e instanceof SQLException));
|
||||
response.setStatus(SC_OK);
|
||||
response.setPayload("Wiped out Cloud SQL in " + RegistryEnvironment.get());
|
||||
} catch (RuntimeException e) {
|
||||
logger.atSevere().withCause(e).log("Failed to wipe out Cloud SQL data.");
|
||||
response.setStatus(SC_INTERNAL_SERVER_ERROR);
|
||||
response.setPayload("Failed to wipe out Cloud SQL in " + RegistryEnvironment.get());
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns a list of all tables in the public schema of a Postgresql database. */
|
||||
static ImmutableList<String> listTables(Connection connection) throws SQLException {
|
||||
try (ResultSet resultSet =
|
||||
connection.getMetaData().getTables(null, null, null, new String[] {"TABLE"})) {
|
||||
ImmutableList.Builder<String> tables = new ImmutableList.Builder<>();
|
||||
while (resultSet.next()) {
|
||||
String schema = resultSet.getString("TABLE_SCHEM");
|
||||
if (schema == null || !schema.equalsIgnoreCase("public")) {
|
||||
continue;
|
||||
}
|
||||
String tableName = resultSet.getString("TABLE_NAME");
|
||||
tables.add("public.\"" + tableName + "\"");
|
||||
}
|
||||
return tables.build();
|
||||
}
|
||||
}
|
||||
|
||||
static void dropAllTables(Connection conn, ImmutableList<String> tables) throws SQLException {
|
||||
if (tables.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (Statement statement = conn.createStatement()) {
|
||||
for (String table : tables) {
|
||||
statement.addBatch(String.format("DROP TABLE IF EXISTS %s CASCADE;", table));
|
||||
}
|
||||
for (int code : statement.executeBatch()) {
|
||||
if (code == Statement.EXECUTE_FAILED) {
|
||||
throw new RuntimeException("Failed to drop some tables. Please check.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns a list of all sequences in a Postgresql database. */
|
||||
static ImmutableList<String> listSequences(Connection conn) throws SQLException {
|
||||
try (Statement statement = conn.createStatement();
|
||||
ResultSet resultSet =
|
||||
statement.executeQuery("SELECT c.relname FROM pg_class c WHERE c.relkind = 'S';")) {
|
||||
ImmutableList.Builder<String> sequences = new ImmutableList.Builder<>();
|
||||
while (resultSet.next()) {
|
||||
sequences.add('\"' + resultSet.getString(1) + '\"');
|
||||
}
|
||||
return sequences.build();
|
||||
}
|
||||
}
|
||||
|
||||
static void dropAllSequences(Connection conn, ImmutableList<String> sequences)
|
||||
throws SQLException {
|
||||
if (sequences.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (Statement statement = conn.createStatement()) {
|
||||
for (String sequence : sequences) {
|
||||
statement.addBatch(String.format("DROP SEQUENCE IF EXISTS %s CASCADE;", sequence));
|
||||
}
|
||||
for (int code : statement.executeBatch()) {
|
||||
if (code == Statement.EXECUTE_FAILED) {
|
||||
throw new RuntimeException("Failed to drop some sequences. Please check.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -209,7 +209,7 @@ public final class RegistryJpaIO {
|
||||
|
||||
@ProcessElement
|
||||
public void processElement(OutputReceiver<T> outputReceiver) {
|
||||
tm().transactNoRetry(
|
||||
tm().transact(
|
||||
() -> {
|
||||
query.stream().map(resultMapper::apply).forEach(outputReceiver::output);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -12,12 +12,10 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
package google.registry.bsa;
|
||||
|
||||
@Component({
|
||||
selector: '[app-domains-widget]',
|
||||
templateUrl: './domains-widget.component.html',
|
||||
})
|
||||
export class DomainsWidgetComponent {
|
||||
constructor() {}
|
||||
/** Identifiers of the BSA lists with blocking labels. */
|
||||
public enum BlockList {
|
||||
BLOCK,
|
||||
BLOCK_PLUS;
|
||||
}
|
||||
46
core/src/main/java/google/registry/bsa/DownloadStage.java
Normal file
46
core/src/main/java/google/registry/bsa/DownloadStage.java
Normal file
@@ -0,0 +1,46 @@
|
||||
// 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;
|
||||
|
||||
/** The processing stages of a download. */
|
||||
public enum DownloadStage {
|
||||
/** Downloads BSA block list files. */
|
||||
DOWNLOAD,
|
||||
/** Generates block list diffs with the previous download. */
|
||||
MAKE_DIFF,
|
||||
/** Applies the label diffs to the database tables. */
|
||||
APPLY_DIFF,
|
||||
/**
|
||||
* Makes a REST API call to BSA endpoint, declaring that processing starts for new orders in the
|
||||
* diffs.
|
||||
*/
|
||||
START_UPLOADING,
|
||||
/** Makes a REST API call to BSA endpoint, sending the domains that cannot be blocked. */
|
||||
UPLOAD_DOMAINS_IN_USE,
|
||||
/** Makes a REST API call to BSA endpoint, declaring the completion of order processing. */
|
||||
FINISH_UPLOADING,
|
||||
/** The terminal stage after processing succeeds. */
|
||||
DONE,
|
||||
/**
|
||||
* The terminal stage indicating that the downloads are discarded because their checksums are the
|
||||
* same as that of the previous download.
|
||||
*/
|
||||
NOP,
|
||||
/**
|
||||
* The terminal stage indicating that the downloads are not processed because their BSA-generated
|
||||
* checksums do not match those calculated by us.
|
||||
*/
|
||||
CHECKSUMS_NOT_MATCH;
|
||||
}
|
||||
95
core/src/main/java/google/registry/bsa/IdnChecker.java
Normal file
95
core/src/main/java/google/registry/bsa/IdnChecker.java
Normal file
@@ -0,0 +1,95 @@
|
||||
// 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;
|
||||
|
||||
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.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableMultimap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.collect.Sets.SetView;
|
||||
import google.registry.model.tld.Tld;
|
||||
import google.registry.model.tld.Tld.TldType;
|
||||
import google.registry.model.tld.Tlds;
|
||||
import google.registry.tldconfig.idn.IdnLabelValidator;
|
||||
import google.registry.tldconfig.idn.IdnTableEnum;
|
||||
import google.registry.util.Clock;
|
||||
import javax.inject.Inject;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/**
|
||||
* Checks labels' validity wrt Idns in TLDs enrolled with BSA.
|
||||
*
|
||||
* <p>Each instance takes a snapshot of the TLDs at instantiation time, and should be limited to the
|
||||
* Request scope.
|
||||
*/
|
||||
public class IdnChecker {
|
||||
private static final IdnLabelValidator IDN_LABEL_VALIDATOR = new IdnLabelValidator();
|
||||
|
||||
private final ImmutableMap<IdnTableEnum, ImmutableSet<Tld>> idnToTlds;
|
||||
private final ImmutableSet<Tld> allTlds;
|
||||
|
||||
@Inject
|
||||
IdnChecker(Clock clock) {
|
||||
this.idnToTlds = getIdnToTldMap(clock.nowUtc());
|
||||
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()
|
||||
.filter(idnTable -> idnTable.getTable().isValidLabel(label))
|
||||
.collect(toImmutableSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the TLDs that support at least one IDN in the {@code idnTables}.
|
||||
*
|
||||
* @param idnTables String names of {@link IdnTableEnum} values
|
||||
*/
|
||||
public ImmutableSet<Tld> getSupportingTlds(ImmutableSet<String> idnTables) {
|
||||
return idnTables.stream()
|
||||
.map(IdnTableEnum::valueOf)
|
||||
.filter(idnToTlds::containsKey)
|
||||
.map(idnToTlds::get)
|
||||
.flatMap(ImmutableSet::stream)
|
||||
.collect(toImmutableSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the TLDs that do not support any IDN in the {@code idnTables}.
|
||||
*
|
||||
* @param idnTables String names of {@link IdnTableEnum} values
|
||||
*/
|
||||
public SetView<Tld> getForbiddingTlds(ImmutableSet<String> idnTables) {
|
||||
return Sets.difference(allTlds, getSupportingTlds(idnTables));
|
||||
}
|
||||
|
||||
private static ImmutableMap<IdnTableEnum, ImmutableSet<Tld>> getIdnToTldMap(DateTime now) {
|
||||
ImmutableMultimap.Builder<IdnTableEnum, Tld> idnToTldMap = new ImmutableMultimap.Builder();
|
||||
Tlds.getTldEntitiesOfType(TldType.REAL).stream()
|
||||
.filter(tld -> isEnrolledWithBsa(tld, now))
|
||||
.forEach(
|
||||
tld -> {
|
||||
for (IdnTableEnum idn : IDN_LABEL_VALIDATOR.getIdnTablesForTld(tld)) {
|
||||
idnToTldMap.put(idn, tld);
|
||||
}
|
||||
});
|
||||
return ImmutableMap.copyOf(transformValues(idnToTldMap.build().asMap(), ImmutableSet::copyOf));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// 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;
|
||||
|
||||
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
||||
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.Action.Service;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.request.auth.Auth;
|
||||
import javax.inject.Inject;
|
||||
|
||||
@Action(
|
||||
service = Service.BSA,
|
||||
path = PlaceholderAction.PATH,
|
||||
method = Action.Method.GET,
|
||||
auth = Auth.AUTH_API_ADMIN)
|
||||
public class PlaceholderAction implements Runnable {
|
||||
private final Response response;
|
||||
|
||||
static final String PATH = "/_dr/task/bsaDownload";
|
||||
|
||||
@Inject
|
||||
public PlaceholderAction(Response response) {
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
response.setStatus(SC_OK);
|
||||
response.setPayload("Hello World");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.bsa.persistence;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import google.registry.bsa.persistence.BsaDomainInUse.BsaDomainInUseId;
|
||||
import google.registry.model.CreateAutoTimestamp;
|
||||
import google.registry.persistence.VKey;
|
||||
import java.io.Serializable;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.EnumType;
|
||||
import javax.persistence.Enumerated;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.IdClass;
|
||||
|
||||
/** A domain matching a BSA label but is in use (registered or reserved), so cannot be blocked. */
|
||||
@Entity
|
||||
@IdClass(BsaDomainInUseId.class)
|
||||
public class BsaDomainInUse {
|
||||
@Id String label;
|
||||
@Id String tld;
|
||||
|
||||
@Column(nullable = false)
|
||||
@Enumerated(EnumType.STRING)
|
||||
Reason reason;
|
||||
|
||||
/**
|
||||
* Creation time of this record, which is the most recent time when the domain was detected to be
|
||||
* in use wrt BSA. It may be during the processing of a download, or during some other job that
|
||||
* refreshes the state.
|
||||
*
|
||||
* <p>This field is for information only.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
@Column(nullable = false)
|
||||
CreateAutoTimestamp createTime = CreateAutoTimestamp.create(null);
|
||||
|
||||
// For Hibernate
|
||||
BsaDomainInUse() {}
|
||||
|
||||
public BsaDomainInUse(String label, String tld, Reason reason) {
|
||||
this.label = label;
|
||||
this.tld = tld;
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof BsaDomainInUse)) {
|
||||
return false;
|
||||
}
|
||||
BsaDomainInUse that = (BsaDomainInUse) o;
|
||||
return Objects.equal(label, that.label)
|
||||
&& Objects.equal(tld, that.tld)
|
||||
&& reason == that.reason
|
||||
&& Objects.equal(createTime, that.createTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(label, tld, reason, createTime);
|
||||
}
|
||||
|
||||
enum Reason {
|
||||
REGISTERED,
|
||||
RESERVED;
|
||||
}
|
||||
|
||||
static class BsaDomainInUseId implements Serializable {
|
||||
|
||||
private String label;
|
||||
private String tld;
|
||||
|
||||
// For Hibernate
|
||||
BsaDomainInUseId() {}
|
||||
|
||||
BsaDomainInUseId(String label, String tld) {
|
||||
this.label = label;
|
||||
this.tld = tld;
|
||||
}
|
||||
}
|
||||
|
||||
static VKey<BsaDomainInUse> vKey(String label, String tld) {
|
||||
return VKey.create(BsaDomainInUse.class, new BsaDomainInUseId(label, tld));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// 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.collect.ImmutableMap.toImmutableMap;
|
||||
import static google.registry.bsa.DownloadStage.DOWNLOAD;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import google.registry.bsa.BlockList;
|
||||
import google.registry.bsa.DownloadStage;
|
||||
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 javax.persistence.Index;
|
||||
import javax.persistence.Table;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Records of ongoing and completed download jobs. */
|
||||
@Entity
|
||||
@Table(indexes = {@Index(columnList = "creationTime")})
|
||||
public class BsaDownload {
|
||||
|
||||
private static final Joiner CSV_JOINER = Joiner.on(',');
|
||||
private static final Splitter CSV_SPLITTER = Splitter.on(',');
|
||||
|
||||
@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)
|
||||
String blockListChecksums = "";
|
||||
|
||||
@Column(nullable = false)
|
||||
@Enumerated(EnumType.STRING)
|
||||
DownloadStage stage = DOWNLOAD;
|
||||
|
||||
BsaDownload() {}
|
||||
|
||||
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 getCreationTime().toString();
|
||||
}
|
||||
|
||||
public DownloadStage getStage() {
|
||||
return this.stage;
|
||||
}
|
||||
|
||||
BsaDownload setStage(DownloadStage stage) {
|
||||
this.stage = stage;
|
||||
return this;
|
||||
}
|
||||
|
||||
BsaDownload setChecksums(ImmutableMap<BlockList, String> checksums) {
|
||||
blockListChecksums =
|
||||
CSV_JOINER.withKeyValueSeparator("=").join(ImmutableSortedMap.copyOf(checksums));
|
||||
return this;
|
||||
}
|
||||
|
||||
ImmutableMap<BlockList, String> getChecksums() {
|
||||
if (blockListChecksums.isEmpty()) {
|
||||
return ImmutableMap.of();
|
||||
}
|
||||
return CSV_SPLITTER.withKeyValueSeparator('=').split(blockListChecksums).entrySet().stream()
|
||||
.collect(
|
||||
toImmutableMap(entry -> BlockList.valueOf(entry.getKey()), entry -> entry.getValue()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof BsaDownload)) {
|
||||
return false;
|
||||
}
|
||||
BsaDownload that = (BsaDownload) o;
|
||||
return Objects.equal(creationTime, that.creationTime)
|
||||
&& Objects.equal(updateTime, that.updateTime)
|
||||
&& Objects.equal(blockListChecksums, that.blockListChecksums)
|
||||
&& stage == that.stage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(creationTime, updateTime, blockListChecksums, stage);
|
||||
}
|
||||
|
||||
static VKey<BsaDownload> vKey(long jobId) {
|
||||
return VKey.create(BsaDownload.class, Long.valueOf(jobId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// 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 com.google.common.base.Objects;
|
||||
import google.registry.persistence.VKey;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/**
|
||||
* Specifies a second-level TLD name that should be blocked from registration in all TLDs except by
|
||||
* the label's owner.
|
||||
*
|
||||
* <p>The label is valid (wrt IDN) in at least one TLD.
|
||||
*/
|
||||
@Entity
|
||||
final class BsaLabel {
|
||||
|
||||
@Id String label;
|
||||
|
||||
/**
|
||||
* Creation time of this label. This field is for human use, and should give the name of the GCS
|
||||
* folder that contains the downloaded BSA data.
|
||||
*
|
||||
* <p>See {@link BsaDownload#getCreationTime} and {@link BsaDownload#getJobName} for more
|
||||
* information.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
@Column(nullable = false)
|
||||
DateTime creationTime;
|
||||
|
||||
// For Hibernate.
|
||||
BsaLabel() {}
|
||||
|
||||
BsaLabel(String label, DateTime creationTime) {
|
||||
this.label = label;
|
||||
this.creationTime = creationTime;
|
||||
}
|
||||
|
||||
/** Returns the label to be blocked. */
|
||||
String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof BsaLabel)) {
|
||||
return false;
|
||||
}
|
||||
BsaLabel label1 = (BsaLabel) o;
|
||||
return Objects.equal(label, label1.label) && Objects.equal(creationTime, label1.creationTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(label, creationTime);
|
||||
}
|
||||
|
||||
static VKey<BsaLabel> vKey(String label) {
|
||||
return VKey.create(BsaLabel.class, label);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.bsa.persistence;
|
||||
|
||||
import static google.registry.config.RegistryConfig.getEppResourceCachingDuration;
|
||||
import static google.registry.config.RegistryConfig.getEppResourceMaxCachedEntries;
|
||||
import static google.registry.model.CacheUtils.newCacheBuilder;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.CacheLoader;
|
||||
import com.github.benmanes.caffeine.cache.LoadingCache;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import google.registry.persistence.VKey;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Helpers for {@link BsaLabel}. */
|
||||
public final class BsaLabelUtils {
|
||||
|
||||
private BsaLabelUtils() {}
|
||||
|
||||
static final CacheLoader<VKey<BsaLabel>, Optional<BsaLabel>> CACHE_LOADER =
|
||||
new CacheLoader<VKey<BsaLabel>, Optional<BsaLabel>>() {
|
||||
|
||||
@Override
|
||||
public Optional<BsaLabel> load(VKey<BsaLabel> key) {
|
||||
return replicaTm().reTransact(() -> replicaTm().loadByKeyIfPresent(key));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<VKey<BsaLabel>, Optional<BsaLabel>> loadAll(
|
||||
Iterable<? extends VKey<BsaLabel>> keys) {
|
||||
// TODO(b/309173359): need this for DomainCheckFlow
|
||||
throw new UnsupportedOperationException(
|
||||
"LoadAll not supported by the BsaLabel cache loader.");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A limited size, limited expiry cache of BSA labels.
|
||||
*
|
||||
* <p>BSA labels are used by the domain creation flow to verify that the requested domain name is
|
||||
* not blocked by the BSA program. Label caching is mainly a defense against two scenarios, the
|
||||
* initial rush and drop-catching, when clients run back-to-back domain creation requests around
|
||||
* the time when a domain becomes available.
|
||||
*
|
||||
* <p>Because of caching and the use of the replica database, new BSA labels installed in the
|
||||
* database will not take effect immediately. A blocked domain may be created due to race
|
||||
* condition. A `refresh` job will detect such domains and report them to BSA as unblockable
|
||||
* domains.
|
||||
*
|
||||
* <p>Since the cached BSA labels have the same usage pattern as the cached EppResources, the
|
||||
* cache configuration for the latter are reused here.
|
||||
*/
|
||||
private static LoadingCache<VKey<BsaLabel>, Optional<BsaLabel>> cacheBsaLabels =
|
||||
createBsaLabelsCache(getEppResourceCachingDuration());
|
||||
|
||||
private static LoadingCache<VKey<BsaLabel>, Optional<BsaLabel>> createBsaLabelsCache(
|
||||
Duration expiry) {
|
||||
return newCacheBuilder(expiry)
|
||||
.maximumSize(getEppResourceMaxCachedEntries())
|
||||
.build(CACHE_LOADER);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void clearCache() {
|
||||
cacheBsaLabels.invalidateAll();
|
||||
}
|
||||
|
||||
/** Checks if the {@code domainLabel} (the leading `part` of a domain name) is blocked by BSA. */
|
||||
public static boolean isLabelBlocked(String domainLabel) {
|
||||
return cacheBsaLabels.get(BsaLabel.vKey(domainLabel)).isPresent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// 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 com.google.auto.value.AutoValue;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import google.registry.bsa.BlockList;
|
||||
import google.registry.bsa.DownloadStage;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Information needed when handling a download from BSA. */
|
||||
@AutoValue
|
||||
public abstract class DownloadSchedule {
|
||||
|
||||
abstract long jobId();
|
||||
|
||||
public abstract String jobName();
|
||||
|
||||
public abstract DownloadStage stage();
|
||||
|
||||
/** The most recent job that ended in the {@code DONE} stage. */
|
||||
public abstract Optional<CompletedJob> latestCompleted();
|
||||
|
||||
/**
|
||||
* Returns true if download should be processed even if the checksums show that it has not changed
|
||||
* from the previous one.
|
||||
*/
|
||||
abstract boolean alwaysDownload();
|
||||
|
||||
static DownloadSchedule of(BsaDownload currentJob) {
|
||||
return new AutoValue_DownloadSchedule(
|
||||
currentJob.getJobId(),
|
||||
currentJob.getJobName(),
|
||||
currentJob.getStage(),
|
||||
Optional.empty(),
|
||||
/* alwaysDownload= */ true);
|
||||
}
|
||||
|
||||
static DownloadSchedule of(
|
||||
BsaDownload currentJob, CompletedJob latestCompleted, boolean alwaysDownload) {
|
||||
return new AutoValue_DownloadSchedule(
|
||||
currentJob.getJobId(),
|
||||
currentJob.getJobName(),
|
||||
currentJob.getStage(),
|
||||
Optional.of(latestCompleted),
|
||||
/* alwaysDownload= */ alwaysDownload);
|
||||
}
|
||||
|
||||
/** Information about a completed BSA download job. */
|
||||
@AutoValue
|
||||
public abstract static class CompletedJob {
|
||||
public abstract String jobName();
|
||||
|
||||
public abstract ImmutableMap<BlockList, String> checksums();
|
||||
|
||||
static CompletedJob of(BsaDownload completedJob) {
|
||||
return new AutoValue_DownloadSchedule_CompletedJob(
|
||||
completedJob.getJobName(), completedJob.getChecksums());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// 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.base.Verify.verify;
|
||||
import static google.registry.bsa.DownloadStage.CHECKSUMS_NOT_MATCH;
|
||||
import static google.registry.bsa.DownloadStage.DONE;
|
||||
import static google.registry.bsa.DownloadStage.NOP;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static org.joda.time.Duration.standardSeconds;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import google.registry.bsa.persistence.DownloadSchedule.CompletedJob;
|
||||
import google.registry.util.Clock;
|
||||
import java.util.Optional;
|
||||
import javax.inject.Inject;
|
||||
import org.joda.time.Duration;
|
||||
|
||||
/**
|
||||
* Assigns work for each cron invocation of the BSA Download job.
|
||||
*
|
||||
* <p>The download job is invoked at a divisible fraction of the desired data freshness to
|
||||
* accommodate potential retries. E.g., for 30-minute data freshness with up to two retries on
|
||||
* error, the cron schedule for the job should be set to 10 minutes.
|
||||
*
|
||||
* <p>The processing of each BSA download progresses through multiple stages as described in {@code
|
||||
* DownloadStage} until it reaches one of the terminal stages. Each stage is check-pointed on
|
||||
* completion, therefore if an invocation fails mid-process, the next invocation will skip the
|
||||
* completed stages. No new downloads will start as long as the most recent one is still being
|
||||
* processed.
|
||||
*
|
||||
* <p>When a new download is scheduled, the block list checksums from the most recent completed job
|
||||
* is included. If the new checksums match the previous ones, the download may be skipped and the
|
||||
* job should terminate in the {@code NOP} stage. However, if the checksums have stayed unchanged
|
||||
* for longer than the user-provided {@code maxNopInterval}, the download will be processed.
|
||||
*
|
||||
* <p>The BSA downloads contains server-provided checksums. If they do not match the checksums
|
||||
* generated on Nomulus' side, the download is skipped and the job should terminate in the {@code
|
||||
* CHECKSUMS_NOT_MATCH} stage.
|
||||
*/
|
||||
public final class DownloadScheduler {
|
||||
|
||||
/** Allows a new download to proceed if the cron job fires a little early due to NTP drift. */
|
||||
private static final Duration CRON_JITTER = standardSeconds(5);
|
||||
|
||||
private final Duration downloadInterval;
|
||||
private final Duration maxNopInterval;
|
||||
private final Clock clock;
|
||||
|
||||
@Inject
|
||||
DownloadScheduler(Duration downloadInterval, Duration maxNopInterval, Clock clock) {
|
||||
this.downloadInterval = downloadInterval;
|
||||
this.maxNopInterval = maxNopInterval;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link DownloadSchedule} instance that describes the work to be performed by an
|
||||
* invocation of the download action, if applicable; or {@link Optional#empty} when there is
|
||||
* nothing to do.
|
||||
*/
|
||||
public Optional<DownloadSchedule> schedule() {
|
||||
return tm().transact(
|
||||
() -> {
|
||||
ImmutableList<BsaDownload> recentJobs = loadRecentProcessedJobs();
|
||||
if (recentJobs.isEmpty()) {
|
||||
// No jobs initiated ever.
|
||||
return Optional.of(scheduleNewJob(Optional.empty()));
|
||||
}
|
||||
BsaDownload mostRecent = recentJobs.get(0);
|
||||
if (mostRecent.getStage().equals(DONE)) {
|
||||
return isTimeAgain(mostRecent, downloadInterval)
|
||||
? Optional.of(scheduleNewJob(Optional.of(mostRecent)))
|
||||
: Optional.empty();
|
||||
} else if (recentJobs.size() == 1) {
|
||||
// First job ever, still in progress
|
||||
return Optional.of(DownloadSchedule.of(recentJobs.get(0)));
|
||||
} else {
|
||||
// Job in progress, with completed previous jobs.
|
||||
BsaDownload prev = recentJobs.get(1);
|
||||
verify(prev.getStage().equals(DONE), "Unexpectedly found two ongoing jobs.");
|
||||
return Optional.of(
|
||||
DownloadSchedule.of(
|
||||
mostRecent,
|
||||
CompletedJob.of(prev),
|
||||
isTimeAgain(mostRecent, maxNopInterval)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isTimeAgain(BsaDownload mostRecent, Duration interval) {
|
||||
return mostRecent.getCreationTime().plus(interval).minus(CRON_JITTER).isBefore(clock.nowUtc());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new {@link BsaDownload} to the database and returns a {@link DownloadSchedule} for it.
|
||||
*/
|
||||
private DownloadSchedule scheduleNewJob(Optional<BsaDownload> prevJob) {
|
||||
BsaDownload job = new BsaDownload();
|
||||
tm().insert(job);
|
||||
return prevJob
|
||||
.map(
|
||||
prev ->
|
||||
DownloadSchedule.of(job, CompletedJob.of(prev), isTimeAgain(prev, maxNopInterval)))
|
||||
.orElseGet(() -> DownloadSchedule.of(job));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
ImmutableList<BsaDownload> loadRecentProcessedJobs() {
|
||||
return ImmutableList.copyOf(
|
||||
tm().getEntityManager()
|
||||
.createQuery(
|
||||
"FROM BsaDownload WHERE stage NOT IN :nop_stages ORDER BY creationTime DESC")
|
||||
.setParameter("nop_stages", ImmutableList.of(CHECKSUMS_NOT_MATCH, NOP))
|
||||
.setMaxResults(2)
|
||||
.getResultList());
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import com.google.common.base.Ascii;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import dagger.Module;
|
||||
@@ -35,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;
|
||||
@@ -1191,6 +1193,12 @@ public final class RegistryConfig {
|
||||
return config.auth.oauthClientId;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Config("fallbackOauthClientId")
|
||||
public static String provideFallbackOauthClientId(RegistryConfigSettings config) {
|
||||
return config.auth.fallbackOauthClientId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the OAuth scopes required for accessing Google APIs using the default credential.
|
||||
*/
|
||||
@@ -1390,6 +1398,86 @@ 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) {
|
||||
return config.bsa.authUrl;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Config("bsaAuthTokenExpiry")
|
||||
public static Duration provideBsaAuthTokenExpiry(RegistryConfigSettings config) {
|
||||
return Duration.standardSeconds(config.bsa.authTokenExpirySeconds);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Config("bsaDataUrls")
|
||||
public static ImmutableMap<String, String> provideBsaDataUrls(RegistryConfigSettings config) {
|
||||
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)
|
||||
@@ -1433,6 +1521,15 @@ public final class RegistryConfig {
|
||||
return makeUrl(CONFIG_SETTINGS.get().gcpProject.backendServiceUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the address of the Nomulus app bsa HTTP server.
|
||||
*
|
||||
* <p>This is used by the {@code nomulus} tool to connect to the App Engine remote API.
|
||||
*/
|
||||
public static URL getBsaServer() {
|
||||
return makeUrl(CONFIG_SETTINGS.get().gcpProject.bsaServiceUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the address of the Nomulus app tools HTTP server.
|
||||
*
|
||||
@@ -1529,9 +1626,9 @@ public final class RegistryConfig {
|
||||
return CONFIG_SETTINGS.get().hibernate.connectionIsolation;
|
||||
}
|
||||
|
||||
/** Returns true if per-transaction isolation level is enabled. */
|
||||
public static boolean getHibernatePerTransactionIsolationEnabled() {
|
||||
return CONFIG_SETTINGS.get().hibernate.perTransactionIsolation;
|
||||
/** Returns true if nested calls to {@code tm().transact()} are allowed. */
|
||||
public static boolean getHibernateAllowNestedTransactions() {
|
||||
return CONFIG_SETTINGS.get().hibernate.allowNestedTransactions;
|
||||
}
|
||||
|
||||
/** Returns true if hibernate.show_sql is enabled. */
|
||||
|
||||
@@ -43,6 +43,7 @@ public class RegistryConfigSettings {
|
||||
public ContactHistory contactHistory;
|
||||
public DnsUpdate dnsUpdate;
|
||||
public BulkPricingPackageMonitoring bulkPricingPackageMonitoring;
|
||||
public Bsa bsa;
|
||||
|
||||
/** Configuration options that apply to the entire GCP project. */
|
||||
public static class GcpProject {
|
||||
@@ -52,6 +53,7 @@ public class RegistryConfigSettings {
|
||||
public boolean isLocal;
|
||||
public String defaultServiceUrl;
|
||||
public String backendServiceUrl;
|
||||
public String bsaServiceUrl;
|
||||
public String toolsServiceUrl;
|
||||
public String pubapiServiceUrl;
|
||||
}
|
||||
@@ -60,6 +62,7 @@ public class RegistryConfigSettings {
|
||||
public static class Auth {
|
||||
public List<String> allowedServiceAccountEmails;
|
||||
public String oauthClientId;
|
||||
public String fallbackOauthClientId;
|
||||
}
|
||||
|
||||
/** Configuration options for accessing Google APIs. */
|
||||
@@ -112,7 +115,7 @@ public class RegistryConfigSettings {
|
||||
|
||||
/** Configuration for Hibernate. */
|
||||
public static class Hibernate {
|
||||
public boolean perTransactionIsolation;
|
||||
public boolean allowNestedTransactions;
|
||||
public String connectionIsolation;
|
||||
public String logSqlQueries;
|
||||
public String hikariConnectionTimeout;
|
||||
@@ -261,4 +264,18 @@ public class RegistryConfigSettings {
|
||||
public String bulkPricingPackageDomainLimitUpgradeEmailSubject;
|
||||
public String bulkPricingPackageDomainLimitUpgradeEmailBody;
|
||||
}
|
||||
|
||||
/** 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,11 @@ gcpProject:
|
||||
# URLs of the services for the project.
|
||||
defaultServiceUrl: https://default.example.com
|
||||
backendServiceUrl: https://backend.example.com
|
||||
bsaServiceUrl: https://bsa.example.com
|
||||
toolsServiceUrl: https://tools.example.com
|
||||
pubapiServiceUrl: https://pubapi.example.com
|
||||
|
||||
|
||||
gSuite:
|
||||
# Publicly accessible domain name of the running G Suite instance.
|
||||
domainName: domain-registry.example
|
||||
@@ -189,11 +191,13 @@ registryPolicy:
|
||||
sunriseDomainCreateDiscount: 0.15
|
||||
|
||||
hibernate:
|
||||
# Make it possible to specify the isolation level for each transaction. If set
|
||||
# to true, nested transactions will throw an exception. If set to false, a
|
||||
# transaction with the isolation override specified will still execute at the
|
||||
# default level (specified below).
|
||||
perTransactionIsolation: true
|
||||
# If set to false, calls to tm().transact() cannot be nested. If set to true,
|
||||
# nested calls to tm().transact() are allowed, as long as they do not specify
|
||||
# a transaction isolation level override. These nested transactions should
|
||||
# either be refactored to non-nested transactions, or changed to
|
||||
# tm().reTransact(), which explicitly allows nested transactions, but does not
|
||||
# allow setting an isolation level override.
|
||||
allowNestedTransactions: true
|
||||
|
||||
# Make 'SERIALIZABLE' the default isolation level to ensure correctness.
|
||||
#
|
||||
@@ -319,6 +323,10 @@ auth:
|
||||
# the same as this one.
|
||||
oauthClientId: iap-oauth-clientid
|
||||
|
||||
# Same as above, but serve as a fallback, so we can switch the client ID of
|
||||
# the proxy without downtime.
|
||||
fallbackOauthClientId: fallback-oauth-clientid
|
||||
|
||||
credentialOAuth:
|
||||
# OAuth scopes required for accessing Google APIs using the default
|
||||
# credential.
|
||||
@@ -598,3 +606,18 @@ bulkPricingPackageMonitoring:
|
||||
Registrar: %3$s
|
||||
Active Domain Limit: %4$s
|
||||
Current Active Domains: %5$s
|
||||
|
||||
# Configurations for integration with Brand Safety Alliance (BSA) API
|
||||
bsa:
|
||||
# Http endpoint for acquiring Auth tokens.
|
||||
authUrl: "https://"
|
||||
# Auth token expiry.
|
||||
authTokenExpirySeconds: 1800
|
||||
# Http endpoints for downloading data
|
||||
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://"
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>backend</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
31
core/src/main/java/google/registry/env/alpha/bsa/WEB-INF/appengine-web.xml
vendored
Normal file
31
core/src/main/java/google/registry/env/alpha/bsa/WEB-INF/appengine-web.xml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java17</runtime>
|
||||
<service>bsa</service>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
<max-instances>100</max-instances>
|
||||
<idle-timeout>10m</idle-timeout>
|
||||
</basic-scaling>
|
||||
|
||||
<system-properties>
|
||||
<property name="java.util.logging.config.file"
|
||||
value="WEB-INF/logging.properties"/>
|
||||
<property name="google.registry.environment"
|
||||
value="alpha"/>
|
||||
</system-properties>
|
||||
|
||||
|
||||
<!-- Enable external traffic to go through VPC, required for static ip -->
|
||||
<vpc-access-connector>
|
||||
<name>projects/domain-registry-alpha/locations/us-central1/connectors/appengine-connector</name>
|
||||
<egress-setting>all-traffic</egress-setting>
|
||||
</vpc-access-connector>
|
||||
|
||||
<static-files>
|
||||
<include path="/*.html" expiration="1m"/>
|
||||
</static-files>
|
||||
</appengine-web-app>
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>default</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>pubapi</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>tools</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -31,6 +31,12 @@ encoding="UTF-8"?>
|
||||
<context-root>backend</context-root>
|
||||
</web>
|
||||
</module>
|
||||
<module>
|
||||
<web>
|
||||
<web-uri>bsa</web-uri>
|
||||
<context-root>bsa</context-root>
|
||||
</web>
|
||||
</module>
|
||||
<module>
|
||||
<web>
|
||||
<web-uri>tools</web-uri>
|
||||
|
||||
17
core/src/main/java/google/registry/env/common/bsa/WEB-INF/logging.properties
vendored
Normal file
17
core/src/main/java/google/registry/env/common/bsa/WEB-INF/logging.properties
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# A default java.util.logging configuration.
|
||||
# (All App Engine logging is through java.util.logging by default).
|
||||
#
|
||||
# To use this configuration, copy it into your application's WEB-INF
|
||||
# folder and add the following to your appengine-web.xml:
|
||||
#
|
||||
# <system-properties>
|
||||
# <property name="java.util.logging.config.file" value="WEB-INF/logging.properties"/>
|
||||
# </system-properties>
|
||||
#
|
||||
|
||||
# Set the default logging level for all loggers to INFO.
|
||||
.level = INFO
|
||||
|
||||
# Turn off logging in Hibernate classes for misleading ERROR-level logs
|
||||
org.hibernate.engine.jdbc.batch.internal.BatchingBatch.level=OFF
|
||||
org.hibernate.engine.jdbc.spi.SqlExceptionHelper.level=OFF
|
||||
70
core/src/main/java/google/registry/env/common/bsa/WEB-INF/web.xml
vendored
Normal file
70
core/src/main/java/google/registry/env/common/bsa/WEB-INF/web.xml
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<web-app xmlns="http://java.sun.com/xml/ns/javaee" version="2.5"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
|
||||
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
|
||||
<!-- Servlets -->
|
||||
|
||||
<!-- Servlet for injected backends actions -->
|
||||
<servlet>
|
||||
<display-name>BsaServlet</display-name>
|
||||
<servlet-name>bsa-servlet</servlet-name>
|
||||
<servlet-class>google.registry.module.bsa.BsaServlet</servlet-class>
|
||||
<load-on-startup>1</load-on-startup>
|
||||
</servlet>
|
||||
|
||||
<!-- Test action -->
|
||||
<servlet-mapping>
|
||||
<servlet-name>bsa-servlet</servlet-name>
|
||||
<url-pattern>/_dr/task/bsaDownload</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<!-- Security config -->
|
||||
<security-constraint>
|
||||
<web-resource-collection>
|
||||
<web-resource-name>Internal</web-resource-name>
|
||||
<description>
|
||||
Admin-only internal section. Requests for paths covered by the URL patterns below will be
|
||||
checked for a logged-in user account that's allowed to access the AppEngine admin console
|
||||
(NOTE: this includes Editor/Viewer permissions in addition to Owner and the new IAM
|
||||
App Engine Admin role. See https://cloud.google.com/appengine/docs/java/access-control
|
||||
specifically the "Access handlers that have a login:admin restriction" line.)
|
||||
|
||||
TODO(b/28219927): lift some of these restrictions so that we can allow OAuth authentication
|
||||
for endpoints that need to be accessed by open-source automated processes.
|
||||
</description>
|
||||
|
||||
<!-- Internal AppEngine endpoints. The '_ah' is short for app hosting. -->
|
||||
<url-pattern>/_ah/*</url-pattern>
|
||||
|
||||
<!-- Registrar console (should not be available on non-default module). -->
|
||||
<url-pattern>/registrar*</url-pattern>
|
||||
|
||||
<!-- Verbatim JavaScript sources (only visible to admins for debugging). -->
|
||||
<url-pattern>/assets/sources/*</url-pattern>
|
||||
|
||||
</web-resource-collection>
|
||||
<auth-constraint>
|
||||
<role-name>admin</role-name>
|
||||
</auth-constraint>
|
||||
|
||||
<!-- Repeated here since catch-all rule below is not inherited. -->
|
||||
<user-data-constraint>
|
||||
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
|
||||
</user-data-constraint>
|
||||
</security-constraint>
|
||||
|
||||
<!-- Require TLS on all requests. -->
|
||||
<security-constraint>
|
||||
<web-resource-collection>
|
||||
<web-resource-name>Secure</web-resource-name>
|
||||
<description>
|
||||
Require encryption for all paths. http URLs will be redirected to https.
|
||||
</description>
|
||||
<url-pattern>/*</url-pattern>
|
||||
</web-resource-collection>
|
||||
<user-data-constraint>
|
||||
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
|
||||
</user-data-constraint>
|
||||
</security-constraint>
|
||||
</web-app>
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>backend</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
30
core/src/main/java/google/registry/env/crash/bsa/WEB-INF/appengine-web.xml
vendored
Normal file
30
core/src/main/java/google/registry/env/crash/bsa/WEB-INF/appengine-web.xml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java17</runtime>
|
||||
<service>bsa</service>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
<max-instances>10</max-instances>
|
||||
<idle-timeout>10m</idle-timeout>
|
||||
</basic-scaling>
|
||||
|
||||
<system-properties>
|
||||
<property name="java.util.logging.config.file"
|
||||
value="WEB-INF/logging.properties"/>
|
||||
<property name="google.registry.environment"
|
||||
value="crash"/>
|
||||
</system-properties>
|
||||
|
||||
<!-- Enable external traffic to go through VPC, required for static ip -->
|
||||
<vpc-access-connector>
|
||||
<name>projects/domain-registry-crash/locations/us-central1/connectors/appengine-connector</name>
|
||||
<egress-setting>all-traffic</egress-setting>
|
||||
</vpc-access-connector>
|
||||
|
||||
<static-files>
|
||||
<include path="/*.html" expiration="1m"/>
|
||||
</static-files>
|
||||
</appengine-web-app>
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>default</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4_1G</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>pubapi</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>tools</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>backend</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
28
core/src/main/java/google/registry/env/local/bsa/WEB-INF/appengine-web.xml
vendored
Normal file
28
core/src/main/java/google/registry/env/local/bsa/WEB-INF/appengine-web.xml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java17</runtime>
|
||||
<service>bsa</service>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
<max-instances>10</max-instances>
|
||||
<idle-timeout>10m</idle-timeout>
|
||||
</basic-scaling>
|
||||
|
||||
<system-properties>
|
||||
<property name="java.util.logging.config.file"
|
||||
value="WEB-INF/logging.properties"/>
|
||||
<property name="google.registry.environment"
|
||||
value="local"/>
|
||||
<property name="appengine.generated.dir"
|
||||
value="/tmp/domain-registry-appengine-generated/local/"/>
|
||||
</system-properties>
|
||||
|
||||
<static-files>
|
||||
<include path="/*.html">
|
||||
<http-header name="Cache-Control" value="max-age=0,must-revalidate" />
|
||||
</include>
|
||||
</static-files>
|
||||
</appengine-web-app>
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>default</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4_1G</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>pubapi</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>tools</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<service>backend</service>
|
||||
<!--app-engine-apis>true</app-engine-apis-->
|
||||
<threadsafe>true</threadsafe>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4_1G</instance-class>
|
||||
|
||||
36
core/src/main/java/google/registry/env/production/bsa/WEB-INF/appengine-web.xml
vendored
Normal file
36
core/src/main/java/google/registry/env/production/bsa/WEB-INF/appengine-web.xml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<service>bsa</service>
|
||||
<!--app-engine-apis>true</app-engine-apis-->
|
||||
<threadsafe>true</threadsafe>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4_1G</instance-class>
|
||||
<basic-scaling>
|
||||
<max-instances>100</max-instances>
|
||||
<idle-timeout>10m</idle-timeout>
|
||||
</basic-scaling>
|
||||
|
||||
<system-properties>
|
||||
<property name="java.util.logging.config.file"
|
||||
value="WEB-INF/logging.properties"/>
|
||||
<property name="google.registry.environment"
|
||||
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>
|
||||
|
||||
<!-- Prevent uncaught servlet errors from leaking a stack trace. -->
|
||||
<static-error-handlers>
|
||||
<handler file="error.html"/>
|
||||
</static-error-handlers>
|
||||
</appengine-web-app>
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<service>default</service>
|
||||
<!--app-engine-apis>true</app-engine-apis-->
|
||||
<threadsafe>true</threadsafe>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4_1G</instance-class>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<service>pubapi</service>
|
||||
<!--app-engine-apis>true</app-engine-apis-->
|
||||
<threadsafe>true</threadsafe>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4_1G</instance-class>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<service>tools</service>
|
||||
<!--app-engine-apis>true</app-engine-apis-->
|
||||
<threadsafe>true</threadsafe>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4_1G</instance-class>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>backend</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
35
core/src/main/java/google/registry/env/qa/bsa/WEB-INF/appengine-web.xml
vendored
Normal file
35
core/src/main/java/google/registry/env/qa/bsa/WEB-INF/appengine-web.xml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java17</runtime>
|
||||
<service>bsa</service>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
<max-instances>10</max-instances>
|
||||
<idle-timeout>10m</idle-timeout>
|
||||
</basic-scaling>
|
||||
|
||||
<system-properties>
|
||||
<property name="java.util.logging.config.file"
|
||||
value="WEB-INF/logging.properties"/>
|
||||
<property name="google.registry.environment"
|
||||
value="qa"/>
|
||||
</system-properties>
|
||||
|
||||
<static-files>
|
||||
<include path="/*.html" expiration="1h"/>
|
||||
</static-files>
|
||||
|
||||
<!-- Enable external traffic to go through VPC, required for static ip -->
|
||||
<vpc-access-connector>
|
||||
<name>projects/domain-registry-qa/locations/us-central1/connectors/appengine-connector</name>
|
||||
<egress-setting>all-traffic</egress-setting>
|
||||
</vpc-access-connector>
|
||||
|
||||
<!-- Prevent uncaught servlet errors from leaking a stack trace. -->
|
||||
<static-error-handlers>
|
||||
<handler file="error.html"/>
|
||||
</static-error-handlers>
|
||||
</appengine-web-app>
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>default</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>F4_1G</instance-class>
|
||||
<automatic-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>pubapi</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<runtime>java17</runtime>
|
||||
<service>tools</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<service>backend</service>
|
||||
<!--app-engine-apis>true</app-engine-apis-->
|
||||
<threadsafe>true</threadsafe>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
|
||||
36
core/src/main/java/google/registry/env/sandbox/bsa/WEB-INF/appengine-web.xml
vendored
Normal file
36
core/src/main/java/google/registry/env/sandbox/bsa/WEB-INF/appengine-web.xml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<service>bsa</service>
|
||||
<!--app-engine-apis>true</app-engine-apis-->
|
||||
<threadsafe>true</threadsafe>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
<basic-scaling>
|
||||
<max-instances>100</max-instances>
|
||||
<idle-timeout>10m</idle-timeout>
|
||||
</basic-scaling>
|
||||
|
||||
<system-properties>
|
||||
<property name="java.util.logging.config.file"
|
||||
value="WEB-INF/logging.properties"/>
|
||||
<property name="google.registry.environment"
|
||||
value="sandbox"/>
|
||||
</system-properties>
|
||||
|
||||
<static-files>
|
||||
<include path="/*.html" expiration="1d"/>
|
||||
</static-files>
|
||||
|
||||
<!-- Enable external traffic to go through VPC, required for static ip -->
|
||||
<vpc-access-connector>
|
||||
<name>projects/domain-registry-sandbox/locations/us-central1/connectors/appengine-connector</name>
|
||||
<egress-setting>all-traffic</egress-setting>
|
||||
</vpc-access-connector>
|
||||
|
||||
<!-- Prevent uncaught servlet errors from leaking a stack trace. -->
|
||||
<static-error-handlers>
|
||||
<handler file="error.html"/>
|
||||
</static-error-handlers>
|
||||
</appengine-web-app>
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<service>default</service>
|
||||
<!--app-engine-apis>true</app-engine-apis-->
|
||||
<threadsafe>true</threadsafe>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4_1G</instance-class>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<service>pubapi</service>
|
||||
<!--app-engine-apis>true</app-engine-apis-->
|
||||
<threadsafe>true</threadsafe>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4_1G</instance-class>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
<runtime>java8</runtime>
|
||||
<service>tools</service>
|
||||
<!--app-engine-apis>true</app-engine-apis-->
|
||||
<threadsafe>true</threadsafe>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4</instance-class>
|
||||
|
||||
@@ -75,14 +75,6 @@ public class EppRequestHandler {
|
||||
&& eppOutput.getResponse().getResult().getCode() == SUCCESS_AND_CLOSE) {
|
||||
response.setHeader(ProxyHttpHeaders.EPP_SESSION, "close");
|
||||
}
|
||||
// If a login request returns a success, a logged-in header is added to the response to inform
|
||||
// the proxy that it is no longer necessary to send the full client certificate to the backend
|
||||
// for this connection.
|
||||
if (eppOutput.isResponse()
|
||||
&& eppOutput.getResponse().isLoginResponse()
|
||||
&& eppOutput.isSuccess()) {
|
||||
response.setHeader(ProxyHttpHeaders.LOGGED_IN, "true");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.atWarning().withCause(e).log("handleEppCommand general exception.");
|
||||
response.setStatus(SC_BAD_REQUEST);
|
||||
|
||||
@@ -33,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;
|
||||
@@ -66,11 +67,11 @@ public class TlsCredentials implements TransportCredentials {
|
||||
public TlsCredentials(
|
||||
@Config("requireSslCertificates") boolean requireSslCertificates,
|
||||
@Header(ProxyHttpHeaders.CERTIFICATE_HASH) Optional<String> clientCertificateHash,
|
||||
@Header(ProxyHttpHeaders.IP_ADDRESS) Optional<String> clientAddress,
|
||||
Optional<InetAddress> clientInetAddr,
|
||||
CertificateChecker certificateChecker) {
|
||||
this.requireSslCertificates = requireSslCertificates;
|
||||
this.clientCertificateHash = clientCertificateHash;
|
||||
this.clientInetAddr = clientAddress.map(TlsCredentials::parseInetAddress);
|
||||
this.clientInetAddr = clientInetAddr;
|
||||
this.certificateChecker = certificateChecker;
|
||||
}
|
||||
|
||||
@@ -104,18 +105,25 @@ public class TlsCredentials implements TransportCredentials {
|
||||
}
|
||||
// In the rare unexpected case that the client inet address wasn't passed along at all, then
|
||||
// by default deny access.
|
||||
if (clientInetAddr.isPresent()) {
|
||||
for (CidrAddressBlock cidrAddressBlock : ipAddressAllowList) {
|
||||
if (cidrAddressBlock.contains(clientInetAddr.get())) {
|
||||
// IP address is in allow list; return early.
|
||||
return;
|
||||
}
|
||||
if (!clientInetAddr.isPresent()) {
|
||||
logger.atWarning().log(
|
||||
"Authentication error: Missing IP address for registrar %s.", registrar.getRegistrarId());
|
||||
throw new BadRegistrarIpAddressException(clientInetAddr);
|
||||
}
|
||||
for (CidrAddressBlock cidrAddressBlock : ipAddressAllowList) {
|
||||
if (cidrAddressBlock.contains(clientInetAddr.get())) {
|
||||
// IP address is in allow list; return early.
|
||||
return;
|
||||
}
|
||||
}
|
||||
logger.atInfo().log(
|
||||
logger.atWarning().log(
|
||||
"Authentication error: IP address %s is not allow-listed for registrar %s; allow list is:"
|
||||
+ " %s",
|
||||
clientInetAddr, registrar.getRegistrarId(), ipAddressAllowList);
|
||||
clientInetAddr,
|
||||
registrar.getRegistrarId(),
|
||||
RegistryEnvironment.get() == RegistryEnvironment.PRODUCTION
|
||||
? "redacted in production"
|
||||
: ipAddressAllowList);
|
||||
throw new BadRegistrarIpAddressException(clientInetAddr);
|
||||
}
|
||||
|
||||
@@ -232,7 +240,7 @@ public class TlsCredentials implements TransportCredentials {
|
||||
? String.format(
|
||||
"Registrar IP address %s is not in stored allow list",
|
||||
clientInetAddr.get().getHostAddress())
|
||||
: "Registrar IP address is not in stored allow list");
|
||||
: "Registrar IP address is missing");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,9 +257,14 @@ public class TlsCredentials implements TransportCredentials {
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Header(ProxyHttpHeaders.IP_ADDRESS)
|
||||
static Optional<String> provideIpAddress(HttpServletRequest req) {
|
||||
return extractOptionalHeader(req, ProxyHttpHeaders.IP_ADDRESS);
|
||||
static Optional<InetAddress> provideIpAddress(HttpServletRequest req) {
|
||||
Optional<String> clientAddress = extractOptionalHeader(req, ProxyHttpHeaders.IP_ADDRESS);
|
||||
Optional<String> fallbackClientAddress =
|
||||
extractOptionalHeader(req, ProxyHttpHeaders.IP_ADDRESS);
|
||||
Optional<InetAddress> clientInetAddr = clientAddress.map(TlsCredentials::parseInetAddress);
|
||||
return clientInetAddr.isPresent()
|
||||
? clientInetAddr
|
||||
: fallbackClientAddress.map(TlsCredentials::parseInetAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,6 @@ import google.registry.model.domain.DomainCommand.Check;
|
||||
import google.registry.model.domain.fee.FeeCheckCommandExtension;
|
||||
import google.registry.model.domain.fee.FeeCheckCommandExtensionItem;
|
||||
import google.registry.model.domain.fee.FeeCheckResponseExtensionItem;
|
||||
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName;
|
||||
import google.registry.model.domain.fee06.FeeCheckCommandExtensionV06;
|
||||
import google.registry.model.domain.launch.LaunchCheckExtension;
|
||||
import google.registry.model.domain.token.AllocationToken;
|
||||
@@ -272,7 +271,7 @@ public final class DomainCheckFlow implements TransactionalFlow {
|
||||
ImmutableList.Builder<FeeCheckResponseExtensionItem> responseItems =
|
||||
new ImmutableList.Builder<>();
|
||||
ImmutableMap<String, Domain> domainObjs =
|
||||
loadDomainsForRestoreChecks(feeCheck, domainNames, existingDomains);
|
||||
loadDomainsForChecks(feeCheck, domainNames, existingDomains);
|
||||
ImmutableMap<String, BillingRecurrence> recurrences = loadRecurrencesForDomains(domainObjs);
|
||||
|
||||
for (FeeCheckCommandExtensionItem feeCheckItem : feeCheck.getItems()) {
|
||||
@@ -335,17 +334,20 @@ public final class DomainCheckFlow implements TransactionalFlow {
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and returns all existing domains that are having restore fees checked.
|
||||
* Loads and returns all existing domains that are having restore/renew/transfer fees checked.
|
||||
*
|
||||
* <p>This is necessary so that we can check their expiration dates to determine if a one-year
|
||||
* renewal is part of the cost of a restore.
|
||||
* <p>These need to be loaded for renews and transfers because there could be a relevant {@link
|
||||
* google.registry.model.billing.BillingBase.RenewalPriceBehavior} on the {@link
|
||||
* BillingRecurrence} affecting the price. They also need to be loaded for restores so that we can
|
||||
* check their expiration dates to determine if a one-year renewal is part of the cost of a
|
||||
* restore.
|
||||
*
|
||||
* <p>This may be resource-intensive for large checks of many restore fees, but those are
|
||||
* comparatively rare, and we are at least using an in-memory cache. Also, this will get a lot
|
||||
* nicer in Cloud SQL when we can SELECT just the fields we want rather than having to load the
|
||||
* entire entity.
|
||||
*/
|
||||
private ImmutableMap<String, Domain> loadDomainsForRestoreChecks(
|
||||
private ImmutableMap<String, Domain> loadDomainsForChecks(
|
||||
FeeCheckCommandExtension<?, ?> feeCheck,
|
||||
ImmutableMap<String, InternetDomainName> domainNames,
|
||||
ImmutableMap<String, VKey<Domain>> existingDomains) {
|
||||
@@ -354,18 +356,18 @@ public final class DomainCheckFlow implements TransactionalFlow {
|
||||
// The V06 fee extension supports specifying the command fees to check on a per-domain basis.
|
||||
restoreCheckDomains =
|
||||
feeCheck.getItems().stream()
|
||||
.filter(fc -> fc.getCommandName() == CommandName.RESTORE)
|
||||
.filter(fc -> fc.getCommandName().shouldLoadDomainForCheck())
|
||||
.map(FeeCheckCommandExtensionItem::getDomainName)
|
||||
.distinct()
|
||||
.collect(toImmutableList());
|
||||
} else if (feeCheck.getItems().stream()
|
||||
.anyMatch(fc -> fc.getCommandName() == CommandName.RESTORE)) {
|
||||
.anyMatch(fc -> fc.getCommandName().shouldLoadDomainForCheck())) {
|
||||
// The more recent fee extension versions support specifying the command fees to check only on
|
||||
// the overall domain check, not per-domain.
|
||||
restoreCheckDomains = ImmutableList.copyOf(domainNames.keySet());
|
||||
} else {
|
||||
// Fall-through case for more recent fee extension versions when the restore fee isn't being
|
||||
// checked.
|
||||
// Fall-through case for more recent fee extension versions when the restore/renew/transfer
|
||||
// fees aren't being checked.
|
||||
restoreCheckDomains = ImmutableList.of();
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user