mirror of
https://github.com/google/nomulus
synced 2026-05-18 22:01:47 +00:00
Compare commits
48 Commits
proxy-2023
...
nomulus-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e647d4e215 | ||
|
|
08471242df | ||
|
|
cd23fea698 | ||
|
|
ba54208dad | ||
|
|
b5e131ecba | ||
|
|
87e99f59bc | ||
|
|
30accea383 | ||
|
|
72e0101746 | ||
|
|
3090df9a78 | ||
|
|
7332b1fa38 | ||
|
|
9330e3a50d | ||
|
|
1d6b119340 | ||
|
|
8158f761c8 | ||
|
|
08838e091f | ||
|
|
59720a207d | ||
|
|
26bae65e1e | ||
|
|
23a2861b37 | ||
|
|
341238305d | ||
|
|
d210bed744 | ||
|
|
fe710e5510 | ||
|
|
8f8ffe7020 | ||
|
|
16e5018489 | ||
|
|
af303bd26f | ||
|
|
bf3bb5d804 | ||
|
|
dcb16e05bd | ||
|
|
2facedd60f | ||
|
|
b1ec81f054 | ||
|
|
779da518df | ||
|
|
4f53ae0e89 | ||
|
|
da04caeea2 | ||
|
|
a63916b08e | ||
|
|
36bd508bf9 | ||
|
|
bbdbfe85ed | ||
|
|
2a7e9a266a | ||
|
|
bd0d8af7b3 | ||
|
|
2da8ea0185 | ||
|
|
7a84844000 | ||
|
|
1580555d30 | ||
|
|
4fb8a1b50b | ||
|
|
e07f25000d | ||
|
|
cc1777af0c | ||
|
|
87e54c001f | ||
|
|
2dc87d42b4 | ||
|
|
1eed9c82dc | ||
|
|
cf43de7755 | ||
|
|
f54bec7553 | ||
|
|
cf698c2586 | ||
|
|
cb240a8f03 |
@@ -60,9 +60,8 @@ dependencyLocking {
|
||||
}
|
||||
|
||||
node {
|
||||
download = true
|
||||
version = "16.14.0"
|
||||
npmVersion = "6.14.11"
|
||||
download = false
|
||||
version = "16.19.0"
|
||||
}
|
||||
|
||||
wrapper {
|
||||
|
||||
@@ -41,8 +41,8 @@
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "5mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
|
||||
2129
console-webapp/package-lock.json
generated
2129
console-webapp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -37,7 +37,7 @@
|
||||
background-color: transparent;
|
||||
}
|
||||
.active {
|
||||
background: #eae1e1;
|
||||
background-color: var(--secondary);
|
||||
}
|
||||
}
|
||||
&__content-wrapper {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, ViewChild } from '@angular/core';
|
||||
import { AfterViewInit, Component, ViewChild } from '@angular/core';
|
||||
import { RegistrarService } from './registrar/registrar.service';
|
||||
import { UserDataService } from './shared/services/userData.service';
|
||||
import { GlobalLoaderService } from './shared/services/globalLoader.service';
|
||||
@@ -24,7 +24,7 @@ import { MatSidenav } from '@angular/material/sidenav';
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
})
|
||||
export class AppComponent {
|
||||
export class AppComponent implements AfterViewInit {
|
||||
renderRouter: boolean = true;
|
||||
|
||||
@ViewChild('sidenav')
|
||||
|
||||
@@ -47,6 +47,12 @@ 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 { 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';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -61,6 +67,8 @@ import { UserDataService } from './shared/services/userData.service';
|
||||
HomeComponent,
|
||||
PromotionsWidgetComponent,
|
||||
RegistrarComponent,
|
||||
RegistrarDetailsComponent,
|
||||
RegistrarDetailsWrapperComponent,
|
||||
RegistrarSelectorComponent,
|
||||
ResourcesWidgetComponent,
|
||||
SecurityComponent,
|
||||
@@ -69,6 +77,7 @@ import { UserDataService } from './shared/services/userData.service';
|
||||
SettingsWidgetComponent,
|
||||
TldsComponent,
|
||||
TldsWidgetComponent,
|
||||
WhoisComponent,
|
||||
],
|
||||
imports: [
|
||||
AppRoutingModule,
|
||||
@@ -77,6 +86,7 @@ import { UserDataService } from './shared/services/userData.service';
|
||||
FormsModule,
|
||||
HttpClientModule,
|
||||
MaterialModule,
|
||||
SnackBarModule,
|
||||
],
|
||||
providers: [
|
||||
BackendService,
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
<p>
|
||||
<p class="console-app__header">
|
||||
<mat-toolbar color="primary">
|
||||
<button mat-icon-button aria-label="Open menu" (click)="toggleNavPane()">
|
||||
<mat-icon>menu</mat-icon>
|
||||
</button>
|
||||
<span>
|
||||
<a
|
||||
[routerLink]="'/home'"
|
||||
routerLinkActive="active"
|
||||
class="console-app__logo"
|
||||
>
|
||||
Google Registry
|
||||
</a>
|
||||
</span>
|
||||
<a
|
||||
[routerLink]="'/home'"
|
||||
routerLinkActive="active"
|
||||
class="console-app__logo"
|
||||
>
|
||||
Google Registry
|
||||
</a>
|
||||
<span class="spacer"></span>
|
||||
<app-registrar-selector />
|
||||
<button mat-icon-button aria-label="Open FAQ">
|
||||
<mat-icon>question_mark</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button aria-label="Open user info">
|
||||
<button
|
||||
mat-icon-button
|
||||
[matMenuTriggerFor]="menu"
|
||||
#menuTrigger
|
||||
aria-label="Open user info"
|
||||
>
|
||||
<mat-icon>person</mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item (click)="logOut()">Log out</button>
|
||||
</mat-menu>
|
||||
</mat-toolbar>
|
||||
</p>
|
||||
|
||||
@@ -17,6 +17,21 @@
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
&__header {
|
||||
@media (max-width: 599px) {
|
||||
.mat-toolbar {
|
||||
padding: 0;
|
||||
}
|
||||
.console-app__logo {
|
||||
font-size: 16px;
|
||||
}
|
||||
button {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
width: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.spacer {
|
||||
flex: 1;
|
||||
|
||||
@@ -28,4 +28,8 @@ export class HeaderComponent {
|
||||
this.isNavOpen = !this.isNavOpen;
|
||||
this.toggleNavOpen.emit(this.isNavOpen);
|
||||
}
|
||||
|
||||
logOut() {
|
||||
window.open('/console?gcp-iap-mode=CLEAR_LOGIN_COOKIE', '_self');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,4 +29,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 510px) {
|
||||
.console-app__widget-wrapper__wide {
|
||||
grid-column: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,21 +5,23 @@
|
||||
<mat-icon class="console-app__widget-icon">call</mat-icon>
|
||||
<h1 class="console-app__widget-title">Contact Support</h1>
|
||||
<h4 class="secondary-text text-center">
|
||||
View Google Registry support email and phone information
|
||||
Let us know if you have any questions
|
||||
</h4>
|
||||
</div>
|
||||
<div class="console-app__widget_right">
|
||||
<button mat-button color="primary" class="console-app__widget-link">
|
||||
Give us a Call
|
||||
</button>
|
||||
<div class="console-app__widget-section-header">Give us a Call</div>
|
||||
<p class="secondary-text">
|
||||
Call Google Registry support at <b>+1 (404) 978 8419</b>
|
||||
Call {{ userDataService.userData?.productName }} support at
|
||||
<a href="tel:{{ userDataService.userData?.supportPhoneNumber }}">{{
|
||||
userDataService.userData?.supportPhoneNumber
|
||||
}}</a>
|
||||
</p>
|
||||
<button mat-button color="primary" class="console-app__widget-link">
|
||||
Send us an Email
|
||||
</button>
|
||||
<div class="console-app__widget-section-header">Send us an Email</div>
|
||||
<p class="secondary-text">
|
||||
Email Google Registry at <b>support@google.com</b>
|
||||
Email {{ userDataService.userData?.productName }} at
|
||||
<a href="mailto:{{ userDataService.userData?.supportEmail }}">{{
|
||||
userDataService.userData?.supportEmail
|
||||
}}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,11 +13,12 @@
|
||||
// limitations under the License.
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { UserDataService } from 'src/app/shared/services/userData.service';
|
||||
|
||||
@Component({
|
||||
selector: '[app-contact-widget]',
|
||||
templateUrl: './contact-widget.component.html',
|
||||
})
|
||||
export class ContactWidgetComponent {
|
||||
constructor() {}
|
||||
constructor(public userDataService: UserDataService) {}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<a
|
||||
class="console-app__widget_left"
|
||||
href="{{ userDataService.userData?.technicalDocsUrl }}"
|
||||
target="_blank"
|
||||
>
|
||||
<mat-icon class="console-app__widget-icon">menu_book</mat-icon>
|
||||
<h1 class="console-app__widget-title">Resources</h1>
|
||||
|
||||
@@ -45,6 +45,7 @@ import { DialogModule } from '@angular/cdk/dialog';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
|
||||
@NgModule({
|
||||
exports: [
|
||||
@@ -81,6 +82,7 @@ import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
DialogModule,
|
||||
MatSnackBarModule,
|
||||
MatPaginatorModule,
|
||||
MatChipsModule,
|
||||
],
|
||||
})
|
||||
export class MaterialModule {}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
white-space: nowrap;
|
||||
|
||||
&-icon {
|
||||
transform: scale(3);
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
<div class="console-app__registrar">
|
||||
<div>
|
||||
<button
|
||||
mat-button
|
||||
[routerLink]="'/settings/registrars'"
|
||||
routerLinkActive="active"
|
||||
*ngIf="isMobile; else desktop"
|
||||
>
|
||||
{{ registrarService.activeRegistrarId || "Select registrar" }}
|
||||
<mat-icon>open_in_new</mat-icon>
|
||||
</button>
|
||||
<ng-template #desktop>
|
||||
<mat-form-field class="mat-form-field-density-5" appearance="fill">
|
||||
<mat-label>Registrar</mat-label>
|
||||
<mat-select
|
||||
[ngModel]="registrarService.activeRegistrarId"
|
||||
(selectionChange)="registrarService.updateRegistrar($event.value)"
|
||||
(selectionChange)="
|
||||
registrarService.updateSelectedRegistrar($event.value)
|
||||
"
|
||||
>
|
||||
<mat-option
|
||||
*ngFor="let registrar of registrarService.registrars"
|
||||
@@ -14,5 +25,5 @@
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
@@ -12,14 +12,35 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { RegistrarService } from './registrar.service';
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import { distinctUntilChanged } from 'rxjs';
|
||||
|
||||
const MOBILE_LAYOUT_BREAKPOINT = '(max-width: 599px)';
|
||||
|
||||
@Component({
|
||||
selector: 'app-registrar-selector',
|
||||
templateUrl: './registrar-selector.component.html',
|
||||
styleUrls: ['./registrar-selector.component.scss'],
|
||||
})
|
||||
export class RegistrarSelectorComponent {
|
||||
constructor(protected registrarService: RegistrarService) {}
|
||||
export class RegistrarSelectorComponent implements OnInit {
|
||||
protected isMobile: boolean = false;
|
||||
|
||||
readonly breakpoint$ = this.breakpointObserver
|
||||
.observe([MOBILE_LAYOUT_BREAKPOINT])
|
||||
.pipe(distinctUntilChanged());
|
||||
|
||||
constructor(
|
||||
protected registrarService: RegistrarService,
|
||||
protected breakpointObserver: BreakpointObserver
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.breakpoint$.subscribe(() => this.breakpointChanged());
|
||||
}
|
||||
|
||||
private breakpointChanged() {
|
||||
this.isMobile = this.breakpointObserver.isMatched(MOBILE_LAYOUT_BREAKPOINT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,11 @@
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
Router,
|
||||
RouterStateSnapshot,
|
||||
} from '@angular/router';
|
||||
|
||||
import { RegistrarService } from './registrar.service';
|
||||
|
||||
@@ -26,13 +30,16 @@ export class RegistrarGuard {
|
||||
private registrarService: RegistrarService
|
||||
) {}
|
||||
|
||||
canActivate(): Promise<boolean> | boolean {
|
||||
canActivate(
|
||||
_: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot
|
||||
): Promise<boolean> | boolean {
|
||||
if (this.registrarService.activeRegistrarId) {
|
||||
return true;
|
||||
}
|
||||
// Get the full URL including any nested children (skip the initial '#/')
|
||||
// NB: an empty nextUrl takes the user to the home page
|
||||
const nextUrl = location.hash.split('#/')[1] || '';
|
||||
return this.router.navigate([`/empty-registrar`, { nextUrl }]);
|
||||
return this.router.navigate([
|
||||
`/empty-registrar`,
|
||||
{ nextUrl: state.url || '' },
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,15 +13,16 @@
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
import { Observable, Subject, tap } from 'rxjs';
|
||||
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
import {
|
||||
GlobalLoader,
|
||||
GlobalLoaderService,
|
||||
} from '../shared/services/globalLoader.service';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
|
||||
interface Address {
|
||||
export interface Address {
|
||||
street?: string[];
|
||||
city?: string;
|
||||
countryCode?: string;
|
||||
@@ -31,16 +32,20 @@ interface Address {
|
||||
|
||||
export interface Registrar {
|
||||
allowedTlds?: string[];
|
||||
ipAddressAllowList?: string[];
|
||||
emailAddress?: string;
|
||||
billingAccountMap?: object;
|
||||
driveFolderId?: string;
|
||||
emailAddress?: string;
|
||||
faxNumber?: string;
|
||||
ianaIdentifier?: number;
|
||||
icannReferralEmail?: string;
|
||||
ipAddressAllowList?: string[];
|
||||
localizedAddress?: Address;
|
||||
phoneNumber?: string;
|
||||
registrarId: string;
|
||||
registrarName: string;
|
||||
registryLockAllowed?: boolean;
|
||||
url?: string;
|
||||
whoisServer?: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
@@ -68,7 +73,7 @@ export class RegistrarService implements GlobalLoader {
|
||||
)[0];
|
||||
}
|
||||
|
||||
public updateRegistrar(registrarId: string) {
|
||||
public updateSelectedRegistrar(registrarId: string) {
|
||||
this.activeRegistrarId = registrarId;
|
||||
this.activeRegistrarIdChange.next(registrarId);
|
||||
}
|
||||
@@ -84,8 +89,6 @@ export class RegistrarService implements GlobalLoader {
|
||||
}
|
||||
|
||||
loadingTimeout() {
|
||||
this._snackBar.open('Timeout loading registrars', undefined, {
|
||||
duration: 1500,
|
||||
});
|
||||
this._snackBar.open('Timeout loading registrars');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<div class="registrarDetails">
|
||||
<h3 mat-dialog-title>Edit Registrar: {{ registrarInEdit.registrarId }}</h3>
|
||||
<div mat-dialog-content>
|
||||
<form (ngSubmit)="saveAndClose($event)">
|
||||
<mat-form-field class="registrarDetails__input">
|
||||
<mat-label>Registry Lock:</mat-label>
|
||||
<mat-select
|
||||
[(ngModel)]="registrarInEdit.registryLockAllowed"
|
||||
name="registryLockAllowed"
|
||||
>
|
||||
<mat-option [value]="true">True</mat-option>
|
||||
<mat-option [value]="false">False</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="registrarDetails__input">
|
||||
<mat-label>Onboarded TLDs: </mat-label>
|
||||
<mat-chip-grid #chipGrid aria-label="Enter TLD">
|
||||
<mat-chip-row
|
||||
*ngFor="let tld of registrarInEdit.allowedTlds"
|
||||
(removed)="removeTLD(tld)"
|
||||
>
|
||||
{{ tld }}
|
||||
<button matChipRemove aria-label="'remove ' + tld">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
</button>
|
||||
</mat-chip-row>
|
||||
</mat-chip-grid>
|
||||
<input
|
||||
placeholder="New tld..."
|
||||
[matChipInputFor]="chipGrid"
|
||||
(matChipInputTokenEnd)="addTLD($event)"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button (click)="onCancel($event)">Cancel</button>
|
||||
<button type="submit" mat-button color="primary">Save</button>
|
||||
</mat-dialog-actions>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,8 @@
|
||||
.registrarDetails {
|
||||
min-width: 30vw;
|
||||
|
||||
&__input {
|
||||
display: block;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
105
console-webapp/src/app/registrar/registrarDetails.component.ts
Normal file
105
console-webapp/src/app/registrar/registrarDetails.component.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
// 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, Injector } 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';
|
||||
|
||||
const MOBILE_LAYOUT_BREAKPOINT = '(max-width: 599px)';
|
||||
|
||||
@Component({
|
||||
selector: 'app-registrar-details',
|
||||
templateUrl: './registrarDetails.component.html',
|
||||
styleUrls: ['./registrarDetails.component.scss'],
|
||||
})
|
||||
export class RegistrarDetailsComponent {
|
||||
registrarInEdit!: Registrar;
|
||||
private elementRef:
|
||||
| MatBottomSheetRef<RegistrarDetailsComponent>
|
||||
| MatDialogRef<RegistrarDetailsComponent>;
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
addTLD(e: MatChipInputEvent) {
|
||||
this.removeTLD(e.value); // Prevent dups
|
||||
this.registrarInEdit.allowedTlds = this.registrarInEdit.allowedTlds?.concat(
|
||||
[e.value.toLowerCase()]
|
||||
);
|
||||
}
|
||||
|
||||
removeTLD(tld: string) {
|
||||
this.registrarInEdit.allowedTlds = this.registrarInEdit.allowedTlds?.filter(
|
||||
(v) => v != tld
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,54 @@
|
||||
<div class="console-app__registrars">
|
||||
<table
|
||||
mat-table
|
||||
[dataSource]="registrarService.registrars"
|
||||
<mat-form-field class="console-app__registrars-filter">
|
||||
<mat-label>Search</mat-label>
|
||||
<input
|
||||
matInput
|
||||
(keyup)="applyFilter($event)"
|
||||
placeholder="..."
|
||||
type="search"
|
||||
/>
|
||||
<mat-icon matPrefix>search</mat-icon>
|
||||
</mat-form-field>
|
||||
<mat-table
|
||||
[dataSource]="dataSource"
|
||||
class="mat-elevation-z8"
|
||||
class="console-app__registrars-table"
|
||||
matSort
|
||||
>
|
||||
<ng-container matColumnDef="edit">
|
||||
<mat-header-cell *matHeaderCellDef></mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">
|
||||
<button
|
||||
mat-icon-button
|
||||
color="primary"
|
||||
aria-label="Edit registrar"
|
||||
(click)="openDetails($event, row)"
|
||||
>
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container
|
||||
*ngFor="let column of columns"
|
||||
[matColumnDef]="column.columnDef"
|
||||
>
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
{{ column.header }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let row" [innerHTML]="column.cell(row)"></td>
|
||||
<mat-header-cell *matHeaderCellDef> {{ column.header }} </mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" [innerHTML]="column.cell(row)"></mat-cell>
|
||||
</ng-container>
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
(click)="registrarService.updateSelectedRegistrar(row.registrarId)"
|
||||
></mat-row>
|
||||
</mat-table>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
</table>
|
||||
<mat-paginator
|
||||
class="mat-elevation-z8"
|
||||
[pageSizeOptions]="[5, 10, 20]"
|
||||
showFirstLastButtons
|
||||
></mat-paginator>
|
||||
<app-registrar-details-wrapper
|
||||
#registrarDetailsView
|
||||
></app-registrar-details-wrapper>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,36 @@
|
||||
.console-app {
|
||||
$min-width: 756px;
|
||||
|
||||
&__registrars {
|
||||
margin-top: 1.5rem;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
&__registrars-filter {
|
||||
min-width: $min-width !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__registrars-table {
|
||||
min-width: $min-width !important;
|
||||
}
|
||||
|
||||
.mat-mdc-paginator {
|
||||
min-width: $min-width !important;
|
||||
}
|
||||
|
||||
.mat-column {
|
||||
&-edit {
|
||||
max-width: 55px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
&-driveId {
|
||||
min-width: 200px;
|
||||
word-break: break-all;
|
||||
}
|
||||
&-registryLockAllowed {
|
||||
max-width: 80px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,16 +12,22 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
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';
|
||||
|
||||
@Component({
|
||||
selector: 'app-registrar',
|
||||
templateUrl: './registrarsTable.component.html',
|
||||
styleUrls: ['./registrarsTable.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
export class RegistrarComponent {
|
||||
public static PATH = 'registrars';
|
||||
dataSource: MatTableDataSource<Registrar>;
|
||||
columns = [
|
||||
{
|
||||
columnDef: 'registrarId',
|
||||
@@ -71,6 +77,31 @@ export class RegistrarComponent {
|
||||
cell: (record: Registrar) => `${record.driveFolderId || ''}`,
|
||||
},
|
||||
];
|
||||
displayedColumns = this.columns.map((c) => c.columnDef);
|
||||
constructor(protected registrarService: RegistrarService) {}
|
||||
displayedColumns = ['edit'].concat(this.columns.map((c) => c.columnDef));
|
||||
|
||||
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
||||
@ViewChild(MatSort) sort!: MatSort;
|
||||
@ViewChild('registrarDetailsView')
|
||||
detailsComponentWrapper!: RegistrarDetailsWrapperComponent;
|
||||
|
||||
constructor(protected registrarService: RegistrarService) {
|
||||
this.dataSource = new MatTableDataSource<Registrar>(
|
||||
registrarService.registrars
|
||||
);
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.dataSource.paginator = this.paginator;
|
||||
this.dataSource.sort = this.sort;
|
||||
}
|
||||
|
||||
openDetails(event: MouseEvent, registrar: Registrar) {
|
||||
event.stopPropagation();
|
||||
this.detailsComponentWrapper.open(registrar);
|
||||
}
|
||||
|
||||
applyFilter(event: Event) {
|
||||
const filterValue = (event.target as HTMLInputElement).value;
|
||||
this.dataSource.filter = filterValue.trim().toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<h3 mat-dialog-title>Contact details</h3>
|
||||
<div mat-dialog-content>
|
||||
<form (ngSubmit)="saveAndClose($event)">
|
||||
<div>
|
||||
<p>
|
||||
<mat-form-field class="contact-details__input">
|
||||
<mat-label>Name: </mat-label>
|
||||
<input
|
||||
@@ -11,9 +11,9 @@
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<p>
|
||||
<mat-form-field class="contact-details__input">
|
||||
<mat-label>Primary account email: </mat-label>
|
||||
<input
|
||||
@@ -25,9 +25,9 @@
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<p>
|
||||
<mat-form-field class="contact-details__input">
|
||||
<mat-label>Phone: </mat-label>
|
||||
<input
|
||||
@@ -36,9 +36,9 @@
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<p>
|
||||
<mat-form-field class="contact-details__input">
|
||||
<mat-label>Fax: </mat-label>
|
||||
<input
|
||||
@@ -47,7 +47,7 @@
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<div class="contact-details__group">
|
||||
<label>Contact type:</label>
|
||||
|
||||
@@ -129,9 +129,7 @@ export class ContactDetailsDialogComponent {
|
||||
operationObservable.subscribe({
|
||||
complete: this.onCloseCallback.bind(this),
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error, undefined, {
|
||||
duration: 1500,
|
||||
});
|
||||
this._snackBar.open(err.error);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -175,9 +173,7 @@ export default class ContactComponent {
|
||||
if (confirm(`Please confirm contact ${contact.name} delete`)) {
|
||||
this.contactService.deleteContact(contact).subscribe({
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error, undefined, {
|
||||
duration: 1500,
|
||||
});
|
||||
this._snackBar.open(err.error);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -64,9 +64,7 @@ export default class SecurityComponent {
|
||||
this.resetDataSource();
|
||||
},
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error, undefined, {
|
||||
duration: 1500,
|
||||
});
|
||||
this._snackBar.open(err.error);
|
||||
},
|
||||
});
|
||||
this.cancel();
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
.console-settings {
|
||||
.mdc-tab {
|
||||
&.active-link {
|
||||
border-bottom: 2px solid #673ab7;
|
||||
border-bottom: 2px solid var(--primary);
|
||||
.mdc-tab__text-label {
|
||||
color: #673ab7;
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,250 @@
|
||||
<p>whois works!</p>
|
||||
<div class="settings-whois">
|
||||
<h2>WHOIS settings</h2>
|
||||
<h3>
|
||||
General registrar information for your WHOIS record. This information is
|
||||
always visible in WHOIS.
|
||||
</h3>
|
||||
<div *ngIf="loading" class="settings-whois__loading">
|
||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||
</div>
|
||||
<div class="settings-whois__section">
|
||||
<div class="settings-whois__section-description">
|
||||
<h3>Name:</h3>
|
||||
</div>
|
||||
<div class="settings-whois__section-form">
|
||||
<mat-form-field>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="registrar.registrarName"
|
||||
disabled
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-whois__section">
|
||||
<div class="settings-whois__section-description">
|
||||
<h3>IANA Identifier:</h3>
|
||||
</div>
|
||||
<div class="settings-whois__section-form">
|
||||
<mat-form-field>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="registrar.ianaIdentifier"
|
||||
disabled
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-whois__section">
|
||||
<div class="settings-whois__section-description">
|
||||
<h3>ICANN Referral Email:</h3>
|
||||
</div>
|
||||
<div class="settings-whois__section-form">
|
||||
<mat-form-field>
|
||||
<input
|
||||
matInput
|
||||
type="email"
|
||||
[(ngModel)]="registrar.icannReferralEmail"
|
||||
disabled
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-whois__section">
|
||||
<div class="settings-whois__section-description">
|
||||
<h3>WHOIS server:</h3>
|
||||
</div>
|
||||
<div class="settings-whois__section-form">
|
||||
<mat-form-field>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="registrar.whoisServer"
|
||||
[disabled]="!inEdit"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-whois__section">
|
||||
<div class="settings-whois__section-description">
|
||||
<h3>Referral URL:</h3>
|
||||
</div>
|
||||
<div class="settings-whois__section-form">
|
||||
<mat-form-field>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="registrar.url"
|
||||
[disabled]="!inEdit"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-whois__section">
|
||||
<div class="settings-whois__section-description">
|
||||
<h3>Email:</h3>
|
||||
</div>
|
||||
<div class="settings-whois__section-form">
|
||||
<mat-form-field>
|
||||
<input
|
||||
matInput
|
||||
type="email"
|
||||
[(ngModel)]="registrar.emailAddress"
|
||||
[disabled]="!inEdit"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-whois__section">
|
||||
<div class="settings-whois__section-description">
|
||||
<h3>Phone::</h3>
|
||||
</div>
|
||||
<div class="settings-whois__section-form">
|
||||
<mat-form-field>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="registrar.phoneNumber"
|
||||
[disabled]="!inEdit"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-whois__section">
|
||||
<div class="settings-whois__section-description">
|
||||
<h3>Fax:</h3>
|
||||
</div>
|
||||
<div class="settings-whois__section-form">
|
||||
<mat-form-field>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="registrar.faxNumber"
|
||||
[disabled]="!inEdit"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-whois__section">
|
||||
<div class="settings-whois__section-address">
|
||||
<div class="settings-whois__section-description">
|
||||
<h3>Address Line 1:</h3>
|
||||
</div>
|
||||
<div class="settings-whois__section-form">
|
||||
<mat-form-field>
|
||||
<input
|
||||
*ngIf="registrar.localizedAddress?.street"
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="(registrar.localizedAddress?.street)![0]"
|
||||
[disabled]="!inEdit"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-whois__section-address">
|
||||
<div class="settings-whois__section-description">
|
||||
<h3>City:</h3>
|
||||
</div>
|
||||
<div class="settings-whois__section-form">
|
||||
<mat-form-field>
|
||||
<input
|
||||
*ngIf="registrar.localizedAddress"
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="registrar.localizedAddress.city"
|
||||
[disabled]="!inEdit"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-whois__section">
|
||||
<div class="settings-whois__section-address">
|
||||
<div class="settings-whois__section-description">
|
||||
<h3>Address Line 2:</h3>
|
||||
</div>
|
||||
<div class="settings-whois__section-form">
|
||||
<mat-form-field>
|
||||
<input
|
||||
*ngIf="registrar.localizedAddress?.street"
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="(registrar.localizedAddress?.street)![1]"
|
||||
[disabled]="!inEdit"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-whois__section-address">
|
||||
<div class="settings-whois__section-description">
|
||||
<h3>State/Region:</h3>
|
||||
</div>
|
||||
<div class="settings-whois__section-form">
|
||||
<mat-form-field>
|
||||
<input
|
||||
*ngIf="registrar.localizedAddress"
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="registrar.localizedAddress.state"
|
||||
[disabled]="!inEdit"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-whois__section">
|
||||
<div class="settings-whois__section-address">
|
||||
<div class="settings-whois__section-description">
|
||||
<h3>Address Line 3:</h3>
|
||||
</div>
|
||||
<div class="settings-whois__section-form">
|
||||
<mat-form-field>
|
||||
<input
|
||||
*ngIf="registrar.localizedAddress?.street"
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="(registrar.localizedAddress?.street)![2]"
|
||||
[disabled]="!inEdit"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-whois__section-address">
|
||||
<div class="settings-whois__section-description">
|
||||
<h3>Country Code:</h3>
|
||||
</div>
|
||||
<div class="settings-whois__section-form">
|
||||
<mat-form-field>
|
||||
<input
|
||||
*ngIf="registrar.localizedAddress"
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="registrar.localizedAddress.countryCode"
|
||||
[disabled]="!inEdit"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-whois__actions">
|
||||
<ng-template [ngIf]="inEdit" [ngIfElse]="inView">
|
||||
<button
|
||||
class="actions-save"
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
(click)="save()"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button class="actions-cancel" mat-stroked-button (click)="cancel()">
|
||||
Cancel
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template #inView>
|
||||
<button #elseBlock mat-raised-button (click)="enableEdit()">Edit</button>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,3 +11,49 @@
|
||||
// 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.
|
||||
|
||||
.settings-whois {
|
||||
margin-top: 1.5rem;
|
||||
&__section {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 10px;
|
||||
min-width: 400px;
|
||||
}
|
||||
&__section-address {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 5px;
|
||||
min-width: 400px;
|
||||
width: 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
&__section-description {
|
||||
display: inline-block;
|
||||
margin-block-start: 1em;
|
||||
width: 160px;
|
||||
}
|
||||
&__section-form {
|
||||
display: inline-block;
|
||||
width: 70%;
|
||||
mat-form-field {
|
||||
width: 90%;
|
||||
min-width: 300px;
|
||||
}
|
||||
input:disabled {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
&__loading {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
&__actions {
|
||||
margin-top: 50px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-right: 50px;
|
||||
button {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,13 +12,65 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Component } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import {
|
||||
Registrar,
|
||||
RegistrarService,
|
||||
} from 'src/app/registrar/registrar.service';
|
||||
|
||||
import { WhoisService } from './whois.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-whois',
|
||||
templateUrl: './whois.component.html',
|
||||
styleUrls: ['./whois.component.scss'],
|
||||
providers: [WhoisService],
|
||||
})
|
||||
export default class WhoisComponent {
|
||||
public static PATH = 'whois';
|
||||
loading = false;
|
||||
inEdit = false;
|
||||
registrar: Registrar;
|
||||
|
||||
constructor(
|
||||
public whoisService: WhoisService,
|
||||
public registrarService: RegistrarService,
|
||||
private _snackBar: MatSnackBar
|
||||
) {
|
||||
this.registrar = JSON.parse(
|
||||
JSON.stringify(this.registrarService.registrar)
|
||||
);
|
||||
}
|
||||
|
||||
enableEdit() {
|
||||
this.inEdit = true;
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.inEdit = false;
|
||||
this.resetDataSource();
|
||||
}
|
||||
|
||||
save() {
|
||||
this.loading = true;
|
||||
this.whoisService.saveChanges(this.registrar).subscribe({
|
||||
complete: () => {
|
||||
this.loading = false;
|
||||
this.resetDataSource();
|
||||
},
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error);
|
||||
this.loading = false;
|
||||
},
|
||||
});
|
||||
this.cancel();
|
||||
}
|
||||
|
||||
resetDataSource() {
|
||||
this.registrar = JSON.parse(
|
||||
JSON.stringify(this.registrarService.registrar)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
45
console-webapp/src/app/settings/whois/whois.service.ts
Normal file
45
console-webapp/src/app/settings/whois/whois.service.ts
Normal file
@@ -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.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { switchMap } from 'rxjs';
|
||||
import { Address, RegistrarService } from 'src/app/registrar/registrar.service';
|
||||
import { BackendService } from 'src/app/shared/services/backend.service';
|
||||
|
||||
export interface WhoisRegistrarFields {
|
||||
ianaIdentifier?: number;
|
||||
icannReferralEmail?: string;
|
||||
localizedAddress?: Address;
|
||||
registrarId?: string;
|
||||
url?: string;
|
||||
whoisServer?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class WhoisService {
|
||||
whoisRegistrarFields: WhoisRegistrarFields = {};
|
||||
|
||||
constructor(
|
||||
private backend: BackendService,
|
||||
private registrarService: RegistrarService
|
||||
) {}
|
||||
|
||||
saveChanges(newWhoisRegistrarFields: WhoisRegistrarFields) {
|
||||
return this.backend.postWhoisRegistrarFields(newWhoisRegistrarFields).pipe(
|
||||
switchMap(() => {
|
||||
return this.registrarService.loadRegistrars();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import { SecuritySettingsBackendModel } from 'src/app/settings/security/security
|
||||
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';
|
||||
|
||||
@Injectable()
|
||||
export class BackendService {
|
||||
@@ -94,7 +95,16 @@ export class BackendService {
|
||||
|
||||
getUserData(): Observable<UserData> {
|
||||
return this.http
|
||||
.get<UserData>(`/console-api/userdata`)
|
||||
.get<UserData>('/console-api/userdata')
|
||||
.pipe(catchError((err) => this.errorCatcher<UserData>(err)));
|
||||
}
|
||||
|
||||
postWhoisRegistrarFields(
|
||||
whoisRegistrarFields: WhoisRegistrarFields
|
||||
): Observable<WhoisRegistrarFields> {
|
||||
return this.http.post<WhoisRegistrarFields>(
|
||||
'/console-api/settings/whois-fields',
|
||||
whoisRegistrarFields
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,11 @@ import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { GlobalLoader, GlobalLoaderService } from './globalLoader.service';
|
||||
|
||||
export interface UserData {
|
||||
isAdmin: boolean;
|
||||
globalRole: string;
|
||||
isAdmin: boolean;
|
||||
productName: string;
|
||||
supportEmail: string;
|
||||
supportPhoneNumber: string;
|
||||
technicalDocsUrl: string;
|
||||
}
|
||||
|
||||
@@ -49,8 +52,6 @@ export class UserDataService implements GlobalLoader {
|
||||
}
|
||||
|
||||
loadingTimeout() {
|
||||
this._snackBar.open('Timeout loading user data', undefined, {
|
||||
duration: 1500,
|
||||
});
|
||||
this._snackBar.open('Timeout loading user data');
|
||||
}
|
||||
}
|
||||
|
||||
24
console-webapp/src/app/snackbar.module.ts
Normal file
24
console-webapp/src/app/snackbar.module.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// 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 { NgModule } from '@angular/core';
|
||||
import { MAT_SNACK_BAR_DEFAULT_OPTIONS } from '@angular/material/snack-bar';
|
||||
|
||||
/** Provides a default set of options for the snack bar. */
|
||||
@NgModule({
|
||||
providers: [
|
||||
{ provide: MAT_SNACK_BAR_DEFAULT_OPTIONS, useValue: { duration: 5000 } },
|
||||
],
|
||||
})
|
||||
export class SnackBarModule {}
|
||||
@@ -44,13 +44,19 @@ body {
|
||||
&-link {
|
||||
padding: 0 !important;
|
||||
text-align: left;
|
||||
height: 20px !important;
|
||||
min-width: auto !important;
|
||||
height: min-content !important;
|
||||
}
|
||||
&-section-header {
|
||||
font-weight: 500;
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
&-title {
|
||||
color: var(--primary) !important;
|
||||
text-align: center;
|
||||
}
|
||||
&-icon {
|
||||
color: var(--text);
|
||||
font-size: 5rem;
|
||||
line-height: 5rem;
|
||||
height: 5rem !important;
|
||||
|
||||
@@ -17,20 +17,9 @@ $theme-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400);
|
||||
// The warn palette is optional (defaults to red).
|
||||
$theme-warn: mat.define-palette(mat.$red-palette);
|
||||
|
||||
// Create the theme object. A theme consists of configurations for individual
|
||||
// theming systems such as "color" or "typography".
|
||||
$theme: mat.define-light-theme(
|
||||
(
|
||||
color: (
|
||||
primary: $theme-primary,
|
||||
accent: $theme-accent,
|
||||
warn: $theme-warn,
|
||||
),
|
||||
density: 0,
|
||||
)
|
||||
);
|
||||
|
||||
/** Application specific section **/
|
||||
/**
|
||||
** Application specific section - Global styles and mixins
|
||||
**/
|
||||
|
||||
@mixin form-field-density($density) {
|
||||
$field-typography: mat.define-typography-config(
|
||||
@@ -46,19 +35,6 @@ $theme: mat.define-light-theme(
|
||||
@include form-field-density(-5);
|
||||
}
|
||||
|
||||
$foreground: map.merge($theme, mat.$light-theme-foreground-palette);
|
||||
|
||||
// Access and define a class with secondary color exposed
|
||||
.secondary-text {
|
||||
color: map.get($foreground, "secondary-text");
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary: #{mat.get-color-from-palette($theme-primary, 500)};
|
||||
--secondary: #{map.get($foreground, "secondary-text")};
|
||||
}
|
||||
|
||||
@include mat.all-component-themes($theme);
|
||||
@import "@angular/material/theming";
|
||||
|
||||
// Define application specific typography settings, font-family, etc
|
||||
@@ -67,3 +43,61 @@ $typography-configuration: mat-typography-config(
|
||||
);
|
||||
|
||||
@include angular-material-typography($typography-configuration);
|
||||
|
||||
/**
|
||||
** Light theme
|
||||
**/
|
||||
$light-theme: mat.define-light-theme(
|
||||
(
|
||||
color: (
|
||||
primary: $theme-primary,
|
||||
accent: $theme-accent,
|
||||
warn: $theme-warn,
|
||||
),
|
||||
density: 0,
|
||||
)
|
||||
);
|
||||
|
||||
// Access and define a class with secondary color exposed
|
||||
.secondary-text {
|
||||
color: map.get(mat.$light-theme-foreground-palette, "secondary-text");
|
||||
}
|
||||
|
||||
:root {
|
||||
--text: #{map.get(mat.$light-theme-foreground-palette, "base")};
|
||||
--primary: #{mat.get-color-from-palette($theme-primary, 500)};
|
||||
--secondary: #{map.get(mat.$light-theme-foreground-palette, "secondary-text")};
|
||||
}
|
||||
|
||||
@include mat.all-component-themes($light-theme);
|
||||
|
||||
/**
|
||||
** Dark theme
|
||||
**/
|
||||
$dark-theme: mat.define-dark-theme(
|
||||
(
|
||||
color: (
|
||||
primary: mat.define-palette(mat.$pink-palette),
|
||||
accent: mat.define-palette(mat.$blue-grey-palette),
|
||||
),
|
||||
density: 0,
|
||||
)
|
||||
);
|
||||
|
||||
@mixin _apply-dark-mode-colors() {
|
||||
@include mat.all-component-colors($dark-theme);
|
||||
|
||||
.secondary-text {
|
||||
color: map.get(mat.$dark-theme-foreground-palette, "secondary-text");
|
||||
}
|
||||
|
||||
:root {
|
||||
--text: #{map.get(mat.$dark-theme-foreground-palette, "base")};
|
||||
--primary: #{mat.get-color-from-palette(mat.$pink-palette, 500)};
|
||||
--secondary: #{map.get(mat.$dark-theme-background-palette, "secondary-text")};
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@include _apply-dark-mode-colors();
|
||||
}
|
||||
|
||||
@@ -43,6 +43,12 @@ public class BatchModule {
|
||||
public static final String PARAM_DRY_RUN = "dryRun";
|
||||
public static final String PARAM_FAST = "fast";
|
||||
|
||||
@Provides
|
||||
@Parameter("url")
|
||||
static String provideUrl(HttpServletRequest req) {
|
||||
return extractRequiredParameter(req, "url");
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Parameter("jobName")
|
||||
static Optional<String> provideJobName(HttpServletRequest req) {
|
||||
|
||||
@@ -14,26 +14,20 @@
|
||||
|
||||
package google.registry.batch;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static google.registry.request.Action.Method.GET;
|
||||
import static google.registry.request.Action.Method.POST;
|
||||
import static google.registry.util.RegistrarUtils.normalizeRegistrarId;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Streams;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.groups.GmailClient;
|
||||
import google.registry.groups.GroupsConnection;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.model.registrar.RegistrarPoc;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.Parameter;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.request.UrlConnectionService;
|
||||
import google.registry.request.UrlConnectionUtils;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.util.EmailMessage;
|
||||
import java.io.IOException;
|
||||
import java.util.Set;
|
||||
import java.net.URL;
|
||||
import javax.inject.Inject;
|
||||
import javax.mail.internet.AddressException;
|
||||
import javax.mail.internet.InternetAddress;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
|
||||
/**
|
||||
* Action that executes a canned script specified by the caller.
|
||||
@@ -50,88 +44,45 @@ import javax.mail.internet.InternetAddress;
|
||||
@Action(
|
||||
service = Action.Service.BACKEND,
|
||||
path = "/_dr/task/executeCannedScript",
|
||||
method = POST,
|
||||
method = {POST, GET},
|
||||
automaticallyPrintOk = true,
|
||||
auth = Auth.AUTH_API_ADMIN)
|
||||
public class CannedScriptExecutionAction implements Runnable {
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
private final GroupsConnection groupsConnection;
|
||||
private final GmailClient gmailClient;
|
||||
|
||||
private final InternetAddress senderAddress;
|
||||
|
||||
private final InternetAddress recipientAddress;
|
||||
|
||||
private final String gSuiteDomainName;
|
||||
@Inject UrlConnectionService urlConnectionService;
|
||||
@Inject Response response;
|
||||
|
||||
@Inject
|
||||
CannedScriptExecutionAction(
|
||||
GroupsConnection groupsConnection,
|
||||
GmailClient gmailClient,
|
||||
@Config("projectId") String projectId,
|
||||
@Config("gSuiteDomainName") String gSuiteDomainName,
|
||||
@Config("newAlertRecipientEmailAddress") InternetAddress recipientAddress) {
|
||||
this.groupsConnection = groupsConnection;
|
||||
this.gmailClient = gmailClient;
|
||||
this.gSuiteDomainName = gSuiteDomainName;
|
||||
try {
|
||||
this.senderAddress = new InternetAddress(String.format("%s@%s", projectId, gSuiteDomainName));
|
||||
} catch (AddressException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
this.recipientAddress = recipientAddress;
|
||||
logger.atInfo().log("Sender:%s; Recipient: %s.", this.senderAddress, this.recipientAddress);
|
||||
}
|
||||
@Parameter("url")
|
||||
String url;
|
||||
|
||||
@Inject
|
||||
CannedScriptExecutionAction() {}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Integer responseCode = null;
|
||||
String responseContent = null;
|
||||
try {
|
||||
// Invoke canned scripts here.
|
||||
checkGroupApi();
|
||||
EmailMessage message = createEmail();
|
||||
this.gmailClient.sendEmail(message);
|
||||
logger.atInfo().log("Finished running scripts.");
|
||||
} catch (Throwable t) {
|
||||
logger.atWarning().withCause(t).log("Error executing scripts.");
|
||||
throw new RuntimeException("Execution failed.");
|
||||
}
|
||||
}
|
||||
|
||||
// Checks if Directory and GroupSettings still work after GWorkspace changes.
|
||||
void checkGroupApi() {
|
||||
ImmutableList<Registrar> registrars =
|
||||
Streams.stream(Registrar.loadAllCached())
|
||||
.filter(registrar -> registrar.isLive() && registrar.getType() == Registrar.Type.REAL)
|
||||
.collect(toImmutableList());
|
||||
logger.atInfo().log("Found %s registrars.", registrars.size());
|
||||
for (Registrar registrar : registrars) {
|
||||
for (final RegistrarPoc.Type type : RegistrarPoc.Type.values()) {
|
||||
String groupKey =
|
||||
String.format(
|
||||
"%s-%s-contacts@%s",
|
||||
normalizeRegistrarId(registrar.getRegistrarId()),
|
||||
type.getDisplayName(),
|
||||
gSuiteDomainName);
|
||||
try {
|
||||
Set<String> currentMembers = groupsConnection.getMembersOfGroup(groupKey);
|
||||
logger.atInfo().log("%s has %s members.", groupKey, currentMembers.size());
|
||||
// One success is enough for validation.
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
logger.atWarning().withCause(e).log("Failed to check %s", groupKey);
|
||||
}
|
||||
logger.atInfo().log("Connecting to: %s", url);
|
||||
HttpsURLConnection connection =
|
||||
(HttpsURLConnection) urlConnectionService.createConnection(new URL(url));
|
||||
responseCode = connection.getResponseCode();
|
||||
logger.atInfo().log("Code: %d", responseCode);
|
||||
logger.atInfo().log("Headers: %s", connection.getHeaderFields());
|
||||
responseContent = new String(UrlConnectionUtils.getResponseBytes(connection), UTF_8);
|
||||
logger.atInfo().log("Response: %s", responseContent);
|
||||
} catch (Exception e) {
|
||||
logger.atWarning().withCause(e).log("Connection to %s failed", url);
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
if (responseCode != null) {
|
||||
response.setStatus(responseCode);
|
||||
}
|
||||
if (responseContent != null) {
|
||||
response.setPayload(responseContent);
|
||||
}
|
||||
}
|
||||
logger.atInfo().log("Finished checking GroupApis.");
|
||||
}
|
||||
|
||||
EmailMessage createEmail() {
|
||||
return EmailMessage.newBuilder()
|
||||
.setFrom(senderAddress)
|
||||
.setSubject("Test: Please ignore<eom>.")
|
||||
.setRecipients(ImmutableList.of(recipientAddress))
|
||||
.setBody("Sent from Nomulus through Google Workspace.")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
@@ -879,6 +880,17 @@ public final class RegistryConfig {
|
||||
return Optional.ofNullable(config.misc.sheetExportId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the desired delay between outgoing emails when sending in bulk.
|
||||
*
|
||||
* <p>Gmail apparently has unpublished limits on peak throughput over short period.
|
||||
*/
|
||||
@Provides
|
||||
@Config("emailThrottleDuration")
|
||||
public static Duration provideEmailThrottleSeconds(RegistryConfigSettings config) {
|
||||
return Duration.standardSeconds(config.misc.emailThrottleSeconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the email address we send various alert e-mails to.
|
||||
*
|
||||
@@ -1163,44 +1175,6 @@ public final class RegistryConfig {
|
||||
return CONFIG_SETTINGS.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the OAuth scopes that authentication logic should detect on access tokens.
|
||||
*
|
||||
* <p>This list should be a superset of the required OAuth scope set provided below. Note that
|
||||
* ideally, this setting would not be required and all scopes on an access token would be
|
||||
* detected automatically, but that is not the case due to the way {@code OAuthService} works.
|
||||
*
|
||||
* <p>This is an independent setting from the required OAuth scopes (below) to support use cases
|
||||
* where certain actions require some additional scope (e.g. access to a user's Google Drive)
|
||||
* but that scope shouldn't be required for authentication alone; in that case the Drive scope
|
||||
* would be specified only for this setting, allowing that action to check for its presence.
|
||||
*/
|
||||
@Provides
|
||||
@Config("availableOauthScopes")
|
||||
public static ImmutableSet<String> provideAvailableOauthScopes(RegistryConfigSettings config) {
|
||||
return ImmutableSet.copyOf(config.auth.availableOauthScopes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the OAuth scopes that are required for authenticating successfully.
|
||||
*
|
||||
* <p>This set contains the scopes which must be present to authenticate a user. It should be a
|
||||
* subset of the scopes we request from the OAuth interface, provided above.
|
||||
*
|
||||
* <p>If we feel the need, we could define additional fixed scopes, similar to the Java remote
|
||||
* API, which requires at least one of:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@code https://www.googleapis.com/auth/appengine.apis}
|
||||
* <li>{@code https://www.googleapis.com/auth/cloud-platform}
|
||||
* </ul>
|
||||
*/
|
||||
@Provides
|
||||
@Config("requiredOauthScopes")
|
||||
public static ImmutableSet<String> provideRequiredOauthScopes(RegistryConfigSettings config) {
|
||||
return ImmutableSet.copyOf(config.auth.requiredOauthScopes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides service account email addresses allowed to authenticate with the app at {@link
|
||||
* google.registry.request.auth.AuthSettings.AuthLevel#APP} level.
|
||||
@@ -1212,13 +1186,6 @@ public final class RegistryConfig {
|
||||
return ImmutableSet.copyOf(config.auth.allowedServiceAccountEmails);
|
||||
}
|
||||
|
||||
/** Provides the allowed OAuth client IDs (could be multibinding). */
|
||||
@Provides
|
||||
@Config("allowedOauthClientIds")
|
||||
public static ImmutableSet<String> provideAllowedOauthClientIds(RegistryConfigSettings config) {
|
||||
return ImmutableSet.copyOf(config.auth.allowedOauthClientIds);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Config("oauthClientId")
|
||||
public static String provideOauthClientId(RegistryConfigSettings config) {
|
||||
@@ -1424,6 +1391,24 @@ public final class RegistryConfig {
|
||||
return config.bulkPricingPackageMonitoring.bulkPricingPackageDomainLimitUpgradeEmailBody;
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
private static String formatComments(String text) {
|
||||
return Splitter.on('\n').omitEmptyStrings().trimResults().splitToList(text).stream()
|
||||
.map(s -> "# " + s)
|
||||
@@ -1563,9 +1548,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 {
|
||||
@@ -58,9 +59,6 @@ public class RegistryConfigSettings {
|
||||
|
||||
/** Configuration options for authenticating users. */
|
||||
public static class Auth {
|
||||
public List<String> availableOauthScopes;
|
||||
public List<String> requiredOauthScopes;
|
||||
public List<String> allowedOauthClientIds;
|
||||
public List<String> allowedServiceAccountEmails;
|
||||
public String oauthClientId;
|
||||
}
|
||||
@@ -115,7 +113,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;
|
||||
@@ -208,6 +206,7 @@ public class RegistryConfigSettings {
|
||||
public static class Misc {
|
||||
public String sheetExportId;
|
||||
public boolean isEmailSendingEnabled;
|
||||
public int emailThrottleSeconds;
|
||||
public String alertRecipientEmailAddress;
|
||||
// TODO(b/279671974): remove below field after migration
|
||||
public String newAlertRecipientEmailAddress;
|
||||
@@ -263,4 +262,11 @@ public class RegistryConfigSettings {
|
||||
public String bulkPricingPackageDomainLimitUpgradeEmailSubject;
|
||||
public String bulkPricingPackageDomainLimitUpgradeEmailBody;
|
||||
}
|
||||
|
||||
/** Configurations for integration with Brand Safety Alliance (BSA) API. */
|
||||
public static class Bsa {
|
||||
public String authUrl;
|
||||
public int authTokenExpirySeconds;
|
||||
public Map<String, String> dataUrls;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,11 +189,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: false
|
||||
# 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.
|
||||
#
|
||||
@@ -304,24 +306,6 @@ caching:
|
||||
# Note: Only allowedServiceAccountEmails and oauthClientId should be configured.
|
||||
# Other fields are related to OAuth-based authentication and will be removed.
|
||||
auth:
|
||||
# Deprecated: Use OIDC-based auth instead. This field is for OAuth-based auth.
|
||||
# OAuth scopes to detect on access tokens. Superset of requiredOauthScopes.
|
||||
availableOauthScopes:
|
||||
- https://www.googleapis.com/auth/userinfo.email
|
||||
|
||||
# Deprecated: Use OIDC-based auth instead. This field is for OAuth-based auth.
|
||||
# OAuth scopes required for authenticating. Subset of availableOauthScopes.
|
||||
requiredOauthScopes:
|
||||
- https://www.googleapis.com/auth/userinfo.email
|
||||
|
||||
# Deprecated: Use OIDC-based auth instead. This field is for OAuth-based auth.
|
||||
# OAuth client IDs that are allowed to authenticate and communicate with
|
||||
# backend services, e.g. nomulus tool, EPP proxy, etc. The value in
|
||||
# registryTool.clientId field should be included in this list. Client IDs are
|
||||
# typically of the format
|
||||
# numbers-alphanumerics.apps.googleusercontent.com
|
||||
allowedOauthClientIds: []
|
||||
|
||||
# Service accounts (e.g. default service account, account used by Cloud
|
||||
# Scheduler) allowed to send authenticated requests.
|
||||
allowedServiceAccountEmails:
|
||||
@@ -443,6 +427,9 @@ misc:
|
||||
# Whether emails may be sent. For Prod and Sandbox this should be true.
|
||||
isEmailSendingEnabled: false
|
||||
|
||||
# Delay between bulk messages to avoid triggering Gmail fraud checks
|
||||
emailThrottleSeconds: 30
|
||||
|
||||
# Address we send alert summary emails to.
|
||||
alertRecipientEmailAddress: email@example.com
|
||||
|
||||
@@ -613,3 +600,14 @@ 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://"
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
# This is a sample production config (to be deployed in the WEB-INF directory).
|
||||
# This is the same as what Google Registry runs in production, except with
|
||||
# placeholders for Google-specific settings.
|
||||
|
||||
gcpProject:
|
||||
projectId: placeholder
|
||||
# Set to true if running against local servers (localhost)
|
||||
isLocal: false
|
||||
# The "<service>-dot-" prefix is used on the project ID in this URL in order
|
||||
# to get around an issue with double-wildcard SSL certs.
|
||||
defaultServiceUrl: https://domain-registry-placeholder.appspot.com
|
||||
backendServiceUrl: https://backend-dot-domain-registry-placeholder.appspot.com
|
||||
toolsServiceUrl: https://tools-dot-domain-registry-placeholder.appspot.com
|
||||
pubapiServiceUrl: https://pubapi-dot-domain-registry-placeholder.appspot.com
|
||||
|
||||
gSuite:
|
||||
domainName: placeholder
|
||||
outgoingEmailDisplayName: placeholder
|
||||
outgoingEmailAddress: placeholder
|
||||
adminAccountEmailAddress: placeholder
|
||||
supportGroupEmailAddress: placeholder
|
||||
|
||||
registryPolicy:
|
||||
contactAndHostRoidSuffix: placeholder
|
||||
productName: placeholder
|
||||
greetingServerId: placeholder
|
||||
registrarChangesNotificationEmailAddresses:
|
||||
- placeholder
|
||||
- placeholder
|
||||
defaultRegistrarWhoisServer: placeholder
|
||||
tmchCaMode: PRODUCTION
|
||||
tmchCrlUrl: http://crl.icann.org/tmch.crl
|
||||
tmchMarksDbUrl: https://ry.marksdb.org
|
||||
checkApiServletClientId: placeholder
|
||||
registryAdminClientId: placeholder
|
||||
whoisDisclaimer: |
|
||||
multi-line
|
||||
placeholder
|
||||
|
||||
icannReporting:
|
||||
icannTransactionsReportingUploadUrl: https://ry-api.icann.org/report/registrar-transactions
|
||||
icannActivityReportingUploadUrl: https://ry-api.icann.org/report/registry-functions-activity
|
||||
|
||||
oAuth:
|
||||
allowedOauthClientIds:
|
||||
- placeholder.apps.googleusercontent.com
|
||||
- placeholder-for-proxy
|
||||
|
||||
rde:
|
||||
reportUrlPrefix: https://ry-api.icann.org/report/registry-escrow-report
|
||||
uploadUrl: sftp://placeholder@sftpipm2.ironmountain.com/Outbox
|
||||
sshIdentityEmailAddress: placeholder
|
||||
|
||||
registrarConsole:
|
||||
logoFilename: placeholder
|
||||
supportPhoneNumber: placeholder
|
||||
supportEmailAddress: placeholder
|
||||
announcementsEmailAddress: placeholder
|
||||
integrationEmailAddress: placeholder
|
||||
technicalDocsUrl: https://drive.google.com/drive/folders/placeholder
|
||||
|
||||
misc:
|
||||
sheetExportId: placeholder
|
||||
|
||||
cloudDns:
|
||||
rootUrl: null
|
||||
servicePath: null
|
||||
|
||||
keyring:
|
||||
activeKeyring: KMS
|
||||
kms:
|
||||
projectId: placeholder
|
||||
|
||||
registryTool:
|
||||
clientId: placeholder.apps.googleusercontent.com
|
||||
clientSecret: placeholder
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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_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>default</service>
|
||||
<threadsafe>true</threadsafe>
|
||||
<app-engine-apis>true</app-engine-apis>
|
||||
<sessions-enabled>true</sessions-enabled>
|
||||
<instance-class>B4_1G</instance-class>
|
||||
<manual-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_1G</instance-class>
|
||||
<manual-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_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>backend</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>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>
|
||||
|
||||
@@ -59,13 +59,4 @@
|
||||
</description>
|
||||
<schedule>7 3 * * *</schedule>
|
||||
</task>
|
||||
|
||||
<task>
|
||||
<url><![CDATA[/_dr/task/wipeOutCloudSql]]></url>
|
||||
<name>wipeOutCloudSql</name>
|
||||
<description>
|
||||
This job runs an action that deletes all data in Cloud SQL.
|
||||
</description>
|
||||
<schedule>7 3 * * 6</schedule>
|
||||
</task>
|
||||
</entries>
|
||||
|
||||
@@ -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>
|
||||
@@ -22,6 +22,12 @@
|
||||
<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"/>
|
||||
|
||||
@@ -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>
|
||||
<manual-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_1G</instance-class>
|
||||
<manual-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>
|
||||
|
||||
@@ -29,7 +29,7 @@ import javax.servlet.http.HttpSession;
|
||||
service = Action.Service.DEFAULT,
|
||||
path = "/_dr/epp",
|
||||
method = Method.POST,
|
||||
auth = Auth.AUTH_API_PUBLIC)
|
||||
auth = Auth.AUTH_API_ADMIN)
|
||||
public class EppTlsAction implements Runnable {
|
||||
|
||||
@Inject @Payload byte[] inputXmlBytes;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -28,11 +28,17 @@ import google.registry.flows.EppException.CommandUseErrorException;
|
||||
import google.registry.flows.EppException.ParameterValuePolicyErrorException;
|
||||
import google.registry.flows.EppException.UnimplementedExtensionException;
|
||||
import google.registry.flows.EppException.UnimplementedObjectServiceException;
|
||||
import google.registry.flows.EppException.UnimplementedProtocolVersionException;
|
||||
import google.registry.flows.ExtensionManager;
|
||||
import google.registry.flows.FlowModule.RegistrarId;
|
||||
import google.registry.flows.FlowUtils.GenericXmlSyntaxErrorException;
|
||||
import google.registry.flows.MutatingFlow;
|
||||
import google.registry.flows.SessionMetadata;
|
||||
import google.registry.flows.TlsCredentials.BadRegistrarCertificateException;
|
||||
import google.registry.flows.TlsCredentials.BadRegistrarIpAddressException;
|
||||
import google.registry.flows.TlsCredentials.MissingRegistrarCertificateException;
|
||||
import google.registry.flows.TransportCredentials;
|
||||
import google.registry.flows.TransportCredentials.BadRegistrarPasswordException;
|
||||
import google.registry.model.eppcommon.ProtocolDefinition;
|
||||
import google.registry.model.eppcommon.ProtocolDefinition.ServiceExtension;
|
||||
import google.registry.model.eppinput.EppInput;
|
||||
@@ -41,6 +47,7 @@ import google.registry.model.eppinput.EppInput.Options;
|
||||
import google.registry.model.eppinput.EppInput.Services;
|
||||
import google.registry.model.eppoutput.EppResponse;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.util.PasswordUtils.HashAlgorithm;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import javax.inject.Inject;
|
||||
@@ -48,14 +55,14 @@ import javax.inject.Inject;
|
||||
/**
|
||||
* An EPP flow for login.
|
||||
*
|
||||
* @error {@link google.registry.flows.EppException.UnimplementedExtensionException}
|
||||
* @error {@link google.registry.flows.EppException.UnimplementedObjectServiceException}
|
||||
* @error {@link google.registry.flows.EppException.UnimplementedProtocolVersionException}
|
||||
* @error {@link google.registry.flows.FlowUtils.GenericXmlSyntaxErrorException}
|
||||
* @error {@link google.registry.flows.TlsCredentials.BadRegistrarCertificateException}
|
||||
* @error {@link google.registry.flows.TlsCredentials.BadRegistrarIpAddressException}
|
||||
* @error {@link google.registry.flows.TlsCredentials.MissingRegistrarCertificateException}
|
||||
* @error {@link google.registry.flows.TransportCredentials.BadRegistrarPasswordException}
|
||||
* @error {@link UnimplementedExtensionException}
|
||||
* @error {@link UnimplementedObjectServiceException}
|
||||
* @error {@link UnimplementedProtocolVersionException}
|
||||
* @error {@link GenericXmlSyntaxErrorException}
|
||||
* @error {@link BadRegistrarCertificateException}
|
||||
* @error {@link BadRegistrarIpAddressException}
|
||||
* @error {@link MissingRegistrarCertificateException}
|
||||
* @error {@link BadRegistrarPasswordException}
|
||||
* @error {@link LoginFlow.AlreadyLoggedInException}
|
||||
* @error {@link BadRegistrarIdException}
|
||||
* @error {@link LoginFlow.TooManyFailedLoginsException}
|
||||
@@ -134,13 +141,24 @@ public class LoginFlow implements MutatingFlow {
|
||||
if (!registrar.get().isLive()) {
|
||||
throw new RegistrarAccountNotActiveException();
|
||||
}
|
||||
if (login.getNewPassword().isPresent()) {
|
||||
|
||||
if (login.getNewPassword().isPresent()
|
||||
|| registrar.get().getCurrentHashAlgorithm(login.getPassword()).orElse(null)
|
||||
!= HashAlgorithm.SCRYPT) {
|
||||
String newPassword =
|
||||
login
|
||||
.getNewPassword()
|
||||
.orElseGet(
|
||||
() -> {
|
||||
logger.atInfo().log("Rehashing existing registrar password with Scrypt");
|
||||
return login.getPassword();
|
||||
});
|
||||
// Load fresh from database (bypassing the cache) to ensure we don't save stale data.
|
||||
Optional<Registrar> freshRegistrar = Registrar.loadByRegistrarId(login.getClientId());
|
||||
if (!freshRegistrar.isPresent()) {
|
||||
throw new BadRegistrarIdException(login.getClientId());
|
||||
}
|
||||
tm().put(freshRegistrar.get().asBuilder().setPassword(login.getNewPassword().get()).build());
|
||||
tm().put(freshRegistrar.get().asBuilder().setPassword(newPassword).build());
|
||||
}
|
||||
|
||||
// We are in!
|
||||
@@ -152,35 +170,35 @@ public class LoginFlow implements MutatingFlow {
|
||||
|
||||
/** Registrar with this ID could not be found. */
|
||||
static class BadRegistrarIdException extends AuthenticationErrorException {
|
||||
public BadRegistrarIdException(String registrarId) {
|
||||
BadRegistrarIdException(String registrarId) {
|
||||
super("Registrar with this ID could not be found: " + registrarId);
|
||||
}
|
||||
}
|
||||
|
||||
/** Registrar login failed too many times. */
|
||||
static class TooManyFailedLoginsException extends AuthenticationErrorClosingConnectionException {
|
||||
public TooManyFailedLoginsException() {
|
||||
TooManyFailedLoginsException() {
|
||||
super("Registrar login failed too many times");
|
||||
}
|
||||
}
|
||||
|
||||
/** Registrar account is not active. */
|
||||
static class RegistrarAccountNotActiveException extends AuthorizationErrorException {
|
||||
public RegistrarAccountNotActiveException() {
|
||||
RegistrarAccountNotActiveException() {
|
||||
super("Registrar account is not active");
|
||||
}
|
||||
}
|
||||
|
||||
/** Registrar is already logged in. */
|
||||
static class AlreadyLoggedInException extends CommandUseErrorException {
|
||||
public AlreadyLoggedInException() {
|
||||
AlreadyLoggedInException() {
|
||||
super("Registrar is already logged in");
|
||||
}
|
||||
}
|
||||
|
||||
/** Specified language is not supported. */
|
||||
static class UnsupportedLanguageException extends ParameterValuePolicyErrorException {
|
||||
public UnsupportedLanguageException() {
|
||||
UnsupportedLanguageException() {
|
||||
super("Specified language is not supported");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ public final class InMemoryKeyring implements Keyring {
|
||||
private final String marksdbDnlLoginAndPassword;
|
||||
private final String marksdbLordnPassword;
|
||||
private final String marksdbSmdrlLoginAndPassword;
|
||||
private final String jsonCredential;
|
||||
private final String bsaApiKey;
|
||||
|
||||
public InMemoryKeyring(
|
||||
PGPKeyPair rdeStagingKey,
|
||||
@@ -53,9 +53,9 @@ public final class InMemoryKeyring implements Keyring {
|
||||
String marksdbDnlLoginAndPassword,
|
||||
String marksdbLordnPassword,
|
||||
String marksdbSmdrlLoginAndPassword,
|
||||
String jsonCredential,
|
||||
String cloudSqlPassword,
|
||||
String toolsCloudSqlPassword) {
|
||||
String toolsCloudSqlPassword,
|
||||
String bsaApiKey) {
|
||||
checkArgument(PgpHelper.isSigningKey(rdeSigningKey.getPublicKey()),
|
||||
"RDE signing key must support signing: %s", rdeSigningKey.getKeyID());
|
||||
checkArgument(rdeStagingKey.getPublicKey().isEncryptionKey(),
|
||||
@@ -80,7 +80,7 @@ public final class InMemoryKeyring implements Keyring {
|
||||
this.marksdbLordnPassword = checkNotNull(marksdbLordnPassword, "marksdbLordnPassword");
|
||||
this.marksdbSmdrlLoginAndPassword =
|
||||
checkNotNull(marksdbSmdrlLoginAndPassword, "marksdbSmdrlLoginAndPassword");
|
||||
this.jsonCredential = checkNotNull(jsonCredential, "jsonCredential");
|
||||
this.bsaApiKey = checkNotNull(bsaApiKey, "bsaApiKey");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -149,8 +149,8 @@ public final class InMemoryKeyring implements Keyring {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getJsonCredential() {
|
||||
return jsonCredential;
|
||||
public String getBsaApiKey() {
|
||||
return bsaApiKey;
|
||||
}
|
||||
|
||||
/** Does nothing. */
|
||||
|
||||
@@ -145,11 +145,8 @@ public interface Keyring extends AutoCloseable {
|
||||
*/
|
||||
String getMarksdbSmdrlLoginAndPassword();
|
||||
|
||||
/**
|
||||
* Returns the credentials for a service account on the Google AppEngine project downloaded from
|
||||
* the Cloud Console dashboard in JSON format.
|
||||
*/
|
||||
String getJsonCredential();
|
||||
/** Returns the API_KEY for authentication with the BSA portal. */
|
||||
String getBsaApiKey();
|
||||
|
||||
// Don't throw so try-with-resources works better.
|
||||
@Override
|
||||
|
||||
@@ -58,8 +58,8 @@ public class SecretManagerKeyring implements Keyring {
|
||||
/** Key labels for string secrets. */
|
||||
enum StringKeyLabel {
|
||||
SAFE_BROWSING_API_KEY,
|
||||
BSA_API_KEY_STRING,
|
||||
ICANN_REPORTING_PASSWORD_STRING,
|
||||
JSON_CREDENTIAL_STRING,
|
||||
MARKSDB_DNL_LOGIN_STRING,
|
||||
MARKSDB_LORDN_PASSWORD_STRING,
|
||||
MARKSDB_SMDRL_LOGIN_STRING,
|
||||
@@ -143,10 +143,9 @@ public class SecretManagerKeyring implements Keyring {
|
||||
return getString(StringKeyLabel.MARKSDB_SMDRL_LOGIN_STRING);
|
||||
}
|
||||
|
||||
// TODO(b/237305940): remove this method and all supports, including entry in secretmanager
|
||||
@Override
|
||||
public String getJsonCredential() {
|
||||
return getString(StringKeyLabel.JSON_CREDENTIAL_STRING);
|
||||
public String getBsaApiKey() {
|
||||
return getString(StringKeyLabel.BSA_API_KEY_STRING);
|
||||
}
|
||||
|
||||
/** No persistent resources are maintained for this Keyring implementation. */
|
||||
|
||||
@@ -24,8 +24,8 @@ import static google.registry.keyring.secretmanager.SecretManagerKeyring.PublicK
|
||||
import static google.registry.keyring.secretmanager.SecretManagerKeyring.PublicKeyLabel.RDE_RECEIVER_PUBLIC;
|
||||
import static google.registry.keyring.secretmanager.SecretManagerKeyring.PublicKeyLabel.RDE_SIGNING_PUBLIC;
|
||||
import static google.registry.keyring.secretmanager.SecretManagerKeyring.PublicKeyLabel.RDE_STAGING_PUBLIC;
|
||||
import static google.registry.keyring.secretmanager.SecretManagerKeyring.StringKeyLabel.BSA_API_KEY_STRING;
|
||||
import static google.registry.keyring.secretmanager.SecretManagerKeyring.StringKeyLabel.ICANN_REPORTING_PASSWORD_STRING;
|
||||
import static google.registry.keyring.secretmanager.SecretManagerKeyring.StringKeyLabel.JSON_CREDENTIAL_STRING;
|
||||
import static google.registry.keyring.secretmanager.SecretManagerKeyring.StringKeyLabel.MARKSDB_DNL_LOGIN_STRING;
|
||||
import static google.registry.keyring.secretmanager.SecretManagerKeyring.StringKeyLabel.MARKSDB_LORDN_PASSWORD_STRING;
|
||||
import static google.registry.keyring.secretmanager.SecretManagerKeyring.StringKeyLabel.MARKSDB_SMDRL_LOGIN_STRING;
|
||||
@@ -120,8 +120,8 @@ public final class SecretManagerKeyringUpdater {
|
||||
return setString(login, MARKSDB_SMDRL_LOGIN_STRING);
|
||||
}
|
||||
|
||||
public SecretManagerKeyringUpdater setJsonCredential(String credential) {
|
||||
return setString(credential, JSON_CREDENTIAL_STRING);
|
||||
public SecretManagerKeyringUpdater setBsaApiKey(String credential) {
|
||||
return setString(credential, BSA_API_KEY_STRING);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -58,13 +58,13 @@ public class EntityYamlUtils {
|
||||
SimpleModule module = new SimpleModule();
|
||||
module.addSerializer(Money.class, new MoneySerializer());
|
||||
module.addDeserializer(Money.class, new MoneyDeserializer());
|
||||
module.addSerializer(Duration.class, new DurationSerializer());
|
||||
ObjectMapper mapper =
|
||||
JsonMapper.builder(new YAMLFactory().disable(Feature.WRITE_DOC_START_MARKER))
|
||||
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
|
||||
.enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY)
|
||||
.build()
|
||||
.registerModule(module);
|
||||
mapper.findAndRegisterModules();
|
||||
.build();
|
||||
mapper.findAndRegisterModules().registerModule(module);
|
||||
return mapper;
|
||||
}
|
||||
|
||||
@@ -201,6 +201,24 @@ public class EntityYamlUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/** A custom JSON serializer for a {@link Duration} object. */
|
||||
public static class DurationSerializer extends StdSerializer<Duration> {
|
||||
|
||||
public DurationSerializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public DurationSerializer(Class<Duration> t) {
|
||||
super(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(Duration value, JsonGenerator gen, SerializerProvider provider)
|
||||
throws IOException {
|
||||
gen.writeString(value.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/** A custom JSON serializer for an Optional of a {@link Duration} object. */
|
||||
public static class OptionalDurationSerializer extends StdSerializer<Optional<Duration>> {
|
||||
|
||||
@@ -216,7 +234,7 @@ public class EntityYamlUtils {
|
||||
public void serialize(Optional<Duration> value, JsonGenerator gen, SerializerProvider provider)
|
||||
throws IOException {
|
||||
if (value.isPresent()) {
|
||||
gen.writeNumber(value.get().getMillis());
|
||||
gen.writeString(value.get().toString());
|
||||
} else {
|
||||
gen.writeNull();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// 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.model.adapters;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.TypeAdapterFactory;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
/**
|
||||
* Adapter factory that allows for (de)serialization of Class objects in GSON.
|
||||
*
|
||||
* <p>GSON's built-in adapter for Class objects throws an exception, but there are situations where
|
||||
* we want to (de)serialize these, such as in VKeys. This instructs GSON to look for our custom
|
||||
* {@link ClassTypeAdapter} rather than the default.
|
||||
*/
|
||||
public class ClassProcessingTypeAdapterFactory implements TypeAdapterFactory {
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
|
||||
if (Class.class.isAssignableFrom(typeToken.getRawType())) {
|
||||
// in this case, T is a class object
|
||||
return (TypeAdapter<T>) new ClassTypeAdapter();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// 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.model.adapters;
|
||||
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* TypeAdapter for {@link Class} objects.
|
||||
*
|
||||
* <p>GSON's default adapter doesn't allow this, but we want to allow for (de)serialization of Class
|
||||
* objects for containers like VKeys using the full name of the class.
|
||||
*/
|
||||
public class ClassTypeAdapter extends TypeAdapter<Class<?>> {
|
||||
|
||||
@Override
|
||||
public void write(JsonWriter out, Class value) throws IOException {
|
||||
out.value(value.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> read(JsonReader reader) throws IOException {
|
||||
String stringValue = reader.nextString();
|
||||
if (stringValue.equals("null")) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Class.forName(stringValue);
|
||||
} catch (ClassNotFoundException e) {
|
||||
// this should not happen...
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// 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.model.adapters;
|
||||
|
||||
import google.registry.util.StringBaseTypeAdapter;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* TypeAdapter for {@link Serializable} objects.
|
||||
*
|
||||
* <p>VKey keys (primary keys in SQL) are usually represented by either a long or a String. There
|
||||
* are a couple situations (CursorId, HistoryEntryId) where the Serializable in question is a
|
||||
* complex object, but we do not need to worry about (de)serializing those objects to/from JSON.
|
||||
*/
|
||||
public class SerializableJsonTypeAdapter extends StringBaseTypeAdapter<Serializable> {
|
||||
|
||||
@Override
|
||||
protected Serializable fromString(String stringValue) throws IOException {
|
||||
try {
|
||||
return Long.parseLong(stringValue);
|
||||
} catch (NumberFormatException e) {
|
||||
return stringValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
import google.registry.model.Buildable;
|
||||
import google.registry.model.UpdateAutoTimestampEntity;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.util.PasswordUtils;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
@@ -35,21 +36,16 @@ import javax.persistence.Table;
|
||||
|
||||
/** A console user, either a registry employee or a registrar partner. */
|
||||
@Entity
|
||||
@Table(
|
||||
indexes = {
|
||||
@Index(columnList = "gaiaId", name = "user_gaia_id_idx"),
|
||||
@Index(columnList = "emailAddress", name = "user_email_address_idx")
|
||||
})
|
||||
@Table(indexes = {@Index(columnList = "emailAddress", name = "user_email_address_idx")})
|
||||
public class User extends UpdateAutoTimestampEntity implements Buildable {
|
||||
|
||||
private static final long serialVersionUID = 6936728603828566721L;
|
||||
|
||||
/** Autogenerated unique ID of this user. */
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/** GAIA ID associated with the user in question. */
|
||||
private String gaiaId;
|
||||
|
||||
/** Email address of the user in question. */
|
||||
@Column(nullable = false)
|
||||
private String emailAddress;
|
||||
@@ -71,10 +67,6 @@ public class User extends UpdateAutoTimestampEntity implements Buildable {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getGaiaId() {
|
||||
return gaiaId;
|
||||
}
|
||||
|
||||
public String getEmailAddress() {
|
||||
return emailAddress;
|
||||
}
|
||||
@@ -93,8 +85,9 @@ public class User extends UpdateAutoTimestampEntity implements Buildable {
|
||||
|| isNullOrEmpty(registryLockPasswordHash)) {
|
||||
return false;
|
||||
}
|
||||
return hashPassword(registryLockPassword, registryLockPasswordSalt)
|
||||
.equals(registryLockPasswordHash);
|
||||
return PasswordUtils.verifyPassword(
|
||||
registryLockPassword, registryLockPasswordHash, registryLockPasswordSalt)
|
||||
.isPresent();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,12 +132,6 @@ public class User extends UpdateAutoTimestampEntity implements Buildable {
|
||||
return super.build();
|
||||
}
|
||||
|
||||
public Builder setGaiaId(String gaiaId) {
|
||||
checkArgument(!isNullOrEmpty(gaiaId), "Gaia ID cannot be null or empty");
|
||||
getInstance().gaiaId = gaiaId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setEmailAddress(String emailAddress) {
|
||||
getInstance().emailAddress = checkValidEmail(emailAddress);
|
||||
return this;
|
||||
@@ -169,9 +156,9 @@ public class User extends UpdateAutoTimestampEntity implements Buildable {
|
||||
!getInstance().hasRegistryLockPassword(), "User already has a password, remove it first");
|
||||
checkArgument(
|
||||
!isNullOrEmpty(registryLockPassword), "Registry lock password was null or empty");
|
||||
getInstance().registryLockPasswordSalt = base64().encode(SALT_SUPPLIER.get());
|
||||
getInstance().registryLockPasswordHash =
|
||||
hashPassword(registryLockPassword, getInstance().registryLockPasswordSalt);
|
||||
byte[] salt = SALT_SUPPLIER.get();
|
||||
getInstance().registryLockPasswordSalt = base64().encode(salt);
|
||||
getInstance().registryLockPasswordHash = hashPassword(registryLockPassword, salt);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,12 +34,14 @@ public abstract class FeeQueryCommandExtensionItem extends ImmutableObject {
|
||||
|
||||
/** The name of a command that might have an associated fee. */
|
||||
public enum CommandName {
|
||||
UNKNOWN,
|
||||
CREATE,
|
||||
RENEW,
|
||||
TRANSFER,
|
||||
RESTORE,
|
||||
UPDATE;
|
||||
UNKNOWN(false),
|
||||
CREATE(false),
|
||||
RENEW(true),
|
||||
TRANSFER(true),
|
||||
RESTORE(true),
|
||||
UPDATE(false);
|
||||
|
||||
private final boolean loadDomainForCheck;
|
||||
|
||||
public static CommandName parseKnownCommand(String string) {
|
||||
try {
|
||||
@@ -52,6 +54,14 @@ public abstract class FeeQueryCommandExtensionItem extends ImmutableObject {
|
||||
+ " UPDATE");
|
||||
}
|
||||
}
|
||||
|
||||
CommandName(boolean loadDomainForCheck) {
|
||||
this.loadDomainForCheck = loadDomainForCheck;
|
||||
}
|
||||
|
||||
public boolean shouldLoadDomainForCheck() {
|
||||
return this.loadDomainForCheck;
|
||||
}
|
||||
}
|
||||
|
||||
/** The default validity period (if not specified) is 1 year for all operations. */
|
||||
|
||||
@@ -305,14 +305,14 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda
|
||||
new CacheLoader<VKey<AllocationToken>, Optional<AllocationToken>>() {
|
||||
@Override
|
||||
public Optional<AllocationToken> load(VKey<AllocationToken> key) {
|
||||
return tm().transact(() -> tm().loadByKeyIfPresent(key));
|
||||
return tm().reTransact(() -> tm().loadByKeyIfPresent(key));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<VKey<AllocationToken>, Optional<AllocationToken>> loadAll(
|
||||
Iterable<? extends VKey<AllocationToken>> keys) {
|
||||
ImmutableSet<VKey<AllocationToken>> keySet = ImmutableSet.copyOf(keys);
|
||||
return tm().transact(
|
||||
return tm().reTransact(
|
||||
() ->
|
||||
keySet.stream()
|
||||
.collect(
|
||||
|
||||
@@ -60,6 +60,8 @@ import google.registry.model.tld.Tld;
|
||||
import google.registry.model.tld.Tld.TldType;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.util.CidrAddressBlock;
|
||||
import google.registry.util.PasswordUtils;
|
||||
import google.registry.util.PasswordUtils.HashAlgorithm;
|
||||
import java.security.cert.CertificateParsingException;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
@@ -97,7 +99,7 @@ import org.joda.time.DateTime;
|
||||
column = @Column(nullable = false, name = "lastUpdateTime"))
|
||||
public class Registrar extends UpdateAutoTimestampEntity implements Buildable, Jsonifiable {
|
||||
|
||||
/** Represents the type of a registrar entity. */
|
||||
/** Represents the type of registrar entity. */
|
||||
public enum Type {
|
||||
/** A real-world, third-party registrar. Should have non-null IANA and billing account IDs. */
|
||||
REAL(Objects::nonNull),
|
||||
@@ -200,7 +202,11 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
|
||||
|
||||
/** A caching {@link Supplier} of a registrarId to {@link Registrar} map. */
|
||||
private static final Supplier<ImmutableMap<String, Registrar>> CACHE_BY_REGISTRAR_ID =
|
||||
memoizeWithShortExpiration(() -> Maps.uniqueIndex(loadAll(), Registrar::getRegistrarId));
|
||||
memoizeWithShortExpiration(
|
||||
() ->
|
||||
Maps.uniqueIndex(
|
||||
tm().reTransact(() -> tm().loadAllOf(Registrar.class)),
|
||||
Registrar::getRegistrarId));
|
||||
|
||||
/**
|
||||
* Unique registrar client id. Must conform to "clIDType" as defined in RFC5730.
|
||||
@@ -372,7 +378,7 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
|
||||
*/
|
||||
@Expose String icannReferralEmail;
|
||||
|
||||
/** Id of the folder in drive used to publish information for this registrar. */
|
||||
/** ID of the folder in drive used to publish information for this registrar. */
|
||||
@Expose String driveFolderId;
|
||||
|
||||
// Metadata.
|
||||
@@ -635,7 +641,11 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
|
||||
}
|
||||
|
||||
public boolean verifyPassword(String password) {
|
||||
return hashPassword(password, salt).equals(passwordHash);
|
||||
return getCurrentHashAlgorithm(password).isPresent();
|
||||
}
|
||||
|
||||
public Optional<HashAlgorithm> getCurrentHashAlgorithm(String password) {
|
||||
return PasswordUtils.verifyPassword(password, passwordHash, salt);
|
||||
}
|
||||
|
||||
public String getPhonePasscode() {
|
||||
@@ -857,8 +867,9 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
|
||||
checkArgument(
|
||||
Range.closed(6, 16).contains(nullToEmpty(password).length()),
|
||||
"Password must be 6-16 characters long.");
|
||||
getInstance().salt = base64().encode(SALT_SUPPLIER.get());
|
||||
getInstance().passwordHash = hashPassword(password, getInstance().salt);
|
||||
byte[] salt = SALT_SUPPLIER.get();
|
||||
getInstance().salt = base64().encode(salt);
|
||||
getInstance().passwordHash = hashPassword(password, salt);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,8 @@ import google.registry.model.Jsonifiable;
|
||||
import google.registry.model.UnsafeSerializable;
|
||||
import google.registry.model.registrar.RegistrarPoc.RegistrarPocId;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.util.PasswordUtils;
|
||||
import google.registry.util.PasswordUtils.HashAlgorithm;
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -240,8 +242,12 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
|
||||
|| isNullOrEmpty(registryLockPasswordHash)) {
|
||||
return false;
|
||||
}
|
||||
return hashPassword(registryLockPassword, registryLockPasswordSalt)
|
||||
.equals(registryLockPasswordHash);
|
||||
return getCurrentHashAlgorithm(registryLockPassword).isPresent();
|
||||
}
|
||||
|
||||
public Optional<HashAlgorithm> getCurrentHashAlgorithm(String registryLockPassword) {
|
||||
return PasswordUtils.verifyPassword(
|
||||
registryLockPassword, registryLockPasswordHash, registryLockPasswordSalt);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -436,9 +442,9 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
|
||||
"Not allowed to set registry lock password for this contact");
|
||||
checkArgument(
|
||||
!isNullOrEmpty(registryLockPassword), "Registry lock password was null or empty");
|
||||
getInstance().registryLockPasswordSalt = base64().encode(SALT_SUPPLIER.get());
|
||||
getInstance().registryLockPasswordHash =
|
||||
hashPassword(registryLockPassword, getInstance().registryLockPasswordSalt);
|
||||
byte[] salt = SALT_SUPPLIER.get();
|
||||
getInstance().registryLockPasswordSalt = base64().encode(salt);
|
||||
getInstance().registryLockPasswordHash = hashPassword(registryLockPassword, salt);
|
||||
getInstance().allowedToSetRegistryLockPassword = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -25,9 +25,10 @@ import com.google.common.base.Strings;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.persistence.transaction.TransactionManager.ThrowingRunnable;
|
||||
import java.io.Serializable;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.concurrent.Callable;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
@@ -184,7 +185,7 @@ public class Lock extends ImmutableObject implements Serializable {
|
||||
public static Optional<Lock> acquire(
|
||||
String resourceName, @Nullable String tld, Duration leaseLength) {
|
||||
String scope = tld != null ? tld : GLOBAL;
|
||||
Supplier<AcquireResult> lockAcquirer =
|
||||
Callable<AcquireResult> lockAcquirer =
|
||||
() -> {
|
||||
DateTime now = tm().getTransactionTime();
|
||||
|
||||
@@ -221,7 +222,7 @@ public class Lock extends ImmutableObject implements Serializable {
|
||||
/** Release the lock. */
|
||||
public void release() {
|
||||
// Just use the default clock because we aren't actually doing anything that will use the clock.
|
||||
Supplier<Void> lockReleaser =
|
||||
ThrowingRunnable lockReleaser =
|
||||
() -> {
|
||||
// To release a lock, check that no one else has already obtained it and if not
|
||||
// delete it. If the lock in the database was different, then this lock is gone already;
|
||||
@@ -246,7 +247,6 @@ public class Lock extends ImmutableObject implements Serializable {
|
||||
logger.atInfo().log(
|
||||
"Not deleting lock: %s - someone else has it: %s", lockId, loadedLock);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
tm().transact(lockReleaser);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ public class SignedMarkRevocationListDao {
|
||||
/** Loads the {@link SignedMarkRevocationList}. */
|
||||
static SignedMarkRevocationList load() {
|
||||
Optional<SignedMarkRevocationList> smdrl =
|
||||
tm().transact(
|
||||
tm().reTransact(
|
||||
() -> {
|
||||
Long revisionId =
|
||||
tm().query("SELECT MAX(revisionId) FROM SignedMarkRevocationList", Long.class)
|
||||
|
||||
@@ -130,7 +130,7 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
public static final Money DEFAULT_REGISTRY_LOCK_OR_UNLOCK_BILLING_COST = Money.of(USD, 0);
|
||||
|
||||
public boolean equalYaml(Tld tldToCompare) {
|
||||
if (this == tldToCompare) {
|
||||
if (this.equals(tldToCompare)) {
|
||||
return true;
|
||||
}
|
||||
ObjectMapper mapper = createObjectMapper();
|
||||
@@ -233,7 +233,8 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
new CacheLoader<String, Tld>() {
|
||||
@Override
|
||||
public Tld load(final String tld) {
|
||||
return tm().transact(() -> tm().loadByKeyIfPresent(createVKey(tld))).orElse(null);
|
||||
return tm().reTransact(() -> tm().loadByKeyIfPresent(createVKey(tld)))
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -241,7 +242,7 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
ImmutableMap<String, VKey<Tld>> keysMap =
|
||||
toMap(ImmutableSet.copyOf(tlds), Tld::createVKey);
|
||||
Map<VKey<? extends Tld>, Tld> entities =
|
||||
tm().transact(() -> tm().loadByKeysIfPresent(keysMap.values()));
|
||||
tm().reTransact(() -> tm().loadByKeysIfPresent(keysMap.values()));
|
||||
return Maps.transformEntries(keysMap, (k, v) -> entities.getOrDefault(v, null));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -56,7 +56,7 @@ public final class Tlds {
|
||||
private static Supplier<ImmutableMap<String, TldType>> createFreshCache() {
|
||||
return memoizeWithShortExpiration(
|
||||
() ->
|
||||
tm().transact(
|
||||
tm().reTransact(
|
||||
() -> {
|
||||
EntityManager entityManager = tm().getEntityManager();
|
||||
Stream<?> resultStream =
|
||||
|
||||
@@ -43,8 +43,6 @@ import google.registry.rde.JSchModule;
|
||||
import google.registry.request.Modules.GsonModule;
|
||||
import google.registry.request.Modules.NetHttpTransportModule;
|
||||
import google.registry.request.Modules.UrlConnectionServiceModule;
|
||||
import google.registry.request.Modules.UrlFetchServiceModule;
|
||||
import google.registry.request.Modules.UrlFetchTransportModule;
|
||||
import google.registry.request.Modules.UserServiceModule;
|
||||
import google.registry.request.auth.AuthModule;
|
||||
import google.registry.util.UtilsModule;
|
||||
@@ -80,8 +78,6 @@ import javax.inject.Singleton;
|
||||
SheetsServiceModule.class,
|
||||
StackdriverModule.class,
|
||||
UrlConnectionServiceModule.class,
|
||||
UrlFetchServiceModule.class,
|
||||
UrlFetchTransportModule.class,
|
||||
UserServiceModule.class,
|
||||
VoidDnsWriterModule.class,
|
||||
UtilsModule.class
|
||||
|
||||
@@ -26,7 +26,6 @@ import google.registry.batch.RelockDomainAction;
|
||||
import google.registry.batch.ResaveAllEppResourcesPipelineAction;
|
||||
import google.registry.batch.ResaveEntityAction;
|
||||
import google.registry.batch.SendExpiringCertificateNotificationEmailAction;
|
||||
import google.registry.batch.WipeOutCloudSqlAction;
|
||||
import google.registry.batch.WipeOutContactHistoryPiiAction;
|
||||
import google.registry.cron.CronModule;
|
||||
import google.registry.cron.TldFanoutAction;
|
||||
@@ -178,8 +177,6 @@ interface BackendRequestComponent {
|
||||
|
||||
UpdateRegistrarRdapBaseUrlsAction updateRegistrarRdapBaseUrlsAction();
|
||||
|
||||
WipeOutCloudSqlAction wipeOutCloudSqlAction();
|
||||
|
||||
WipeOutContactHistoryPiiAction wipeOutContactHistoryPiiAction();
|
||||
|
||||
@Subcomponent.Builder
|
||||
|
||||
@@ -26,6 +26,7 @@ import google.registry.request.RequestComponentBuilder;
|
||||
import google.registry.request.RequestModule;
|
||||
import google.registry.request.RequestScope;
|
||||
import google.registry.ui.server.console.ConsoleDomainGetAction;
|
||||
import google.registry.ui.server.console.ConsoleDomainListAction;
|
||||
import google.registry.ui.server.console.ConsoleUserDataAction;
|
||||
import google.registry.ui.server.console.RegistrarsAction;
|
||||
import google.registry.ui.server.console.settings.ContactAction;
|
||||
@@ -55,6 +56,8 @@ import google.registry.ui.server.registrar.RegistryLockVerifyAction;
|
||||
interface FrontendRequestComponent {
|
||||
ConsoleDomainGetAction consoleDomainGetAction();
|
||||
|
||||
ConsoleDomainListAction consoleDomainListAction();
|
||||
|
||||
ConsoleOteSetupAction consoleOteSetupAction();
|
||||
ConsoleRegistrarCreatorAction consoleRegistrarCreatorAction();
|
||||
ConsoleUiAction consoleUiAction();
|
||||
|
||||
@@ -57,7 +57,7 @@ public class VKey<T> extends ImmutableObject implements Serializable {
|
||||
// The primary key for the referenced entity.
|
||||
@Expose Serializable key;
|
||||
|
||||
Class<? extends T> kind;
|
||||
@Expose Class<? extends T> kind;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
VKey() {}
|
||||
|
||||
@@ -62,7 +62,7 @@ class DatabaseException extends PersistenceException {
|
||||
* <p>If the {@code original Throwable} has at least one {@link SQLException} in its chain of
|
||||
* causes, a {@link DatabaseException} is thrown; otherwise this does nothing.
|
||||
*/
|
||||
static void tryWrapAndThrow(Throwable original) {
|
||||
static void throwIfSqlException(Throwable original) {
|
||||
Throwable t = original;
|
||||
do {
|
||||
if (t instanceof SQLException) {
|
||||
|
||||
@@ -16,7 +16,6 @@ package google.registry.persistence.transaction;
|
||||
|
||||
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
|
||||
import google.registry.persistence.VKey;
|
||||
import java.util.function.Supplier;
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.Query;
|
||||
import javax.persistence.TypedQuery;
|
||||
@@ -62,24 +61,6 @@ public interface JpaTransactionManager extends TransactionManager {
|
||||
*/
|
||||
Query query(String sqlString);
|
||||
|
||||
/** Executes the work in a transaction with no retries and returns the result. */
|
||||
<T> T transactNoRetry(Supplier<T> work);
|
||||
|
||||
/**
|
||||
* Executes the work in a transaction at the given {@link TransactionIsolationLevel} with no
|
||||
* retries and returns the result.
|
||||
*/
|
||||
<T> T transactNoRetry(Supplier<T> work, TransactionIsolationLevel isolationLevel);
|
||||
|
||||
/** Executes the work in a transaction with no retries. */
|
||||
void transactNoRetry(Runnable work);
|
||||
|
||||
/**
|
||||
* Executes the work in a transaction at the given {@link TransactionIsolationLevel} with no
|
||||
* retries.
|
||||
*/
|
||||
void transactNoRetry(Runnable work, TransactionIsolationLevel isolationLevel);
|
||||
|
||||
/** Deletes the entity by its id, throws exception if the entity is not deleted. */
|
||||
<T> void assertDelete(VKey<T> key);
|
||||
|
||||
@@ -103,7 +84,4 @@ public interface JpaTransactionManager extends TransactionManager {
|
||||
|
||||
/** Return the {@link TransactionIsolationLevel} used in the current transaction. */
|
||||
TransactionIsolationLevel getCurrentTransactionIsolationLevel();
|
||||
|
||||
/** Asserts that the current transaction runs at the given level. */
|
||||
void assertTransactionIsolationLevel(TransactionIsolationLevel expectedLevel);
|
||||
}
|
||||
|
||||
@@ -15,11 +15,12 @@
|
||||
package google.registry.persistence.transaction;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Throwables.throwIfUnchecked;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.collect.ImmutableMap.toImmutableMap;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static google.registry.config.RegistryConfig.getHibernatePerTransactionIsolationEnabled;
|
||||
import static google.registry.persistence.transaction.DatabaseException.tryWrapAndThrow;
|
||||
import static google.registry.config.RegistryConfig.getHibernateAllowNestedTransactions;
|
||||
import static google.registry.persistence.transaction.DatabaseException.throwIfSqlException;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
import static java.util.AbstractMap.SimpleEntry;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
@@ -31,6 +32,7 @@ import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Streams;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.flogger.StackSize;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.persistence.JpaRetries;
|
||||
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
|
||||
@@ -52,7 +54,7 @@ import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
import javax.annotation.Nullable;
|
||||
@@ -76,6 +78,9 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
private static final Retrier retrier = new Retrier(new SystemSleeper(), 3);
|
||||
private static final String NESTED_TRANSACTION_MESSAGE =
|
||||
"Nested transaction detected. Try refactoring to avoid nested transactions. If unachievable,"
|
||||
+ " use reTransact() in nested transactions";
|
||||
|
||||
// EntityManagerFactory is thread safe.
|
||||
private final EntityManagerFactory emf;
|
||||
@@ -138,21 +143,23 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assertTransactionIsolationLevel(TransactionIsolationLevel expectedLevel) {
|
||||
assertInTransaction();
|
||||
TransactionIsolationLevel currentLevel = getCurrentTransactionIsolationLevel();
|
||||
if (currentLevel != expectedLevel) {
|
||||
throw new IllegalStateException(
|
||||
String.format(
|
||||
"Current transaction isolation level (%s) is not as expected (%s)",
|
||||
currentLevel, expectedLevel));
|
||||
public <T> T reTransact(Callable<T> work) {
|
||||
// This prevents inner transaction from retrying, thus avoiding a cascade retry effect.
|
||||
if (inTransaction()) {
|
||||
return transactNoRetry(work, null);
|
||||
}
|
||||
return retrier.callWithRetry(
|
||||
() -> transactNoRetry(work, null), JpaRetries::isFailedTxnRetriable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T transact(Supplier<T> work, TransactionIsolationLevel isolationLevel) {
|
||||
// This prevents inner transaction from retrying, thus avoiding a cascade retry effect.
|
||||
public <T> T transact(Callable<T> work, TransactionIsolationLevel isolationLevel) {
|
||||
if (inTransaction()) {
|
||||
if (!getHibernateAllowNestedTransactions()) {
|
||||
throw new IllegalStateException(NESTED_TRANSACTION_MESSAGE);
|
||||
}
|
||||
logger.atWarning().withStackTrace(StackSize.MEDIUM).log(NESTED_TRANSACTION_MESSAGE);
|
||||
// This prevents inner transaction from retrying, thus avoiding a cascade retry effect.
|
||||
return transactNoRetry(work, isolationLevel);
|
||||
}
|
||||
return retrier.callWithRetry(
|
||||
@@ -160,30 +167,32 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T reTransact(Supplier<T> work) {
|
||||
return transact(work);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T transact(Supplier<T> work) {
|
||||
public <T> T transact(Callable<T> work) {
|
||||
return transact(work, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T transactNoRetry(
|
||||
Supplier<T> work, @Nullable TransactionIsolationLevel isolationLevel) {
|
||||
Callable<T> work, @Nullable TransactionIsolationLevel isolationLevel) {
|
||||
if (inTransaction()) {
|
||||
if (isolationLevel != null && getHibernatePerTransactionIsolationEnabled()) {
|
||||
TransactionIsolationLevel enclosingLevel = getCurrentTransactionIsolationLevel();
|
||||
if (isolationLevel != enclosingLevel) {
|
||||
throw new IllegalStateException(
|
||||
String.format(
|
||||
"Isolation level conflict detected in nested transactions.\n"
|
||||
+ "Enclosing transaction: %s\nCurrent transaction: %s",
|
||||
enclosingLevel, isolationLevel));
|
||||
}
|
||||
// This check will no longer be necessary when the transact() method always throws
|
||||
// inside a nested transaction, as the only way to pass a non-null isolation level
|
||||
// is by calling the transact() method (and its variants), which would have already
|
||||
// thrown before calling transactNoRetry() when inside a nested transaction.
|
||||
//
|
||||
// For now, we still need it, so we don't accidentally call a nested transact() with an
|
||||
// isolation level override. This buys us time to detect nested transact() calls and either
|
||||
// remove them or change the call site to reTransact().
|
||||
if (isolationLevel != null) {
|
||||
throw new IllegalStateException(
|
||||
"Transaction isolation level cannot be specified for nested transactions");
|
||||
}
|
||||
try {
|
||||
return work.call();
|
||||
} catch (Exception e) {
|
||||
throwIfSqlException(e);
|
||||
throwIfUnchecked(e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return work.get();
|
||||
}
|
||||
TransactionInfo txnInfo = transactionInfo.get();
|
||||
txnInfo.entityManager = emf.createEntityManager();
|
||||
@@ -191,43 +200,36 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
try {
|
||||
txn.begin();
|
||||
txnInfo.start(clock);
|
||||
if (isolationLevel != null) {
|
||||
if (getHibernatePerTransactionIsolationEnabled()) {
|
||||
getEntityManager()
|
||||
.createNativeQuery(
|
||||
String.format("SET TRANSACTION ISOLATION LEVEL %s", isolationLevel.getMode()))
|
||||
.executeUpdate();
|
||||
logger.atInfo().log("Running transaction at %s", isolationLevel);
|
||||
} else {
|
||||
logger.atWarning().log(
|
||||
"Per-transaction isolation level disabled, but %s was requested", isolationLevel);
|
||||
}
|
||||
if (isolationLevel != null && isolationLevel != getDefaultTransactionIsolationLevel()) {
|
||||
getEntityManager()
|
||||
.createNativeQuery(
|
||||
String.format("SET TRANSACTION ISOLATION LEVEL %s", isolationLevel.getMode()))
|
||||
.executeUpdate();
|
||||
logger.atInfo().log(
|
||||
"Overriding transaction isolation level from %s to %s",
|
||||
getDefaultTransactionIsolationLevel(), isolationLevel);
|
||||
}
|
||||
T result = work.get();
|
||||
T result = work.call();
|
||||
txn.commit();
|
||||
return result;
|
||||
} catch (RuntimeException | Error e) {
|
||||
// Error is unchecked!
|
||||
} catch (Throwable e) {
|
||||
// Catch a Throwable here so even Errors would lead to a rollback.
|
||||
try {
|
||||
txn.rollback();
|
||||
logger.atWarning().log("Error during transaction; transaction rolled back.");
|
||||
} catch (Throwable rollbackException) {
|
||||
} catch (Exception rollbackException) {
|
||||
logger.atSevere().withCause(rollbackException).log("Rollback failed; suppressing error.");
|
||||
}
|
||||
tryWrapAndThrow(e);
|
||||
throw e;
|
||||
throwIfSqlException(e);
|
||||
throwIfUnchecked(e);
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
txnInfo.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T transactNoRetry(Supplier<T> work) {
|
||||
return transactNoRetry(work, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transact(Runnable work, TransactionIsolationLevel isolationLevel) {
|
||||
public void transact(ThrowingRunnable work, TransactionIsolationLevel isolationLevel) {
|
||||
transact(
|
||||
() -> {
|
||||
work.run();
|
||||
@@ -237,28 +239,17 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reTransact(Runnable work) {
|
||||
transact(work);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transact(Runnable work) {
|
||||
public void transact(ThrowingRunnable work) {
|
||||
transact(work, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transactNoRetry(Runnable work, TransactionIsolationLevel isolationLevel) {
|
||||
transactNoRetry(
|
||||
public void reTransact(ThrowingRunnable work) {
|
||||
reTransact(
|
||||
() -> {
|
||||
work.run();
|
||||
return null;
|
||||
},
|
||||
isolationLevel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transactNoRetry(Runnable work) {
|
||||
transactNoRetry(work, null);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -22,7 +22,7 @@ import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
|
||||
import google.registry.persistence.VKey;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.stream.Stream;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
@@ -48,51 +48,51 @@ public interface TransactionManager {
|
||||
void assertInTransaction();
|
||||
|
||||
/** Executes the work in a transaction and returns the result. */
|
||||
<T> T transact(Supplier<T> work);
|
||||
<T> T transact(Callable<T> work);
|
||||
|
||||
/**
|
||||
* Executes the work in a transaction at the given {@link TransactionIsolationLevel} and returns
|
||||
* the result.
|
||||
*/
|
||||
<T> T transact(Supplier<T> work, TransactionIsolationLevel isolationLevel);
|
||||
<T> T transact(Callable<T> work, TransactionIsolationLevel isolationLevel);
|
||||
|
||||
/**
|
||||
* Executes the work in a (potentially wrapped) transaction and returns the result.
|
||||
*
|
||||
* <p>Calls to this method are typically going to be in inner functions, that are called either as
|
||||
* top-level transactions themselves or are nested inside of larger transactions (e.g. a
|
||||
* top-level transactions themselves or are nested inside larger transactions (e.g. a
|
||||
* transactional flow). Invocations of reTransact must be vetted to occur in both situations and
|
||||
* with such complexity that it is not trivial to refactor out the nested transaction calls. New
|
||||
* code should be written in such a way as to avoid requiring reTransact in the first place.
|
||||
*
|
||||
* <p>In the future we will be enforcing that {@link #transact(Supplier)} calls be top-level only,
|
||||
* <p>In the future we will be enforcing that {@link #transact(Callable)} calls be top-level only,
|
||||
* with reTransact calls being the only ones that can potentially be an inner nested transaction
|
||||
* (which is a noop). Note that, as this can be a nested inner exception, there is no overload
|
||||
* provided to specify a (potentially conflicting) transaction isolation level.
|
||||
*/
|
||||
<T> T reTransact(Supplier<T> work);
|
||||
<T> T reTransact(Callable<T> work);
|
||||
|
||||
/** Executes the work in a transaction. */
|
||||
void transact(Runnable work);
|
||||
void transact(ThrowingRunnable work);
|
||||
|
||||
/** Executes the work in a transaction at the given {@link TransactionIsolationLevel}. */
|
||||
void transact(Runnable work, TransactionIsolationLevel isolationLevel);
|
||||
void transact(ThrowingRunnable work, TransactionIsolationLevel isolationLevel);
|
||||
|
||||
/**
|
||||
* Executes the work in a (potentially wrapped) transaction and returns the result.
|
||||
*
|
||||
* <p>Calls to this method are typically going to be in inner functions, that are called either as
|
||||
* top-level transactions themselves or are nested inside of larger transactions (e.g. a
|
||||
* top-level transactions themselves or are nested inside larger transactions (e.g. a
|
||||
* transactional flow). Invocations of reTransact must be vetted to occur in both situations and
|
||||
* with such complexity that it is not trivial to refactor out the nested transaction calls. New
|
||||
* code should be written in such a way as to avoid requiring reTransact in the first place.
|
||||
*
|
||||
* <p>In the future we will be enforcing that {@link #transact(Runnable)} calls be top-level only,
|
||||
* with reTransact calls being the only ones that can potentially be an inner nested transaction
|
||||
* (which is a noop). Note that, as this can be a nested inner exception, there is no overload *
|
||||
* provided to specify a (potentially conflicting) transaction isolation level.
|
||||
* <p>In the future we will be enforcing that {@link #transact(ThrowingRunnable)} calls be
|
||||
* top-level only, with reTransact calls being the only ones that can potentially be an inner
|
||||
* nested transaction (which is a noop). Note that, as this can be a nested inner exception, there
|
||||
* is no overload provided to specify a (potentially conflicting) transaction isolation level.
|
||||
*/
|
||||
void reTransact(Runnable work);
|
||||
void reTransact(ThrowingRunnable work);
|
||||
|
||||
/** Returns the time associated with the start of this particular transaction attempt. */
|
||||
DateTime getTransactionTime();
|
||||
@@ -216,4 +216,15 @@ public interface TransactionManager {
|
||||
|
||||
/** Returns a QueryComposer which can be used to perform queries against the current database. */
|
||||
<T> QueryComposer<T> createQueryComposer(Class<T> entity);
|
||||
|
||||
/**
|
||||
* A runnable that allows for checked exceptions to be thrown.
|
||||
*
|
||||
* <p>This makes it easier to write lambdas without having to worry about wrapping and re-throwing
|
||||
* checked excpetions as unchecked ones.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
interface ThrowingRunnable {
|
||||
void run() throws Exception;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,22 +14,26 @@
|
||||
|
||||
package google.registry.rdap;
|
||||
|
||||
import static com.google.api.client.http.HttpStatusCodes.STATUS_CODE_OK;
|
||||
import static com.google.common.net.HttpHeaders.ACCEPT_ENCODING;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.google.api.client.http.GenericUrl;
|
||||
import com.google.api.client.http.HttpRequest;
|
||||
import com.google.api.client.http.HttpResponse;
|
||||
import com.google.api.client.http.HttpTransport;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.HttpException.InternalServerErrorException;
|
||||
import google.registry.request.UrlConnectionService;
|
||||
import google.registry.request.UrlConnectionUtils;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.util.UrlConnectionException;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.security.GeneralSecurityException;
|
||||
import javax.inject.Inject;
|
||||
import org.apache.commons.csv.CSVFormat;
|
||||
import org.apache.commons.csv.CSVParser;
|
||||
@@ -41,9 +45,9 @@ import org.apache.commons.csv.CSVRecord;
|
||||
* <p>This will update ALL the REAL registrars. If a REAL registrar doesn't have an RDAP entry in
|
||||
* MoSAPI, we'll delete any BaseUrls it has.
|
||||
*
|
||||
* <p>The ICANN base website that provides this information can be found at
|
||||
* https://www.iana.org/assignments/registrar-ids/registrar-ids.xhtml. The provided CSV endpoint
|
||||
* requires no authentication.
|
||||
* <p>The ICANN base website that provides this information can be found at <a
|
||||
* href=https://www.iana.org/assignments/registrar-ids/registrar-ids.xhtml>here</a>. The provided
|
||||
* CSV endpoint requires no authentication.
|
||||
*/
|
||||
@Action(
|
||||
service = Action.Service.BACKEND,
|
||||
@@ -52,22 +56,26 @@ import org.apache.commons.csv.CSVRecord;
|
||||
auth = Auth.AUTH_API_ADMIN)
|
||||
public final class UpdateRegistrarRdapBaseUrlsAction implements Runnable {
|
||||
|
||||
private static final GenericUrl RDAP_IDS_URL =
|
||||
new GenericUrl("https://www.iana.org/assignments/registrar-ids/registrar-ids-1.csv");
|
||||
private static final String RDAP_IDS_URL =
|
||||
"https://www.iana.org/assignments/registrar-ids/registrar-ids-1.csv";
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
@Inject HttpTransport httpTransport;
|
||||
@Inject UrlConnectionService urlConnectionService;
|
||||
|
||||
@Inject
|
||||
UpdateRegistrarRdapBaseUrlsAction() {}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
ImmutableMap<String, String> ianaIdsToUrls = getIanaIdsToUrls();
|
||||
tm().transact(() -> processAllRegistrars(ianaIdsToUrls));
|
||||
try {
|
||||
ImmutableMap<String, String> ianaIdsToUrls = getIanaIdsToUrls();
|
||||
tm().transact(() -> processAllRegistrars(ianaIdsToUrls));
|
||||
} catch (Exception e) {
|
||||
throw new InternalServerErrorException("Error when retrieving RDAP base URL CSV file", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void processAllRegistrars(ImmutableMap<String, String> ianaIdsToUrls) {
|
||||
private static void processAllRegistrars(ImmutableMap<String, String> ianaIdsToUrls) {
|
||||
int nonUpdatedRegistrars = 0;
|
||||
for (Registrar registrar : Registrar.loadAll()) {
|
||||
// Only update REAL registrars
|
||||
@@ -95,23 +103,28 @@ public final class UpdateRegistrarRdapBaseUrlsAction implements Runnable {
|
||||
logger.atInfo().log("No change in RDAP base URLs for %d registrars", nonUpdatedRegistrars);
|
||||
}
|
||||
|
||||
private ImmutableMap<String, String> getIanaIdsToUrls() {
|
||||
private ImmutableMap<String, String> getIanaIdsToUrls()
|
||||
throws IOException, GeneralSecurityException {
|
||||
CSVParser csv;
|
||||
HttpURLConnection connection = urlConnectionService.createConnection(new URL(RDAP_IDS_URL));
|
||||
// Explictly set the accepted encoding, as we know Brotli causes us problems when talking to
|
||||
// ICANN.
|
||||
connection.setRequestProperty(ACCEPT_ENCODING, "gzip");
|
||||
String csvString;
|
||||
try {
|
||||
HttpRequest request = httpTransport.createRequestFactory().buildGetRequest(RDAP_IDS_URL);
|
||||
// AppEngine might insert accept-encodings for us if we use the default gzip, so remove it
|
||||
request.getHeaders().setAcceptEncoding(null);
|
||||
HttpResponse response = request.execute();
|
||||
String csvString = new String(ByteStreams.toByteArray(response.getContent()), UTF_8);
|
||||
csv =
|
||||
CSVFormat.Builder.create(CSVFormat.DEFAULT)
|
||||
.setHeader()
|
||||
.setSkipHeaderRecord(true)
|
||||
.build()
|
||||
.parse(new StringReader(csvString));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Error when retrieving RDAP base URL CSV file", e);
|
||||
if (connection.getResponseCode() != STATUS_CODE_OK) {
|
||||
throw new UrlConnectionException("Failed to load RDAP base URLs from ICANN", connection);
|
||||
}
|
||||
csvString = new String(UrlConnectionUtils.getResponseBytes(connection), UTF_8);
|
||||
} finally {
|
||||
connection.disconnect();
|
||||
}
|
||||
csv =
|
||||
CSVFormat.Builder.create(CSVFormat.DEFAULT)
|
||||
.setHeader()
|
||||
.setSkipHeaderRecord(true)
|
||||
.build()
|
||||
.parse(new StringReader(csvString));
|
||||
ImmutableMap.Builder<String, String> result = new ImmutableMap.Builder<>();
|
||||
for (CSVRecord record : csv) {
|
||||
String ianaIdentifierString = record.get("ID");
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user