mirror of
https://github.com/google/nomulus
synced 2026-06-09 16:33:02 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f5839777d1 | |||
| 43d325d2a5 | |||
| 9b17adcb28 | |||
| 9873772150 | |||
| 342051e11d | |||
| 5f5cb8df9f | |||
| 311d5ac9b6 |
@@ -31,6 +31,7 @@ tmp/
|
||||
local.properties
|
||||
.settings/
|
||||
.loadpath
|
||||
.DS_Store
|
||||
|
||||
# Eclipse Core
|
||||
.project
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
"/console-api":
|
||||
{
|
||||
"target": "http://localhost:8080",
|
||||
"secure": true
|
||||
"secure": false,
|
||||
"logLevel": "debug",
|
||||
"changeOrigin": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"start": "ng serve --proxy-config dev-proxy.config.json",
|
||||
"build": "ng build --base-href=/console/",
|
||||
"build:local": "ng build --base-href=/default/console/",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="console-app">
|
||||
<app-header (toggleNavOpen)="sidenav.toggle()"></app-header>
|
||||
<mat-sidenav-container class="console-app__content-wrapper">
|
||||
<mat-sidenav-container class="console-app__container">
|
||||
<mat-sidenav #sidenav class="console-app__sidebar">
|
||||
<mat-nav-list>
|
||||
<a mat-list-item [routerLink]="'/home'" routerLinkActive="active">
|
||||
@@ -17,8 +17,10 @@
|
||||
</a>
|
||||
</mat-nav-list>
|
||||
</mat-sidenav>
|
||||
<mat-sidenav-content class="console-app__content">
|
||||
<router-outlet></router-outlet>
|
||||
<mat-sidenav-content class="console-app__content-wrapper">
|
||||
<div class="console-app__content">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</mat-sidenav-content>
|
||||
</mat-sidenav-container>
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
&__content-wrapper {
|
||||
&__container {
|
||||
flex: 1;
|
||||
margin-top: -12px;
|
||||
padding-bottom: 36px;
|
||||
@@ -40,7 +40,11 @@
|
||||
background: #eae1e1;
|
||||
}
|
||||
}
|
||||
&__content {
|
||||
&__content-wrapper {
|
||||
margin: 12px 24px;
|
||||
}
|
||||
&__content {
|
||||
max-width: 1340px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,13 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { MaterialModule } from './material.module';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RouterTestingModule],
|
||||
imports: [RouterTestingModule, MaterialModule, BrowserAnimationsModule],
|
||||
declarations: [AppComponent],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
@@ -33,6 +33,7 @@ import SettingsContactComponent, {
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { RegistrarComponent } from './registrar/registrar.component';
|
||||
import { RegistrarGuard } from './registrar/registrar.guard';
|
||||
import SecurityComponent from './settings/security/security.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -44,6 +45,7 @@ import { RegistrarGuard } from './registrar/registrar.guard';
|
||||
SettingsContactComponent,
|
||||
ContactDetailsDialogComponent,
|
||||
RegistrarComponent,
|
||||
SecurityComponent,
|
||||
],
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HeaderComponent } from './header.component';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { MaterialModule } from '../material.module';
|
||||
|
||||
describe('HeaderComponent', () => {
|
||||
let component: HeaderComponent;
|
||||
@@ -22,6 +24,7 @@ describe('HeaderComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MaterialModule, BrowserAnimationsModule],
|
||||
declarations: [HeaderComponent],
|
||||
}).compileComponents();
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ import { RegistrarComponent } from './registrar.component';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { MaterialModule } from '../material.module';
|
||||
|
||||
describe('RegistrarComponent', () => {
|
||||
let component: RegistrarComponent;
|
||||
@@ -26,7 +28,11 @@ describe('RegistrarComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [RegistrarComponent],
|
||||
imports: [HttpClientTestingModule],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
MaterialModule,
|
||||
BrowserAnimationsModule,
|
||||
],
|
||||
providers: [
|
||||
BackendService,
|
||||
{ provide: ActivatedRoute, useValue: {} as ActivatedRoute },
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
>
|
||||
</section>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button (click)="onClose()">Cancel</button>
|
||||
<button mat-button (click)="onClose($event)">Cancel</button>
|
||||
<button type="submit" mat-button>Save</button>
|
||||
</mat-dialog-actions>
|
||||
</form>
|
||||
|
||||
@@ -80,11 +80,11 @@ class ContactDetailsEventsResponder {
|
||||
styleUrls: ['./contact.component.less'],
|
||||
})
|
||||
export class ContactDetailsDialogComponent {
|
||||
onClose!: Function;
|
||||
contact: Contact;
|
||||
contactTypes = contactTypes;
|
||||
operation: Operations;
|
||||
contactIndex: number;
|
||||
onCloseCallback: Function;
|
||||
|
||||
constructor(
|
||||
public contactService: ContactService,
|
||||
@@ -96,7 +96,7 @@ export class ContactDetailsDialogComponent {
|
||||
operation: Operations;
|
||||
}
|
||||
) {
|
||||
this.onClose = data.onClose;
|
||||
this.onCloseCallback = data.onClose;
|
||||
this.contactIndex = contactService.contacts.findIndex(
|
||||
(c) => c === data.contact
|
||||
);
|
||||
@@ -104,9 +104,14 @@ export class ContactDetailsDialogComponent {
|
||||
this.operation = data.operation;
|
||||
}
|
||||
|
||||
saveAndClose(e: any) {
|
||||
onClose(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
if (!e.target.checkValidity()) {
|
||||
this.onCloseCallback.call(this);
|
||||
}
|
||||
|
||||
saveAndClose(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
if (!(e.target as HTMLFormElement).checkValidity()) {
|
||||
return;
|
||||
}
|
||||
let operationObservable;
|
||||
@@ -122,7 +127,7 @@ export class ContactDetailsDialogComponent {
|
||||
}
|
||||
|
||||
operationObservable.subscribe({
|
||||
complete: this.onClose.bind(this),
|
||||
complete: this.onCloseCallback.bind(this),
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.statusText, undefined, {
|
||||
duration: 1500,
|
||||
@@ -169,7 +174,7 @@ export default class ContactComponent {
|
||||
}
|
||||
}
|
||||
|
||||
openCreateNew(e: Event) {
|
||||
openCreateNew(e: MouseEvent) {
|
||||
const newContact: Contact = {
|
||||
name: '',
|
||||
phoneNumber: '',
|
||||
@@ -180,7 +185,7 @@ export default class ContactComponent {
|
||||
}
|
||||
|
||||
openDetails(
|
||||
e: Event,
|
||||
e: MouseEvent,
|
||||
contact: Contact,
|
||||
operation: Operations = Operations.UPDATE
|
||||
) {
|
||||
|
||||
@@ -1 +1,96 @@
|
||||
<p>security works!</p>
|
||||
<div *ngIf="loading" class="settings-security__loading">
|
||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||
</div>
|
||||
<div class="settings-security" *ngIf="!loading">
|
||||
<div class="settings-security__section">
|
||||
<div class="settings-security__section-description">
|
||||
<h2>IP Allowlist</h2>
|
||||
<p>
|
||||
Restrict access to EPP production servers to the following IP/IPv6
|
||||
addresses, or ranges like 1.1.1.0/24
|
||||
</p>
|
||||
</div>
|
||||
<div class="settings-security__section-form">
|
||||
<div
|
||||
*ngIf="
|
||||
dataSource.ipAddressAllowList &&
|
||||
dataSource.ipAddressAllowList.length > 0
|
||||
"
|
||||
>
|
||||
<div *ngFor="let item of dataSource.ipAddressAllowList; index as index">
|
||||
<div>{{ item.value }}</div>
|
||||
<mat-form-field>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
class="settings-security__ip-allowlist"
|
||||
[(ngModel)]="item.value"
|
||||
[disabled]="!inEdit"
|
||||
/>
|
||||
<button
|
||||
*ngIf="inEdit"
|
||||
matSuffix
|
||||
mat-icon-button
|
||||
aria-label="Remove"
|
||||
(click)="removeIpEntry(index)"
|
||||
>
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<button mat-stroked-button (click)="enableEdit(); createIpEntry()">
|
||||
Add IP
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-security__section">
|
||||
<div class="settings-security__section-description">
|
||||
<h2>SSL Certificate</h2>
|
||||
<p>X.509 PEM certificate for EPP production access.</p>
|
||||
</div>
|
||||
<div class="settings-security__section-form">
|
||||
<textarea
|
||||
matInput
|
||||
class="settings-security__clientCertificate"
|
||||
[(ngModel)]="dataSource.clientCertificate"
|
||||
[disabled]="!inEdit"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-security__section">
|
||||
<div class="settings-security__section-description">
|
||||
<h2>Failover SSL Certificate</h2>
|
||||
<p>X.509 PEM backup certificate for EPP Production Access.</p>
|
||||
</div>
|
||||
<div class="settings-security__section-form">
|
||||
<textarea
|
||||
matInput
|
||||
[(ngModel)]="dataSource.failoverClientCertificate"
|
||||
[disabled]="!inEdit"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-security__actions">
|
||||
<ng-template [ngIf]="inEdit" [ngIfElse]="inView">
|
||||
<button
|
||||
class="settings-security__actions-save"
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
(click)="save()"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
class="settings-security__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,40 @@
|
||||
// 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-security {
|
||||
margin-top: 1.5rem;
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
&__section {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
margin-bottom: 3rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
&__section-description {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
}
|
||||
&__section-form {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
textarea {
|
||||
min-height: 100%;
|
||||
min-width: 100%;
|
||||
box-sizing: border-box;
|
||||
min-height: 100px;
|
||||
}
|
||||
}
|
||||
&__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
button {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
&__loading {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,18 +12,56 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import SecurityComponent from './security.component';
|
||||
import { SecurityService } from './security.service';
|
||||
import { BackendService } from 'src/app/shared/services/backend.service';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { MaterialModule } from 'src/app/material.module';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { of } from 'rxjs';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
describe('SecurityComponent', () => {
|
||||
let component: SecurityComponent;
|
||||
let fixture: ComponentFixture<SecurityComponent>;
|
||||
let fetchSecurityDetailsSpy: Function;
|
||||
let saveSpy: Function;
|
||||
|
||||
beforeEach(async () => {
|
||||
const securityServiceSpy = jasmine.createSpyObj(SecurityService, [
|
||||
'fetchSecurityDetails',
|
||||
'saveChanges',
|
||||
]);
|
||||
|
||||
fetchSecurityDetailsSpy =
|
||||
securityServiceSpy.fetchSecurityDetails.and.returnValue(of());
|
||||
|
||||
saveSpy = securityServiceSpy.saveChanges;
|
||||
|
||||
securityServiceSpy.securitySettings = {
|
||||
ipAddressAllowList: [{ value: '123.123.123.123' }],
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
MaterialModule,
|
||||
BrowserAnimationsModule,
|
||||
FormsModule,
|
||||
],
|
||||
declarations: [SecurityComponent],
|
||||
}).compileComponents();
|
||||
providers: [BackendService],
|
||||
})
|
||||
.overrideComponent(SecurityComponent, {
|
||||
set: {
|
||||
providers: [
|
||||
{ provide: SecurityService, useValue: securityServiceSpy },
|
||||
],
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(SecurityComponent);
|
||||
component = fixture.componentInstance;
|
||||
@@ -33,4 +71,86 @@ describe('SecurityComponent', () => {
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should call fetch spy', () => {
|
||||
expect(fetchSecurityDetailsSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should render ip allow list', waitForAsync(() => {
|
||||
component.enableEdit();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(
|
||||
Array.from(
|
||||
fixture.nativeElement.querySelectorAll(
|
||||
'.settings-security__ip-allowlist'
|
||||
)
|
||||
)
|
||||
).toHaveSize(1);
|
||||
expect(
|
||||
fixture.nativeElement.querySelector('.settings-security__ip-allowlist')
|
||||
.value
|
||||
).toBe('123.123.123.123');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should remove ip', waitForAsync(() => {
|
||||
expect(
|
||||
Array.from(
|
||||
fixture.nativeElement.querySelectorAll(
|
||||
'.settings-security__ip-allowlist'
|
||||
)
|
||||
)
|
||||
).toHaveSize(1);
|
||||
component.removeIpEntry(0);
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(
|
||||
Array.from(
|
||||
fixture.nativeElement.querySelectorAll(
|
||||
'.settings-security__ip-allowlist'
|
||||
)
|
||||
)
|
||||
).toHaveSize(0);
|
||||
});
|
||||
}));
|
||||
|
||||
it('should toggle inEdit', () => {
|
||||
expect(component.inEdit).toBeFalse();
|
||||
component.enableEdit();
|
||||
expect(component.inEdit).toBeTrue();
|
||||
});
|
||||
|
||||
it('should create temporary data structure', () => {
|
||||
expect(component.dataSource).toBe(
|
||||
component.securityService.securitySettings
|
||||
);
|
||||
component.enableEdit();
|
||||
expect(component.dataSource).not.toBe(
|
||||
component.securityService.securitySettings
|
||||
);
|
||||
component.cancel();
|
||||
expect(component.dataSource).toBe(
|
||||
component.securityService.securitySettings
|
||||
);
|
||||
});
|
||||
|
||||
it('should call save', waitForAsync(async () => {
|
||||
component.enableEdit();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
const el = fixture.nativeElement.querySelector(
|
||||
'.settings-security__clientCertificate'
|
||||
);
|
||||
el.value = 'test';
|
||||
el.dispatchEvent(new Event('input'));
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
fixture.nativeElement
|
||||
.querySelector('.settings-security__actions-save')
|
||||
.click();
|
||||
expect(saveSpy).toHaveBeenCalledOnceWith({
|
||||
ipAddressAllowList: [{ value: '123.123.123.123' }],
|
||||
clientCertificate: 'test',
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -13,10 +13,79 @@
|
||||
// limitations under the License.
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { SecurityService, SecuritySettings } from './security.service';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
|
||||
@Component({
|
||||
selector: 'app-security',
|
||||
templateUrl: './security.component.html',
|
||||
styleUrls: ['./security.component.less'],
|
||||
providers: [SecurityService],
|
||||
})
|
||||
export default class SecurityComponent {}
|
||||
export default class SecurityComponent {
|
||||
loading: boolean = false;
|
||||
inEdit: boolean = false;
|
||||
dataSource: SecuritySettings = {};
|
||||
|
||||
constructor(
|
||||
public securityService: SecurityService,
|
||||
private _snackBar: MatSnackBar
|
||||
) {
|
||||
this.loading = true;
|
||||
this.securityService.fetchSecurityDetails().subscribe({
|
||||
complete: () => {
|
||||
this.dataSource = this.securityService.securitySettings;
|
||||
this.loading = false;
|
||||
},
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error, undefined, {
|
||||
duration: 1500,
|
||||
});
|
||||
this.loading = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
enableEdit() {
|
||||
this.inEdit = true;
|
||||
this.dataSource = JSON.parse(
|
||||
JSON.stringify(this.securityService.securitySettings)
|
||||
);
|
||||
}
|
||||
|
||||
disableEdit() {
|
||||
this.inEdit = false;
|
||||
this.dataSource = this.securityService.securitySettings;
|
||||
}
|
||||
|
||||
createIpEntry() {
|
||||
this.dataSource.ipAddressAllowList?.push({ value: '' });
|
||||
}
|
||||
|
||||
save() {
|
||||
this.loading = true;
|
||||
this.securityService.saveChanges(this.dataSource).subscribe({
|
||||
complete: () => {
|
||||
this.loading = false;
|
||||
this.dataSource = this.securityService.securitySettings;
|
||||
},
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error, undefined, {
|
||||
duration: 1500,
|
||||
});
|
||||
},
|
||||
});
|
||||
this.disableEdit();
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.dataSource = this.securityService.securitySettings;
|
||||
this.inEdit = false;
|
||||
}
|
||||
|
||||
removeIpEntry(index: number) {
|
||||
this.dataSource.ipAddressAllowList =
|
||||
this.dataSource.ipAddressAllowList?.filter((_, i) => i != index);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// 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 { TestBed } from '@angular/core/testing';
|
||||
|
||||
import {
|
||||
SecurityService,
|
||||
SecuritySettings,
|
||||
SecuritySettingsBackendModel,
|
||||
apiToUiConverter,
|
||||
uiToApiConverter,
|
||||
} from './security.service';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import SecurityComponent from './security.component';
|
||||
import { BackendService } from 'src/app/shared/services/backend.service';
|
||||
|
||||
describe('SecurityService', () => {
|
||||
const uiMockData: SecuritySettings = {
|
||||
clientCertificate: 'clientCertificateTest',
|
||||
failoverClientCertificate: 'failoverClientCertificateTest',
|
||||
ipAddressAllowList: [{ value: '123.123.123.123' }],
|
||||
};
|
||||
const apiMockData: SecuritySettingsBackendModel = {
|
||||
clientCertificate: 'clientCertificateTest',
|
||||
failoverClientCertificate: 'failoverClientCertificateTest',
|
||||
ipAddressAllowList: ['123.123.123.123'],
|
||||
};
|
||||
|
||||
let service: SecurityService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
declarations: [SecurityComponent],
|
||||
providers: [SecurityService, BackendService],
|
||||
});
|
||||
service = TestBed.inject(SecurityService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should convert from api to ui', () => {
|
||||
expect(apiToUiConverter(apiMockData)).toEqual(uiMockData);
|
||||
});
|
||||
|
||||
it('should convert from ui to api', () => {
|
||||
expect(uiToApiConverter(uiMockData)).toEqual(apiMockData);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
// 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 { tap } from 'rxjs';
|
||||
import { RegistrarService } from 'src/app/registrar/registrar.service';
|
||||
import { BackendService } from 'src/app/shared/services/backend.service';
|
||||
|
||||
interface ipAllowListItem {
|
||||
value: string;
|
||||
}
|
||||
export interface SecuritySettings {
|
||||
clientCertificate?: string;
|
||||
failoverClientCertificate?: string;
|
||||
ipAddressAllowList?: Array<ipAllowListItem>;
|
||||
}
|
||||
|
||||
export interface SecuritySettingsBackendModel {
|
||||
clientCertificate?: string;
|
||||
failoverClientCertificate?: string;
|
||||
ipAddressAllowList?: Array<string>;
|
||||
}
|
||||
|
||||
export function apiToUiConverter(
|
||||
securitySettings: SecuritySettingsBackendModel = {}
|
||||
): SecuritySettings {
|
||||
return Object.assign({}, securitySettings, {
|
||||
ipAddressAllowList: (securitySettings.ipAddressAllowList || []).map(
|
||||
(value) => ({ value })
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
export function uiToApiConverter(
|
||||
securitySettings: SecuritySettings
|
||||
): SecuritySettingsBackendModel {
|
||||
return Object.assign({}, securitySettings, {
|
||||
ipAddressAllowList: (securitySettings.ipAddressAllowList || [])
|
||||
.filter((s) => s.value)
|
||||
.map((ipAllowItem: ipAllowListItem) => ipAllowItem.value),
|
||||
});
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SecurityService {
|
||||
securitySettings: SecuritySettings = {};
|
||||
|
||||
constructor(
|
||||
private backend: BackendService,
|
||||
private registrarService: RegistrarService
|
||||
) {}
|
||||
|
||||
fetchSecurityDetails() {
|
||||
return this.backend
|
||||
.getSecuritySettings(this.registrarService.activeRegistrarId)
|
||||
.pipe(
|
||||
tap((securitySettings: SecuritySettingsBackendModel) => {
|
||||
this.securitySettings = apiToUiConverter(securitySettings);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
saveChanges(newSecuritySettings: SecuritySettings) {
|
||||
return this.backend
|
||||
.postSecuritySettings(
|
||||
this.registrarService.activeRegistrarId,
|
||||
uiToApiConverter(newSecuritySettings)
|
||||
)
|
||||
.pipe(
|
||||
tap((_) => {
|
||||
this.securitySettings = newSecuritySettings;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,8 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SettingsComponent } from './settings.component';
|
||||
import { MaterialModule } from '../material.module';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
describe('SettingsComponent', () => {
|
||||
let component: SettingsComponent;
|
||||
@@ -22,6 +24,7 @@ describe('SettingsComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MaterialModule, BrowserAnimationsModule],
|
||||
declarations: [SettingsComponent],
|
||||
}).compileComponents();
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||
import { Observable, catchError, of } from 'rxjs';
|
||||
import { Contact } from '../../settings/contact/contact.service';
|
||||
import { SecuritySettingsBackendModel } from 'src/app/settings/security/security.service';
|
||||
|
||||
@Injectable()
|
||||
export class BackendService {
|
||||
@@ -63,4 +64,28 @@ export class BackendService {
|
||||
.get<string[]>('/console-api/registrars')
|
||||
.pipe(catchError((err) => this.errorCatcher<string[]>(err)));
|
||||
}
|
||||
|
||||
getSecuritySettings(
|
||||
registrarId: string
|
||||
): Observable<SecuritySettingsBackendModel> {
|
||||
return this.http
|
||||
.get<SecuritySettingsBackendModel>(
|
||||
`/console-api/settings/security?registrarId=${registrarId}`
|
||||
)
|
||||
.pipe(
|
||||
catchError((err) =>
|
||||
this.errorCatcher<SecuritySettingsBackendModel>(err)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
postSecuritySettings(
|
||||
registrarId: string,
|
||||
securitySettings: SecuritySettingsBackendModel
|
||||
): Observable<SecuritySettingsBackendModel> {
|
||||
return this.http.post<SecuritySettingsBackendModel>(
|
||||
`/console-api/settings/security?registrarId=${registrarId}`,
|
||||
{ registrar: securitySettings }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,12 +50,6 @@ public final class AsyncTaskEnqueuer {
|
||||
this.cloudTasksUtils = cloudTasksUtils;
|
||||
}
|
||||
|
||||
/** Enqueues a task to asynchronously re-save an entity at some point in the future. */
|
||||
public void enqueueAsyncResave(
|
||||
VKey<? extends EppResource> entityToResave, DateTime now, DateTime whenToResave) {
|
||||
enqueueAsyncResave(entityToResave, now, ImmutableSortedSet.of(whenToResave));
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues a task to asynchronously re-save an entity at some point(s) in the future.
|
||||
*
|
||||
|
||||
@@ -72,22 +72,21 @@ public class FlowRunner {
|
||||
}
|
||||
eppMetricBuilder.setCommandNameFromFlow(flowClass.getSimpleName());
|
||||
if (!isTransactional) {
|
||||
EppOutput eppOutput = EppOutput.create(flowProvider.get().run());
|
||||
if (flowClass.equals(LoginFlow.class)) {
|
||||
// In LoginFlow, registrarId isn't known until after the flow executes, so save it then.
|
||||
eppMetricBuilder.setRegistrarId(sessionMetadata.getRegistrarId());
|
||||
}
|
||||
return eppOutput;
|
||||
return EppOutput.create(flowProvider.get().run());
|
||||
}
|
||||
try {
|
||||
return tm()
|
||||
.transact(
|
||||
return tm().transact(
|
||||
() -> {
|
||||
try {
|
||||
EppOutput output = EppOutput.create(flowProvider.get().run());
|
||||
if (isDryRun) {
|
||||
throw new DryRunException(output);
|
||||
}
|
||||
if (flowClass.equals(LoginFlow.class)) {
|
||||
// In LoginFlow, registrarId isn't known until after the flow executes, so save
|
||||
// it then.
|
||||
eppMetricBuilder.setRegistrarId(sessionMetadata.getRegistrarId());
|
||||
}
|
||||
return output;
|
||||
} catch (EppException e) {
|
||||
throw new EppRuntimeException(e);
|
||||
|
||||
@@ -192,7 +192,7 @@ public final class DomainPricingLogic {
|
||||
new FeesAndCredits.Builder()
|
||||
.setCurrency(tld.getCurrency())
|
||||
.addFeeOrCredit(
|
||||
Fee.create(tld.getStandardRestoreCost().getAmount(), FeeType.RESTORE, false));
|
||||
Fee.create(tld.getRestoreBillingCost().getAmount(), FeeType.RESTORE, false));
|
||||
if (isExpired) {
|
||||
feesAndCredits.addFeeOrCredit(
|
||||
Fee.create(
|
||||
|
||||
@@ -38,6 +38,7 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedSet;
|
||||
import google.registry.batch.AsyncTaskEnqueuer;
|
||||
import google.registry.flows.EppException;
|
||||
import google.registry.flows.ExtensionManager;
|
||||
@@ -286,7 +287,8 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
|
||||
.build();
|
||||
DomainHistory domainHistory = buildDomainHistory(newDomain, tld, now, period);
|
||||
|
||||
asyncTaskEnqueuer.enqueueAsyncResave(newDomain.createVKey(), now, automaticTransferTime);
|
||||
asyncTaskEnqueuer.enqueueAsyncResave(
|
||||
newDomain.createVKey(), now, ImmutableSortedSet.of(automaticTransferTime));
|
||||
tm().putAll(
|
||||
new ImmutableSet.Builder<>()
|
||||
.add(newDomain, domainHistory, requestPollMessage)
|
||||
|
||||
@@ -337,7 +337,7 @@ public final class DomainUpdateFlow implements TransactionalFlow {
|
||||
.setReason(Reason.SERVER_STATUS)
|
||||
.setTargetId(targetId)
|
||||
.setRegistrarId(registrarId)
|
||||
.setCost(Tld.get(existingDomain.getTld()).getServerStatusChangeCost())
|
||||
.setCost(Tld.get(existingDomain.getTld()).getServerStatusChangeBillingCost())
|
||||
.setEventTime(now)
|
||||
.setBillingTime(now)
|
||||
.setDomainHistory(historyEntry)
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package google.registry.flows.session;
|
||||
|
||||
import static com.google.common.collect.Sets.difference;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.util.CollectionUtils.nullToEmpty;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
@@ -27,11 +28,10 @@ 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.UnimplementedOptionException;
|
||||
import google.registry.flows.ExtensionManager;
|
||||
import google.registry.flows.Flow;
|
||||
import google.registry.flows.FlowModule.RegistrarId;
|
||||
import google.registry.flows.SessionMetadata;
|
||||
import google.registry.flows.TransactionalFlow;
|
||||
import google.registry.flows.TransportCredentials;
|
||||
import google.registry.model.eppcommon.ProtocolDefinition;
|
||||
import google.registry.model.eppcommon.ProtocolDefinition.ServiceExtension;
|
||||
@@ -51,6 +51,7 @@ import javax.inject.Inject;
|
||||
* @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}
|
||||
@@ -58,11 +59,10 @@ import javax.inject.Inject;
|
||||
* @error {@link LoginFlow.AlreadyLoggedInException}
|
||||
* @error {@link BadRegistrarIdException}
|
||||
* @error {@link LoginFlow.TooManyFailedLoginsException}
|
||||
* @error {@link LoginFlow.PasswordChangesNotSupportedException}
|
||||
* @error {@link LoginFlow.RegistrarAccountNotActiveException}
|
||||
* @error {@link LoginFlow.UnsupportedLanguageException}
|
||||
*/
|
||||
public class LoginFlow implements Flow {
|
||||
public class LoginFlow implements TransactionalFlow {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
@@ -134,8 +134,13 @@ public class LoginFlow implements Flow {
|
||||
if (!registrar.get().isLive()) {
|
||||
throw new RegistrarAccountNotActiveException();
|
||||
}
|
||||
if (login.getNewPassword() != null) { // We don't support in-band password changes.
|
||||
throw new PasswordChangesNotSupportedException();
|
||||
if (login.getNewPassword().isPresent()) {
|
||||
// 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());
|
||||
}
|
||||
|
||||
// We are in!
|
||||
@@ -179,11 +184,4 @@ public class LoginFlow implements Flow {
|
||||
super("Specified language is not supported");
|
||||
}
|
||||
}
|
||||
|
||||
/** In-band password changes are not supported. */
|
||||
static class PasswordChangesNotSupportedException extends UnimplementedOptionException {
|
||||
public PasswordChangesNotSupportedException() {
|
||||
super("In-band password changes are not supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,8 +291,8 @@ public class EppInput extends ImmutableObject {
|
||||
return password;
|
||||
}
|
||||
|
||||
public String getNewPassword() {
|
||||
return newPassword;
|
||||
public Optional<String> getNewPassword() {
|
||||
return Optional.ofNullable(newPassword);
|
||||
}
|
||||
|
||||
public Options getOptions() {
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ public final class StaticPremiumListPricingEngine implements PremiumPricingEngin
|
||||
tld.getPremiumListName().flatMap(pl -> PremiumListDao.getPremiumPrice(pl, label));
|
||||
return DomainPrices.create(
|
||||
premiumPrice.isPresent(),
|
||||
premiumPrice.orElse(tld.getStandardCreateCost()),
|
||||
premiumPrice.orElse(tld.getCreateBillingCost()),
|
||||
premiumPrice.orElse(tld.getStandardRenewCost(priceTime)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@ import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
import static org.joda.money.CurrencyUnit.USD;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.github.benmanes.caffeine.cache.CacheLoader;
|
||||
import com.github.benmanes.caffeine.cache.LoadingCache;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
@@ -48,6 +52,15 @@ import google.registry.model.domain.fee.BaseFee.FeeType;
|
||||
import google.registry.model.domain.fee.Fee;
|
||||
import google.registry.model.domain.token.AllocationToken;
|
||||
import google.registry.model.domain.token.AllocationToken.TokenType;
|
||||
import google.registry.model.tld.TldYamlUtils.CreateAutoTimestampDeserializer;
|
||||
import google.registry.model.tld.TldYamlUtils.CurrencyDeserializer;
|
||||
import google.registry.model.tld.TldYamlUtils.CurrencySerializer;
|
||||
import google.registry.model.tld.TldYamlUtils.OptionalDurationSerializer;
|
||||
import google.registry.model.tld.TldYamlUtils.OptionalStringSerializer;
|
||||
import google.registry.model.tld.TldYamlUtils.TimedTransitionPropertyMoneyDeserializer;
|
||||
import google.registry.model.tld.TldYamlUtils.TimedTransitionPropertyTldStateDeserializer;
|
||||
import google.registry.model.tld.TldYamlUtils.TokenVKeyListDeserializer;
|
||||
import google.registry.model.tld.TldYamlUtils.TokenVKeyListSerializer;
|
||||
import google.registry.model.tld.label.PremiumList;
|
||||
import google.registry.model.tld.label.ReservedList;
|
||||
import google.registry.persistence.VKey;
|
||||
@@ -281,6 +294,7 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
*
|
||||
* <p>When this field is null, the "dnsDefaultATtl" value from the config file will be used.
|
||||
*/
|
||||
@JsonSerialize(using = OptionalDurationSerializer.class)
|
||||
Duration dnsAPlusAaaaTtl;
|
||||
|
||||
/**
|
||||
@@ -288,6 +302,7 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
*
|
||||
* <p>When this field is null, the "dnsDefaultNsTtl" value from the config file will be used.
|
||||
*/
|
||||
@JsonSerialize(using = OptionalDurationSerializer.class)
|
||||
Duration dnsNsTtl;
|
||||
|
||||
/**
|
||||
@@ -295,6 +310,7 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
*
|
||||
* <p>When this field is null, the "dnsDefaultDsTtl" value from the config file will be used.
|
||||
*/
|
||||
@JsonSerialize(using = OptionalDurationSerializer.class)
|
||||
Duration dnsDsTtl;
|
||||
/**
|
||||
* The unicode-aware representation of the TLD associated with this {@link Tld}.
|
||||
@@ -328,11 +344,13 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
|
||||
/** A property that transitions to different {@link TldState}s at different times. */
|
||||
@Column(nullable = false)
|
||||
@JsonDeserialize(using = TimedTransitionPropertyTldStateDeserializer.class)
|
||||
TimedTransitionProperty<TldState> tldStateTransitions =
|
||||
TimedTransitionProperty.withInitialValue(DEFAULT_TLD_STATE);
|
||||
|
||||
/** An automatically managed creation timestamp. */
|
||||
@Column(nullable = false)
|
||||
@JsonDeserialize(using = CreateAutoTimestampDeserializer.class)
|
||||
CreateAutoTimestamp creationTime = CreateAutoTimestamp.create(null);
|
||||
|
||||
/** The set of reserved list names that are applicable to this tld. */
|
||||
@@ -359,6 +377,7 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
* the database should be queried for the entity with this name that has the largest revision ID.
|
||||
*/
|
||||
@Column(name = "premium_list_name")
|
||||
@JsonSerialize(using = OptionalStringSerializer.class)
|
||||
String premiumListName;
|
||||
|
||||
/** Should RDE upload a nightly escrow deposit for this TLD? */
|
||||
@@ -408,6 +427,8 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
|
||||
/** The currency unit for all costs associated with this TLD. */
|
||||
@Column(nullable = false)
|
||||
@JsonSerialize(using = CurrencySerializer.class)
|
||||
@JsonDeserialize(using = CurrencyDeserializer.class)
|
||||
CurrencyUnit currency = DEFAULT_CURRENCY;
|
||||
|
||||
/** The per-year billing cost for registering a new domain name. */
|
||||
@@ -454,11 +475,13 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
* renewal to ensure transfers have a cost.
|
||||
*/
|
||||
@Column(nullable = false)
|
||||
@JsonDeserialize(using = TimedTransitionPropertyMoneyDeserializer.class)
|
||||
TimedTransitionProperty<Money> renewBillingCostTransitions =
|
||||
TimedTransitionProperty.withInitialValue(DEFAULT_RENEW_BILLING_COST);
|
||||
|
||||
/** A property that tracks the EAP fee schedule (if any) for the TLD. */
|
||||
@Column(nullable = false)
|
||||
@JsonDeserialize(using = TimedTransitionPropertyMoneyDeserializer.class)
|
||||
TimedTransitionProperty<Money> eapFeeSchedule =
|
||||
TimedTransitionProperty.withInitialValue(DEFAULT_EAP_BILLING_COST);
|
||||
|
||||
@@ -475,6 +498,11 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
/** An allowlist of hosts allowed to be used on domains on this TLD (ignored if empty). */
|
||||
@Nullable Set<String> allowedFullyQualifiedHostNames;
|
||||
|
||||
/**
|
||||
* Indicates when the TLD is being modified using locally modified files to override the source
|
||||
* control procedures. This field is ignored in Tld YAML files.
|
||||
*/
|
||||
@JsonIgnore
|
||||
@Column(nullable = false)
|
||||
boolean breakglassMode = false;
|
||||
|
||||
@@ -488,6 +516,8 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
* (ex: add a token to the list or remove a token from the list) should not be allowed without
|
||||
* resetting the entire list contents.
|
||||
*/
|
||||
@JsonSerialize(using = TokenVKeyListSerializer.class)
|
||||
@JsonDeserialize(using = TokenVKeyListDeserializer.class)
|
||||
List<VKey<AllocationToken>> defaultPromoTokens;
|
||||
|
||||
/** A set of allowed {@link IdnTableEnum}s for this TLD, or empty if we should use the default. */
|
||||
@@ -502,6 +532,7 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
}
|
||||
|
||||
/** Retrieve the actual domain name representing the TLD for which this registry operates. */
|
||||
@JsonIgnore
|
||||
public InternetDomainName getTld() {
|
||||
return InternetDomainName.from(tldStr);
|
||||
}
|
||||
@@ -511,6 +542,11 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
return tldType;
|
||||
}
|
||||
|
||||
/** Retrieve whether invoicing is enabled. */
|
||||
public boolean isInvoicingEnabled() {
|
||||
return invoicingEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the TLD state at the given time. Defaults to {@link TldState#PREDELEGATION}.
|
||||
*
|
||||
@@ -588,7 +624,7 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
* domain create.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public Money getStandardCreateCost() {
|
||||
public Money getCreateBillingCost() {
|
||||
return createBillingCost;
|
||||
}
|
||||
|
||||
@@ -596,7 +632,7 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
* Returns the add-on cost of a domain restore (the flat tld-wide fee charged in addition to one
|
||||
* year of renewal for that name).
|
||||
*/
|
||||
public Money getStandardRestoreCost() {
|
||||
public Money getRestoreBillingCost() {
|
||||
return restoreBillingCost;
|
||||
}
|
||||
|
||||
@@ -610,7 +646,7 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
}
|
||||
|
||||
/** Returns the cost of a server status change (i.e. lock). */
|
||||
public Money getServerStatusChangeCost() {
|
||||
public Money getServerStatusChangeBillingCost() {
|
||||
return serverStatusChangeBillingCost;
|
||||
}
|
||||
|
||||
@@ -648,6 +684,7 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@JsonProperty("eapFeeSchedule")
|
||||
public ImmutableSortedMap<DateTime, Money> getEapFeeScheduleAsMap() {
|
||||
return eapFeeSchedule.toValueMap();
|
||||
}
|
||||
@@ -660,7 +697,7 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
return claimsPeriodEnd;
|
||||
}
|
||||
|
||||
public String getPremiumPricingEngineClassName() {
|
||||
public String getPricingEngineClassName() {
|
||||
return pricingEngineClassName;
|
||||
}
|
||||
|
||||
@@ -688,6 +725,11 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
return Optional.ofNullable(dnsDsTtl);
|
||||
}
|
||||
|
||||
/** Retrieve the TLD unicode representation. */
|
||||
public String getTldUnicode() {
|
||||
return tldUnicode;
|
||||
}
|
||||
|
||||
public ImmutableSet<String> getAllowedRegistrantContactIds() {
|
||||
return nullToEmptyImmutableCopy(allowedRegistrantContactIds);
|
||||
}
|
||||
@@ -1037,13 +1079,13 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
// All costs must be in the expected currency.
|
||||
checkArgumentNotNull(instance.getCurrency(), "Currency must be set");
|
||||
checkArgument(
|
||||
instance.getStandardCreateCost().getCurrencyUnit().equals(instance.currency),
|
||||
instance.getCreateBillingCost().getCurrencyUnit().equals(instance.currency),
|
||||
"Create cost must be in the tld's currency");
|
||||
checkArgument(
|
||||
instance.getStandardRestoreCost().getCurrencyUnit().equals(instance.currency),
|
||||
instance.getRestoreBillingCost().getCurrencyUnit().equals(instance.currency),
|
||||
"Restore cost must be in the TLD's currency");
|
||||
checkArgument(
|
||||
instance.getServerStatusChangeCost().getCurrencyUnit().equals(instance.currency),
|
||||
instance.getServerStatusChangeBillingCost().getCurrencyUnit().equals(instance.currency),
|
||||
"Server status change cost must be in the TLD's currency");
|
||||
checkArgument(
|
||||
instance.getRegistryLockOrUnlockBillingCost().getCurrencyUnit().equals(instance.currency),
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
// 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.tld;
|
||||
|
||||
import static com.google.common.collect.ImmutableSortedMap.toImmutableSortedMap;
|
||||
import static com.google.common.collect.Ordering.natural;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature;
|
||||
import google.registry.model.CreateAutoTimestamp;
|
||||
import google.registry.model.common.TimedTransitionProperty;
|
||||
import google.registry.model.domain.token.AllocationToken;
|
||||
import google.registry.model.tld.Tld.TldState;
|
||||
import google.registry.persistence.VKey;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.SortedMap;
|
||||
import org.joda.money.CurrencyUnit;
|
||||
import org.joda.money.Money;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Duration;
|
||||
|
||||
/** A collection of static utility classes and functions for TLD YAML conversions. */
|
||||
public class TldYamlUtils {
|
||||
|
||||
/**
|
||||
* Returns an {@link ObjectMapper} object that can be used to convert a {@link Tld} object to and
|
||||
* from YAML.
|
||||
*/
|
||||
public static ObjectMapper getObjectMapper() {
|
||||
SimpleModule module = new SimpleModule();
|
||||
module.addSerializer(Money.class, new MoneySerializer());
|
||||
module.addDeserializer(Money.class, new MoneyDeserializer());
|
||||
ObjectMapper mapper =
|
||||
new ObjectMapper(new YAMLFactory().disable(Feature.WRITE_DOC_START_MARKER))
|
||||
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
|
||||
.registerModule(module);
|
||||
mapper.findAndRegisterModules();
|
||||
return mapper;
|
||||
}
|
||||
|
||||
/** A custom JSON serializer for {@link Money}. */
|
||||
public static class MoneySerializer extends StdSerializer<Money> {
|
||||
|
||||
public MoneySerializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public MoneySerializer(Class<Money> t) {
|
||||
super(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(Money value, JsonGenerator gen, SerializerProvider provider)
|
||||
throws IOException {
|
||||
gen.writeStartObject();
|
||||
gen.writeStringField("currency", String.valueOf(value.getCurrencyUnit()));
|
||||
gen.writeNumberField("amount", value.getAmount());
|
||||
gen.writeEndObject();
|
||||
}
|
||||
}
|
||||
|
||||
/** A custom JSON deserializer for {@link Money}. */
|
||||
public static class MoneyDeserializer extends StdDeserializer<Money> {
|
||||
public MoneyDeserializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public MoneyDeserializer(Class<Money> t) {
|
||||
super(t);
|
||||
}
|
||||
|
||||
static class MoneyJson {
|
||||
public String currency;
|
||||
public BigDecimal amount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Money deserialize(JsonParser jp, DeserializationContext context) throws IOException {
|
||||
MoneyJson json = jp.readValueAs(MoneyJson.class);
|
||||
CurrencyUnit currencyUnit = CurrencyUnit.of(json.currency);
|
||||
return Money.of(currencyUnit, json.amount);
|
||||
}
|
||||
}
|
||||
|
||||
/** A custom JSON serializer for {@link CurrencyUnit}. */
|
||||
public static class CurrencySerializer extends StdSerializer<CurrencyUnit> {
|
||||
|
||||
public CurrencySerializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public CurrencySerializer(Class<CurrencyUnit> t) {
|
||||
super(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(CurrencyUnit value, JsonGenerator gen, SerializerProvider provider)
|
||||
throws IOException {
|
||||
gen.writeString(value.getCode());
|
||||
}
|
||||
}
|
||||
|
||||
/** A custom JSON deserializer for {@link CurrencyUnit}. */
|
||||
public static class CurrencyDeserializer extends StdDeserializer<CurrencyUnit> {
|
||||
public CurrencyDeserializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public CurrencyDeserializer(Class<CurrencyUnit> t) {
|
||||
super(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CurrencyUnit deserialize(JsonParser jp, DeserializationContext context)
|
||||
throws IOException {
|
||||
String currencyCode = jp.readValueAs(String.class);
|
||||
return CurrencyUnit.of(currencyCode);
|
||||
}
|
||||
}
|
||||
|
||||
/** A custom JSON serializer for an Optional of a {@link Duration} object. */
|
||||
public static class OptionalDurationSerializer extends StdSerializer<Optional<Duration>> {
|
||||
|
||||
public OptionalDurationSerializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public OptionalDurationSerializer(Class<Optional<Duration>> t) {
|
||||
super(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(Optional<Duration> value, JsonGenerator gen, SerializerProvider provider)
|
||||
throws IOException {
|
||||
if (value.isPresent()) {
|
||||
gen.writeNumber(value.get().getMillis());
|
||||
} else {
|
||||
gen.writeNull();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** A custom JSON serializer for an Optional String. */
|
||||
public static class OptionalStringSerializer extends StdSerializer<Optional<String>> {
|
||||
|
||||
public OptionalStringSerializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public OptionalStringSerializer(Class<Optional<String>> t) {
|
||||
super(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(Optional<String> value, JsonGenerator gen, SerializerProvider provider)
|
||||
throws IOException {
|
||||
if (value.isPresent()) {
|
||||
gen.writeString(value.get());
|
||||
} else {
|
||||
gen.writeNull();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** A custom JSON serializer for a list of {@link AllocationToken} VKeys. */
|
||||
public static class TokenVKeyListSerializer extends StdSerializer<List<VKey<AllocationToken>>> {
|
||||
|
||||
public TokenVKeyListSerializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public TokenVKeyListSerializer(Class<List<VKey<AllocationToken>>> t) {
|
||||
super(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(
|
||||
List<VKey<AllocationToken>> list, JsonGenerator gen, SerializerProvider provider)
|
||||
throws IOException {
|
||||
gen.writeStartArray();
|
||||
for (VKey<AllocationToken> vkey : list) {
|
||||
gen.writeString(vkey.getKey().toString());
|
||||
}
|
||||
gen.writeEndArray();
|
||||
}
|
||||
}
|
||||
|
||||
/** A custom JSON deserializer for a list of {@link AllocationToken} VKeys. */
|
||||
public static class TokenVKeyListDeserializer
|
||||
extends StdDeserializer<List<VKey<AllocationToken>>> {
|
||||
|
||||
public TokenVKeyListDeserializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public TokenVKeyListDeserializer(Class<VKey<AllocationToken>> t) {
|
||||
super(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VKey<AllocationToken>> deserialize(JsonParser jp, DeserializationContext context)
|
||||
throws IOException {
|
||||
List<VKey<AllocationToken>> tokens = new ArrayList<>();
|
||||
String[] keyStrings = jp.readValueAs(String[].class);
|
||||
for (String token : keyStrings) {
|
||||
tokens.add(VKey.create(AllocationToken.class, token));
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
}
|
||||
|
||||
/** A custom JSON deserializer for a {@link TimedTransitionProperty} of {@link TldState}. */
|
||||
public static class TimedTransitionPropertyTldStateDeserializer
|
||||
extends StdDeserializer<TimedTransitionProperty<TldState>> {
|
||||
|
||||
public TimedTransitionPropertyTldStateDeserializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public TimedTransitionPropertyTldStateDeserializer(Class<TimedTransitionProperty<TldState>> t) {
|
||||
super(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimedTransitionProperty<TldState> deserialize(
|
||||
JsonParser jp, DeserializationContext context) throws IOException {
|
||||
SortedMap<String, String> valueMap = jp.readValueAs(SortedMap.class);
|
||||
return TimedTransitionProperty.fromValueMap(
|
||||
valueMap.keySet().stream()
|
||||
.collect(
|
||||
toImmutableSortedMap(
|
||||
natural(), DateTime::parse, key -> TldState.valueOf(valueMap.get(key)))));
|
||||
}
|
||||
}
|
||||
|
||||
/** A custom JSON deserializer for a {@link TimedTransitionProperty} of {@link Money}. */
|
||||
public static class TimedTransitionPropertyMoneyDeserializer
|
||||
extends StdDeserializer<TimedTransitionProperty<Money>> {
|
||||
|
||||
public TimedTransitionPropertyMoneyDeserializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public TimedTransitionPropertyMoneyDeserializer(Class<TimedTransitionProperty<Money>> t) {
|
||||
super(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimedTransitionProperty<Money> deserialize(JsonParser jp, DeserializationContext context)
|
||||
throws IOException {
|
||||
SortedMap<String, LinkedHashMap> valueMap = jp.readValueAs(SortedMap.class);
|
||||
return TimedTransitionProperty.fromValueMap(
|
||||
valueMap.keySet().stream()
|
||||
.collect(
|
||||
toImmutableSortedMap(
|
||||
natural(),
|
||||
DateTime::parse,
|
||||
key ->
|
||||
Money.of(
|
||||
CurrencyUnit.of(valueMap.get(key).get("currency").toString()),
|
||||
(double) valueMap.get(key).get("amount")))));
|
||||
}
|
||||
}
|
||||
|
||||
/** A custom JSON deserializer for a {@link CreateAutoTimestamp}. */
|
||||
public static class CreateAutoTimestampDeserializer extends StdDeserializer<CreateAutoTimestamp> {
|
||||
|
||||
public CreateAutoTimestampDeserializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public CreateAutoTimestampDeserializer(Class<CreateAutoTimestamp> t) {
|
||||
super(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CreateAutoTimestamp deserialize(JsonParser jp, DeserializationContext context)
|
||||
throws IOException {
|
||||
DateTime creationTime = jp.readValueAs(DateTime.class);
|
||||
return CreateAutoTimestamp.create(creationTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ public final class PricingEngineProxy {
|
||||
*/
|
||||
public static DomainPrices getPricesForDomainName(String domainName, DateTime priceTime) {
|
||||
String tld = getTldFromDomainName(domainName);
|
||||
String clazz = Tld.get(tld).getPremiumPricingEngineClassName();
|
||||
String clazz = Tld.get(tld).getPricingEngineClassName();
|
||||
PremiumPricingEngine engine = premiumPricingEngines.get(clazz);
|
||||
checkState(engine != null, "Could not load pricing engine %s for TLD %s", clazz, tld);
|
||||
return engine.getDomainPrices(domainName, priceTime);
|
||||
|
||||
+2
-1
@@ -58,7 +58,8 @@ SELECT
|
||||
SUM(IF(metricName = 'srs-cont-transfer-query', count, 0)) AS srs_cont_transfer_query,
|
||||
SUM(IF(metricName = 'srs-cont-transfer-reject', count, 0)) AS srs_cont_transfer_reject,
|
||||
SUM(IF(metricName = 'srs-cont-transfer-request', count, 0)) AS srs_cont_transfer_request,
|
||||
SUM(IF(metricName = 'srs-cont-update', count, 0)) AS srs_cont_update
|
||||
SUM(IF(metricName = 'srs-cont-update', count, 0)) AS srs_cont_update,
|
||||
SUM(IF(metricName = 'rdap-queries', count, 0)) AS rdap_queries
|
||||
-- Cross join a list of all TLDs against TLD-specific metrics and then
|
||||
-- filter so that only metrics with that TLD or a NULL TLD are counted
|
||||
-- towards a given TLD.
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ FROM (
|
||||
-- should have negligible impact as the edge cage happens very rarely, more specifically
|
||||
-- when a cancellation happens during grace period by a registrar other than the the
|
||||
-- owning one. All the numbers here should be positive to pass ICANN validation.
|
||||
MAX(report_amount, 0) AS amount,
|
||||
GREATEST(report_amount, 0) AS amount,
|
||||
reporting_time AS reportingTime
|
||||
FROM EXTERNAL_QUERY("projects/%PROJECT_ID%/locations/us/connections/%PROJECT_ID%-sql",
|
||||
''' SELECT history_type, history_other_registrar_id, history_registrar_id, domain_repo_id, history_revision_id FROM "DomainHistory";''') AS dh
|
||||
|
||||
@@ -23,6 +23,7 @@ SELECT
|
||||
CASE
|
||||
WHEN requestPath = '/_dr/whois' THEN 'whois-43-queries'
|
||||
WHEN SUBSTR(requestPath, 0, 7) = '/whois/' THEN 'web-whois-queries'
|
||||
WHEN SUBSTR(requestPath, 0, 6) = '/rdap/' THEN 'rdap-queries'
|
||||
END AS metricName,
|
||||
COUNT(requestPath) AS count
|
||||
FROM
|
||||
|
||||
@@ -69,7 +69,7 @@ public class AsyncTaskEnqueuerTest {
|
||||
void test_enqueueAsyncResave_success() {
|
||||
Contact contact = persistActiveContact("jd23456");
|
||||
asyncTaskEnqueuer.enqueueAsyncResave(
|
||||
contact.createVKey(), clock.nowUtc(), clock.nowUtc().plusDays(5));
|
||||
contact.createVKey(), clock.nowUtc(), ImmutableSortedSet.of(clock.nowUtc().plusDays(5)));
|
||||
cloudTasksHelper.assertTasksEnqueued(
|
||||
QUEUE_ASYNC_ACTIONS,
|
||||
new CloudTasksHelper.TaskMatcher()
|
||||
@@ -108,7 +108,7 @@ public class AsyncTaskEnqueuerTest {
|
||||
void test_enqueueAsyncResave_ignoresTasksTooFarIntoFuture() {
|
||||
Contact contact = persistActiveContact("jd23456");
|
||||
asyncTaskEnqueuer.enqueueAsyncResave(
|
||||
contact.createVKey(), clock.nowUtc(), clock.nowUtc().plusDays(31));
|
||||
contact.createVKey(), clock.nowUtc(), ImmutableSortedSet.of(clock.nowUtc().plusDays(31)));
|
||||
cloudTasksHelper.assertNoTasksEnqueued(QUEUE_ASYNC_ACTIONS);
|
||||
assertLogMessage(logHandler, Level.INFO, "Ignoring async re-save");
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ public class DatabaseSnapshotTest {
|
||||
Tld updated =
|
||||
registry
|
||||
.asBuilder()
|
||||
.setCreateBillingCost(registry.getStandardCreateCost().plus(1))
|
||||
.setCreateBillingCost(registry.getCreateBillingCost().plus(1))
|
||||
.build();
|
||||
tm().transact(() -> tm().put(updated));
|
||||
|
||||
@@ -152,7 +152,7 @@ public class DatabaseSnapshotTest {
|
||||
Tld updated =
|
||||
registry
|
||||
.asBuilder()
|
||||
.setCreateBillingCost(registry.getStandardCreateCost().plus(1))
|
||||
.setCreateBillingCost(registry.getCreateBillingCost().plus(1))
|
||||
.build();
|
||||
tm().transact(() -> tm().put(updated));
|
||||
|
||||
|
||||
@@ -21,15 +21,16 @@ import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import google.registry.flows.EppException;
|
||||
import google.registry.flows.EppException.UnimplementedExtensionException;
|
||||
import google.registry.flows.EppException.UnimplementedObjectServiceException;
|
||||
import google.registry.flows.EppException.UnimplementedProtocolVersionException;
|
||||
import google.registry.flows.FlowTestCase;
|
||||
import google.registry.flows.FlowUtils.GenericXmlSyntaxErrorException;
|
||||
import google.registry.flows.TransportCredentials.BadRegistrarPasswordException;
|
||||
import google.registry.flows.session.LoginFlow.AlreadyLoggedInException;
|
||||
import google.registry.flows.session.LoginFlow.BadRegistrarIdException;
|
||||
import google.registry.flows.session.LoginFlow.PasswordChangesNotSupportedException;
|
||||
import google.registry.flows.session.LoginFlow.RegistrarAccountNotActiveException;
|
||||
import google.registry.flows.session.LoginFlow.TooManyFailedLoginsException;
|
||||
import google.registry.flows.session.LoginFlow.UnsupportedLanguageException;
|
||||
@@ -61,7 +62,7 @@ public abstract class LoginFlowTestCase extends FlowTestCase<LoginFlow> {
|
||||
// Also called in subclasses.
|
||||
void doSuccessfulTest(String xmlFilename) throws Exception {
|
||||
setEppInput(xmlFilename);
|
||||
assertTransactionalFlow(false);
|
||||
assertTransactionalFlow(true);
|
||||
runFlowAssertResponse(loadFile("generic_success_response.xml"));
|
||||
}
|
||||
|
||||
@@ -80,7 +81,7 @@ public abstract class LoginFlowTestCase extends FlowTestCase<LoginFlow> {
|
||||
@Test
|
||||
void testSuccess_setsIsLoginResponse() throws Exception {
|
||||
setEppInput("login_valid.xml");
|
||||
assertTransactionalFlow(false);
|
||||
assertTransactionalFlow(true);
|
||||
EppOutput output = runFlow();
|
||||
assertThat(output.getResponse().isLoginResponse()).isTrue();
|
||||
}
|
||||
@@ -118,8 +119,52 @@ public abstract class LoginFlowTestCase extends FlowTestCase<LoginFlow> {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_newPassword() {
|
||||
doFailingTest("login_invalid_newpw.xml", PasswordChangesNotSupportedException.class);
|
||||
void testSetNewPassword() throws Exception {
|
||||
assertThat(registrar.verifyPassword("foo-BAR2")).isTrue();
|
||||
assertThat(registrar.verifyPassword("ANewPassword")).isFalse();
|
||||
assertThat(registrar.verifyPassword("randomstring")).isFalse();
|
||||
|
||||
setEppInput("login_set_new_password.xml", ImmutableMap.of("NEWPW", "ANewPassword"));
|
||||
assertTransactionalFlow(true);
|
||||
runFlowAssertResponse(loadFile("generic_success_response.xml"));
|
||||
|
||||
Registrar newRegistrar = loadRegistrar("NewRegistrar");
|
||||
assertThat(newRegistrar.verifyPassword("foo-BAR2")).isFalse();
|
||||
assertThat(newRegistrar.verifyPassword("ANewPassword")).isTrue();
|
||||
assertThat(registrar.verifyPassword("randomstring")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_invalidNewPassword_tooShort() throws Exception {
|
||||
setEppInput("login_set_new_password.xml", ImmutableMap.of("NEWPW", "5Char"));
|
||||
EppException thrown = assertThrows(GenericXmlSyntaxErrorException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.contains(
|
||||
"length = '5' is not facet-valid with respect to minLength '6' for type 'pwType'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_invalidNewPassword_tooLong() throws Exception {
|
||||
setEppInput(
|
||||
"login_set_new_password.xml", ImmutableMap.of("NEWPW", "ThisIsMoreThan16Characters"));
|
||||
EppException thrown = assertThrows(GenericXmlSyntaxErrorException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.contains(
|
||||
"length = '26' is not facet-valid with respect to maxLength '16' for type 'pwType'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_invalidNewPassword_containsInvalidCharacter() throws Exception {
|
||||
setEppInput("login_set_new_password.xml", ImmutableMap.of("NEWPW", "TheChar&IsNotValid"));
|
||||
EppException thrown = assertThrows(GenericXmlSyntaxErrorException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
// Just generically assert on this error message because it's a pretty broad error owing to the
|
||||
// overall XML simply not parsing correctly.
|
||||
assertThat(thrown).hasMessageThat().contains("Syntax error");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -17,18 +17,22 @@ package google.registry.model.tld;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth.assertWithMessage;
|
||||
import static com.google.common.truth.Truth8.assertThat;
|
||||
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
|
||||
import static google.registry.model.domain.token.AllocationToken.TokenType.DEFAULT_PROMO;
|
||||
import static google.registry.model.domain.token.AllocationToken.TokenType.SINGLE_USE;
|
||||
import static google.registry.model.tld.Tld.TldState.GENERAL_AVAILABILITY;
|
||||
import static google.registry.model.tld.Tld.TldState.PREDELEGATION;
|
||||
import static google.registry.model.tld.Tld.TldState.QUIET_PERIOD;
|
||||
import static google.registry.model.tld.Tld.TldState.START_DATE_SUNRISE;
|
||||
import static google.registry.model.tld.TldYamlUtils.getObjectMapper;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.testing.DatabaseHelper.createTld;
|
||||
import static google.registry.testing.DatabaseHelper.newTld;
|
||||
import static google.registry.testing.DatabaseHelper.persistPremiumList;
|
||||
import static google.registry.testing.DatabaseHelper.persistReservedList;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static google.registry.testing.TestDataHelper.filePath;
|
||||
import static google.registry.testing.TestDataHelper.loadFile;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
import static java.math.RoundingMode.UNNECESSARY;
|
||||
@@ -36,6 +40,7 @@ import static org.joda.money.CurrencyUnit.EUR;
|
||||
import static org.joda.money.CurrencyUnit.USD;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
@@ -48,11 +53,14 @@ import google.registry.model.tld.label.PremiumList;
|
||||
import google.registry.model.tld.label.PremiumListDao;
|
||||
import google.registry.model.tld.label.ReservedList;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.tldconfig.idn.IdnTableEnum;
|
||||
import google.registry.util.SerializeUtils;
|
||||
import java.io.File;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Optional;
|
||||
import org.joda.money.Money;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Duration;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -94,6 +102,106 @@ public final class TldTest extends EntityTestCase {
|
||||
assertThat(SerializeUtils.serializeDeserialize(persisted)).isEqualTo(persisted);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTldToYaml() throws Exception {
|
||||
fakeClock.setTo(START_OF_TIME);
|
||||
AllocationToken defaultToken =
|
||||
persistResource(
|
||||
new AllocationToken.Builder()
|
||||
.setToken("bbbbb")
|
||||
.setTokenType(DEFAULT_PROMO)
|
||||
.setAllowedTlds(ImmutableSet.of("tld"))
|
||||
.build());
|
||||
Tld existingTld =
|
||||
createTld("tld")
|
||||
.asBuilder()
|
||||
.setDnsAPlusAaaaTtl(Duration.standardHours(1))
|
||||
.setDnsWriters(ImmutableSet.of("baz", "bang"))
|
||||
.setEapFeeSchedule(
|
||||
ImmutableSortedMap.of(
|
||||
START_OF_TIME,
|
||||
Money.of(USD, 0),
|
||||
DateTime.parse("2000-06-01T00:00:00Z"),
|
||||
Money.of(USD, 100),
|
||||
DateTime.parse("2000-06-02T00:00:00Z"),
|
||||
Money.of(USD, 0)))
|
||||
.setAllowedFullyQualifiedHostNames(ImmutableSet.of("foo"))
|
||||
.setDefaultPromoTokens(ImmutableList.of(defaultToken.createVKey()))
|
||||
.setIdnTables(ImmutableSet.of(IdnTableEnum.JA, IdnTableEnum.EXTENDED_LATIN))
|
||||
.build();
|
||||
|
||||
ObjectMapper mapper = getObjectMapper();
|
||||
String yaml = mapper.writeValueAsString(existingTld);
|
||||
assertThat(yaml).isEqualTo(loadFile(getClass(), "tld.yaml"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testYamlToTld() throws Exception {
|
||||
fakeClock.setTo(START_OF_TIME);
|
||||
AllocationToken defaultToken =
|
||||
persistResource(
|
||||
new AllocationToken.Builder()
|
||||
.setToken("bbbbb")
|
||||
.setTokenType(DEFAULT_PROMO)
|
||||
.setAllowedTlds(ImmutableSet.of("tld"))
|
||||
.build());
|
||||
Tld existingTld =
|
||||
createTld("tld")
|
||||
.asBuilder()
|
||||
.setDnsAPlusAaaaTtl(Duration.standardHours(1))
|
||||
.setDnsWriters(ImmutableSet.of("baz", "bang"))
|
||||
.setEapFeeSchedule(
|
||||
ImmutableSortedMap.of(
|
||||
START_OF_TIME,
|
||||
Money.of(USD, 0),
|
||||
DateTime.parse("2000-06-01T00:00:00Z"),
|
||||
Money.of(USD, 100),
|
||||
DateTime.parse("2000-06-02T00:00:00Z"),
|
||||
Money.of(USD, 0)))
|
||||
.setAllowedFullyQualifiedHostNames(ImmutableSet.of("foo"))
|
||||
.setDefaultPromoTokens(ImmutableList.of(defaultToken.createVKey()))
|
||||
.setIdnTables(ImmutableSet.of(IdnTableEnum.JA, IdnTableEnum.EXTENDED_LATIN))
|
||||
.build();
|
||||
|
||||
ObjectMapper mapper = getObjectMapper();
|
||||
Tld constructedTld = mapper.readValue(new File(filePath(getClass(), "tld.yaml")), Tld.class);
|
||||
compareTlds(existingTld, constructedTld);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_tldYamlRoundtrip() throws Exception {
|
||||
Tld testTld = createTld("test");
|
||||
ObjectMapper mapper = getObjectMapper();
|
||||
String yaml = mapper.writeValueAsString(testTld);
|
||||
Tld constructedTld = mapper.readValue(yaml, Tld.class);
|
||||
compareTlds(testTld, constructedTld);
|
||||
}
|
||||
|
||||
// On YAML serialization/deserialization some null values may be changed to empty collections
|
||||
void compareTlds(Tld existingTld, Tld constructedTld) {
|
||||
assertAboutImmutableObjects()
|
||||
.that(constructedTld)
|
||||
.isEqualExceptFields(
|
||||
existingTld,
|
||||
"dnsWriters",
|
||||
"idnTables",
|
||||
"reservedListNames",
|
||||
"allowedRegistrantContactIds",
|
||||
"allowedFullyQualifiedHostNames",
|
||||
"defaultPromoTokens");
|
||||
assertThat(constructedTld.getDnsWriters())
|
||||
.containsExactlyElementsIn(existingTld.getDnsWriters());
|
||||
assertThat(constructedTld.getIdnTables()).containsExactlyElementsIn(existingTld.getIdnTables());
|
||||
assertThat(constructedTld.getReservedListNames())
|
||||
.containsExactlyElementsIn(existingTld.getReservedListNames());
|
||||
assertThat(constructedTld.getAllowedRegistrantContactIds())
|
||||
.containsExactlyElementsIn(existingTld.getAllowedRegistrantContactIds());
|
||||
assertThat(constructedTld.getAllowedFullyQualifiedHostNames())
|
||||
.containsExactlyElementsIn(existingTld.getAllowedFullyQualifiedHostNames());
|
||||
assertThat(constructedTld.getDefaultPromoTokens())
|
||||
.containsExactlyElementsIn(existingTld.getDefaultPromoTokens());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_registryNotFound() {
|
||||
createTld("foo");
|
||||
@@ -111,17 +219,17 @@ public final class TldTest extends EntityTestCase {
|
||||
@Test
|
||||
void testSettingCreateBillingCost() {
|
||||
Tld registry = Tld.get("tld").asBuilder().setCreateBillingCost(Money.of(USD, 42)).build();
|
||||
assertThat(registry.getStandardCreateCost()).isEqualTo(Money.of(USD, 42));
|
||||
assertThat(registry.getCreateBillingCost()).isEqualTo(Money.of(USD, 42));
|
||||
// The default value of 17 is set in createTld().
|
||||
assertThat(registry.getStandardRestoreCost()).isEqualTo(Money.of(USD, 17));
|
||||
assertThat(registry.getRestoreBillingCost()).isEqualTo(Money.of(USD, 17));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSettingRestoreBillingCost() {
|
||||
Tld registry = Tld.get("tld").asBuilder().setRestoreBillingCost(Money.of(USD, 42)).build();
|
||||
// The default value of 13 is set in createTld().
|
||||
assertThat(registry.getStandardCreateCost()).isEqualTo(Money.of(USD, 13));
|
||||
assertThat(registry.getStandardRestoreCost()).isEqualTo(Money.of(USD, 42));
|
||||
assertThat(registry.getCreateBillingCost()).isEqualTo(Money.of(USD, 13));
|
||||
assertThat(registry.getRestoreBillingCost()).isEqualTo(Money.of(USD, 42));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -251,7 +359,7 @@ public final class TldTest extends EntityTestCase {
|
||||
void testSettingServerStatusChangeBillingCost() {
|
||||
Tld registry =
|
||||
Tld.get("tld").asBuilder().setServerStatusChangeBillingCost(Money.of(USD, 42)).build();
|
||||
assertThat(registry.getServerStatusChangeCost()).isEqualTo(Money.of(USD, 42));
|
||||
assertThat(registry.getServerStatusChangeBillingCost()).isEqualTo(Money.of(USD, 42));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -225,7 +225,7 @@ class CreateTldCommandTest extends CommandTestCase<CreateTldCommand> {
|
||||
"--roid_suffix=Q9JYB4C",
|
||||
"--dns_writers=VoidDnsWriter",
|
||||
"xn--q9jyb4c");
|
||||
assertThat(Tld.get("xn--q9jyb4c").getStandardCreateCost()).isEqualTo(Money.of(USD, 42.42));
|
||||
assertThat(Tld.get("xn--q9jyb4c").getCreateBillingCost()).isEqualTo(Money.of(USD, 42.42));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -235,7 +235,7 @@ class CreateTldCommandTest extends CommandTestCase<CreateTldCommand> {
|
||||
"--roid_suffix=Q9JYB4C",
|
||||
"--dns_writers=VoidDnsWriter",
|
||||
"xn--q9jyb4c");
|
||||
assertThat(Tld.get("xn--q9jyb4c").getStandardRestoreCost()).isEqualTo(Money.of(USD, 42.42));
|
||||
assertThat(Tld.get("xn--q9jyb4c").getRestoreBillingCost()).isEqualTo(Money.of(USD, 42.42));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -245,7 +245,8 @@ class CreateTldCommandTest extends CommandTestCase<CreateTldCommand> {
|
||||
"--roid_suffix=Q9JYB4C",
|
||||
"--dns_writers=VoidDnsWriter",
|
||||
"xn--q9jyb4c");
|
||||
assertThat(Tld.get("xn--q9jyb4c").getServerStatusChangeCost()).isEqualTo(Money.of(USD, 42.42));
|
||||
assertThat(Tld.get("xn--q9jyb4c").getServerStatusChangeBillingCost())
|
||||
.isEqualTo(Money.of(USD, 42.42));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -271,8 +272,8 @@ class CreateTldCommandTest extends CommandTestCase<CreateTldCommand> {
|
||||
"--dns_writers=VoidDnsWriter",
|
||||
"xn--q9jyb4c");
|
||||
Tld registry = Tld.get("xn--q9jyb4c");
|
||||
assertThat(registry.getStandardCreateCost()).isEqualTo(Money.ofMajor(JPY, 12345));
|
||||
assertThat(registry.getStandardRestoreCost()).isEqualTo(Money.ofMajor(JPY, 67890));
|
||||
assertThat(registry.getCreateBillingCost()).isEqualTo(Money.ofMajor(JPY, 12345));
|
||||
assertThat(registry.getRestoreBillingCost()).isEqualTo(Money.ofMajor(JPY, 67890));
|
||||
assertThat(registry.getStandardRenewCost(START_OF_TIME)).isEqualTo(Money.ofMajor(JPY, 101112));
|
||||
}
|
||||
|
||||
|
||||
@@ -299,13 +299,13 @@ class UpdateTldCommandTest extends CommandTestCase<UpdateTldCommand> {
|
||||
@Test
|
||||
void testSuccess_createBillingCostFlag() throws Exception {
|
||||
runCommandForced("--create_billing_cost=\"USD 42.42\"", "xn--q9jyb4c");
|
||||
assertThat(Tld.get("xn--q9jyb4c").getStandardCreateCost()).isEqualTo(Money.of(USD, 42.42));
|
||||
assertThat(Tld.get("xn--q9jyb4c").getCreateBillingCost()).isEqualTo(Money.of(USD, 42.42));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_restoreBillingCostFlag() throws Exception {
|
||||
runCommandForced("--restore_billing_cost=\"USD 42.42\"", "xn--q9jyb4c");
|
||||
assertThat(Tld.get("xn--q9jyb4c").getStandardRestoreCost()).isEqualTo(Money.of(USD, 42.42));
|
||||
assertThat(Tld.get("xn--q9jyb4c").getRestoreBillingCost()).isEqualTo(Money.of(USD, 42.42));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -330,10 +330,10 @@ class UpdateTldCommandTest extends CommandTestCase<UpdateTldCommand> {
|
||||
"--registry_lock_or_unlock_cost=\"JPY 9001\"",
|
||||
"xn--q9jyb4c");
|
||||
Tld registry = Tld.get("xn--q9jyb4c");
|
||||
assertThat(registry.getStandardCreateCost()).isEqualTo(Money.ofMajor(JPY, 12345));
|
||||
assertThat(registry.getStandardRestoreCost()).isEqualTo(Money.ofMajor(JPY, 67890));
|
||||
assertThat(registry.getCreateBillingCost()).isEqualTo(Money.ofMajor(JPY, 12345));
|
||||
assertThat(registry.getRestoreBillingCost()).isEqualTo(Money.ofMajor(JPY, 67890));
|
||||
assertThat(registry.getStandardRenewCost(START_OF_TIME)).isEqualTo(Money.ofMajor(JPY, 101112));
|
||||
assertThat(registry.getServerStatusChangeCost()).isEqualTo(Money.ofMajor(JPY, 97865));
|
||||
assertThat(registry.getServerStatusChangeBillingCost()).isEqualTo(Money.ofMajor(JPY, 97865));
|
||||
assertThat(registry.getRegistryLockOrUnlockBillingCost()).isEqualTo(Money.ofMajor(JPY, 9001));
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
<login>
|
||||
<clID>NewRegistrar</clID>
|
||||
<pw>foo-BAR2</pw>
|
||||
<newPW>ANewPassword</newPW>
|
||||
<newPW>%NEWPW%</newPW>
|
||||
<options>
|
||||
<version>1.0</version>
|
||||
<lang>en</lang>
|
||||
@@ -0,0 +1,66 @@
|
||||
tldStr: "tld"
|
||||
roidSuffix: "TLD"
|
||||
pricingEngineClassName: "google.registry.model.pricing.StaticPremiumListPricingEngine"
|
||||
dnsWriters:
|
||||
- "baz"
|
||||
- "bang"
|
||||
numDnsPublishLocks: 1
|
||||
dnsAPlusAaaaTtl: 3600000
|
||||
dnsNsTtl: null
|
||||
dnsDsTtl: null
|
||||
tldUnicode: "tld"
|
||||
driveFolderId: null
|
||||
tldType: "REAL"
|
||||
invoicingEnabled: false
|
||||
tldStateTransitions:
|
||||
"1970-01-01T00:00:00.000Z": "GENERAL_AVAILABILITY"
|
||||
creationTime: "1970-01-01T00:00:00.000Z"
|
||||
reservedListNames: []
|
||||
premiumListName: "tld"
|
||||
escrowEnabled: false
|
||||
dnsPaused: false
|
||||
addGracePeriodLength: 432000000
|
||||
anchorTenantAddGracePeriodLength: 2592000000
|
||||
autoRenewGracePeriodLength: 3888000000
|
||||
redemptionGracePeriodLength: 2592000000
|
||||
renewGracePeriodLength: 432000000
|
||||
transferGracePeriodLength: 432000000
|
||||
automaticTransferLength: 432000000
|
||||
pendingDeleteLength: 432000000
|
||||
currency: "USD"
|
||||
createBillingCost:
|
||||
currency: "USD"
|
||||
amount: 13.00
|
||||
restoreBillingCost:
|
||||
currency: "USD"
|
||||
amount: 17.00
|
||||
serverStatusChangeBillingCost:
|
||||
currency: "USD"
|
||||
amount: 19.00
|
||||
registryLockOrUnlockBillingCost:
|
||||
currency: "USD"
|
||||
amount: 0.00
|
||||
renewBillingCostTransitions:
|
||||
"1970-01-01T00:00:00.000Z":
|
||||
currency: "USD"
|
||||
amount: 11.00
|
||||
lordnUsername: null
|
||||
claimsPeriodEnd: "294247-01-10T04:00:54.775Z"
|
||||
allowedRegistrantContactIds: []
|
||||
allowedFullyQualifiedHostNames:
|
||||
- "foo"
|
||||
defaultPromoTokens:
|
||||
- "bbbbb"
|
||||
idnTables:
|
||||
- "JA"
|
||||
- "EXTENDED_LATIN"
|
||||
eapFeeSchedule:
|
||||
"1970-01-01T00:00:00.000Z":
|
||||
currency: "USD"
|
||||
amount: 0.00
|
||||
"2000-06-01T00:00:00.000Z":
|
||||
currency: "USD"
|
||||
amount: 100.00
|
||||
"2000-06-02T00:00:00.000Z":
|
||||
currency: "USD"
|
||||
amount: 0.00
|
||||
+2
-1
@@ -58,7 +58,8 @@ SELECT
|
||||
SUM(IF(metricName = 'srs-cont-transfer-query', count, 0)) AS srs_cont_transfer_query,
|
||||
SUM(IF(metricName = 'srs-cont-transfer-reject', count, 0)) AS srs_cont_transfer_reject,
|
||||
SUM(IF(metricName = 'srs-cont-transfer-request', count, 0)) AS srs_cont_transfer_request,
|
||||
SUM(IF(metricName = 'srs-cont-update', count, 0)) AS srs_cont_update
|
||||
SUM(IF(metricName = 'srs-cont-update', count, 0)) AS srs_cont_update,
|
||||
SUM(IF(metricName = 'rdap-queries', count, 0)) AS rdap_queries
|
||||
-- Cross join a list of all TLDs against TLD-specific metrics and then
|
||||
-- filter so that only metrics with that TLD or a NULL TLD are counted
|
||||
-- towards a given TLD.
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ FROM (
|
||||
-- should have negligible impact as the edge cage happens very rarely, more specifically
|
||||
-- when a cancellation happens during grace period by a registrar other than the the
|
||||
-- owning one. All the numbers here should be positive to pass ICANN validation.
|
||||
MAX(report_amount, 0) AS amount,
|
||||
GREATEST(report_amount, 0) AS amount,
|
||||
reporting_time AS reportingTime
|
||||
FROM EXTERNAL_QUERY("projects/domain-registry-alpha/locations/us/connections/domain-registry-alpha-sql",
|
||||
''' SELECT history_type, history_other_registrar_id, history_registrar_id, domain_repo_id, history_revision_id FROM "DomainHistory";''') AS dh
|
||||
|
||||
+1
@@ -23,6 +23,7 @@ SELECT
|
||||
CASE
|
||||
WHEN requestPath = '/_dr/whois' THEN 'whois-43-queries'
|
||||
WHEN SUBSTR(requestPath, 0, 7) = '/whois/' THEN 'web-whois-queries'
|
||||
WHEN SUBSTR(requestPath, 0, 6) = '/rdap/' THEN 'rdap-queries'
|
||||
END AS metricName,
|
||||
COUNT(requestPath) AS count
|
||||
FROM
|
||||
|
||||
+17
-5
@@ -56,11 +56,23 @@ following steps:
|
||||
table statements can be used as is, whereas alter table statements should be
|
||||
written to change any existing tables.
|
||||
|
||||
Note that each incremental file MUST be limited to changes to a single
|
||||
table (otherwise it may hit deadlock when applying on sandbox/production
|
||||
where it'll be competing against live traffic that may also be locking said
|
||||
tables but in a different order). It's OK to include these separate Flyway
|
||||
scripts in a single PR.
|
||||
If an incremental file changes more than one schema element (table, index,
|
||||
or sequence), it MAY hit deadlocks when applied on sandbox/production where
|
||||
it'll be competing against live traffic that may also be locking said
|
||||
elements but in a different order. The `FlywayDeadlockTest` checks for this
|
||||
risk for every new incremental file to be merged. Simply put, the test
|
||||
treats any of the following as a changed element, and raises an error if a
|
||||
new file has more than one changed elements:
|
||||
|
||||
* A schema element (table, index, or sequence) being altered.
|
||||
* The table on which an index is created without the `concurrently`
|
||||
modifier. Please refer to
|
||||
<a href="./src/test/java/google/registry/sql/flyway/FlywayDeadlockTest.java">
|
||||
the test class's javadoc</a> for more information.
|
||||
|
||||
Any file failing this test should be split up according to the error
|
||||
message. It's OK to include these separate Flyway scripts in a single PR.
|
||||
|
||||
|
||||
This script should be stored in a new file in the
|
||||
`db/src/main/resources/sql/flyway` folder using the naming pattern
|
||||
|
||||
@@ -168,6 +168,7 @@ dependencies {
|
||||
testRuntimeOnly deps['com.google.flogger:flogger-system-backend']
|
||||
testImplementation deps['com.google.guava:guava']
|
||||
testImplementation deps['com.google.truth:truth']
|
||||
testImplementation deps['com.google.truth.extensions:truth-java8-extension']
|
||||
testRuntimeOnly deps['io.github.java-diff-utils:java-diff-utils']
|
||||
testImplementation deps['org.junit.jupiter:junit-jupiter-api']
|
||||
testImplementation deps['org.junit.jupiter:junit-jupiter-engine']
|
||||
|
||||
@@ -53,6 +53,7 @@ com.google.j2objc:j2objc-annotations:1.3=checkstyle,default,deploy_jar,runtimeCl
|
||||
com.google.j2objc:j2objc-annotations:2.8=testCompileClasspath,testRuntimeClasspath
|
||||
com.google.oauth-client:google-oauth-client:1.34.1=default,deploy_jar,runtimeClasspath,testRuntimeClasspath
|
||||
com.google.protobuf:protobuf-java:3.4.0=annotationProcessor,errorprone,testAnnotationProcessor
|
||||
com.google.truth.extensions:truth-java8-extension:1.1.3=testCompileClasspath,testRuntimeClasspath
|
||||
com.google.truth:truth:1.1.3=testCompileClasspath,testRuntimeClasspath
|
||||
com.googlecode.java-diff-utils:diffutils:1.3.0=annotationProcessor,errorprone,testAnnotationProcessor
|
||||
com.puppycrawl.tools:checkstyle:8.37=checkstyle
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
// 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.sql.flyway;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static com.google.common.truth.Truth.assertWithMessage;
|
||||
import static com.google.common.truth.Truth8.assertThat;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.nio.file.Files.readAllLines;
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
import static java.util.regex.Pattern.CASE_INSENSITIVE;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Checks if new Flyway scripts may cause Database deadlock.
|
||||
*
|
||||
* <p>Flyway deploys each DDL script in one transaction. If a script modifies multiple schema
|
||||
* elements (table, index, sequence), it MAY hit deadlocks when applied on sandbox/production where
|
||||
* it'll be competing against live traffic that may also be locking said elements but in a different
|
||||
* order. This test checks every new script to be merged, counts the elements it locks, and raise an
|
||||
* error when there are more than one locked elements.
|
||||
*
|
||||
* <p>For deadlock-prevention purpose, we can ignore elements being created or dropped: our
|
||||
* schema-server compatibility tests ensure that such elements are not accessed by live traffic.
|
||||
* Therefore, we focus on 'alter' statements. However, 'create index' is a special case: if the
|
||||
* 'concurrently' modifier is not present, the indexed table is locked.
|
||||
*/
|
||||
public class FlywayDeadlockTest {
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
// Relative path (from the root of the repository) of the scripts directory.
|
||||
private static final String FLYWAY_SCRIPTS_DIR = "db/src/main/resources/sql/flyway";
|
||||
|
||||
// For splitting a shell command into an array of strings.
|
||||
private static final Splitter SHELL_COMMAND_SPLITTER = Splitter.on(' ').trimResults();
|
||||
|
||||
// For splitting a multi-statement SQL string into individual statements.
|
||||
private static final Splitter SQL_TEXT_SPLITTER =
|
||||
Splitter.on(";").trimResults().omitEmptyStrings();
|
||||
|
||||
// For splitting the multi-line text containing all new script paths.
|
||||
private static final Splitter CHANGED_FILENAMES_SPLITTER =
|
||||
Splitter.on('\n').trimResults().omitEmptyStrings();
|
||||
|
||||
// Command that returns the git repo's root dir when executed anywhere in the repo.
|
||||
private static final String GIT_GET_ROOT_DIR_CMD = "git rev-parse --show-toplevel";
|
||||
|
||||
// Returns the commit hash when the current branch branches of the main branch.
|
||||
private static final String GIT_FORK_POINT_CMD = "git merge-base origin/master HEAD";
|
||||
|
||||
// Command template to get changed Flyways scripts, with fork-point to be filled in. This command
|
||||
// is executed at the root dir of the repo. Any path returned is relative to the root dir.
|
||||
private static final String GET_CHANGED_SCRIPTS_CMD =
|
||||
"git diff %s --name-only " + FLYWAY_SCRIPTS_DIR;
|
||||
|
||||
// Map of DDL patterns and the capture group index of the element name in it.
|
||||
public static final ImmutableMap<Pattern, Integer> DDL_PATTERNS =
|
||||
ImmutableMap.of(
|
||||
Pattern.compile(
|
||||
"^\\s*CREATE\\s+(UNIQUE\\s+)?INDEX\\s+"
|
||||
+ "(IF\\s+NOT\\s+EXISTS\\s+)*(public.)?((\\w+)|(\"\\w+\"))\\s+ON\\s+(ONLY\\s+)?"
|
||||
+ "(public.)?((\\w+)|(\"\\w+\"))[^;]+$",
|
||||
CASE_INSENSITIVE),
|
||||
9,
|
||||
Pattern.compile(
|
||||
"^\\s*ALTER\\s+INDEX\\s+(IF\\s+EXISTS\\s+)?(public.)?((\\w+)|(\"\\w+\"))[^;]+$",
|
||||
CASE_INSENSITIVE),
|
||||
3,
|
||||
Pattern.compile(
|
||||
"^\\s*ALTER\\s+SEQUENCE\\s+(IF\\s+EXISTS\\s+)?(public.)?((\\w+)|(\"\\w+\"))[^;]+$",
|
||||
CASE_INSENSITIVE),
|
||||
3,
|
||||
Pattern.compile(
|
||||
"^\\s*ALTER\\s+TABLE\\s+(IF\\s+EXISTS\\s+|ONLY\\s+)*(public.)?((\\w+)|(\"\\w+\"))[^;]+$",
|
||||
CASE_INSENSITIVE),
|
||||
3);
|
||||
|
||||
@Test
|
||||
public void validateNewFlywayScripts() {
|
||||
ImmutableList<Path> newScriptPaths = findChangedFlywayScripts();
|
||||
ImmutableList<String> scriptAndLockedElements =
|
||||
newScriptPaths.stream()
|
||||
.filter(path -> parseDdlScript(path).size() > 1)
|
||||
.map(path -> String.format("%s: %s", path, parseDdlScript(path)))
|
||||
.collect(toImmutableList());
|
||||
|
||||
assertWithMessage("Scripts changing more than one schema elements:")
|
||||
.that(scriptAndLockedElements)
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDdlLockedElementName_found() {
|
||||
ImmutableList<String> ddls =
|
||||
ImmutableList.of(
|
||||
"alter table element_name ...",
|
||||
"ALTER table if EXISTS ONLY \"element_name\" ...",
|
||||
"alter index element_name \n...",
|
||||
"Alter sequence public.\"element_name\" ...",
|
||||
"create index if not exists \"index_name\" on element_name ...",
|
||||
"create index if not exists \"index_name\" on public.\"element_name\" ...",
|
||||
"create index if not exists index_name on public.element_name ...",
|
||||
"create index if not exists \"index_name\" on public.element_name ...",
|
||||
"create unique index public.index_name on public.\"element_name\" ...");
|
||||
ddls.forEach(
|
||||
ddl -> {
|
||||
assertThat(getDdlLockedElementName(ddl)).hasValue("element_name");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDdlLockedElementName_notFound() {
|
||||
ImmutableList<String> ddls =
|
||||
ImmutableList.of(
|
||||
"create table element_name ...;",
|
||||
"create sequence public.\"element_name\" ...;",
|
||||
"create index concurrently if not exists index_name on public.element_name ...;",
|
||||
"create unique index concurrently public.index_name on public.\"element_name\" ...;",
|
||||
"drop table element_name ...;",
|
||||
"drop sequence element_name ...;",
|
||||
"drop INDEX element_name ...;");
|
||||
ddls.forEach(
|
||||
ddl -> {
|
||||
assertThat(getDdlLockedElementName(ddl)).isEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
static Optional<String> getDdlLockedElementName(String ddl) {
|
||||
for (Map.Entry<Pattern, Integer> patternEntry : DDL_PATTERNS.entrySet()) {
|
||||
Matcher matcher = patternEntry.getKey().matcher(ddl);
|
||||
if (matcher.find()) {
|
||||
String name = matcher.group(patternEntry.getValue());
|
||||
return Optional.of(name.replace("\"", ""));
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
static ImmutableSet<String> parseDdlScript(Path path) {
|
||||
try {
|
||||
return SQL_TEXT_SPLITTER
|
||||
.splitToStream(
|
||||
readAllLines(path, UTF_8).stream()
|
||||
.map(line -> line.replaceAll("--.*", ""))
|
||||
.filter(line -> !line.isBlank())
|
||||
.collect(joining(" ")))
|
||||
.map(FlywayDeadlockTest::getDdlLockedElementName)
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get)
|
||||
.collect(toImmutableSet());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
static ImmutableList<Path> findChangedFlywayScripts() {
|
||||
try {
|
||||
String forkPoint;
|
||||
try {
|
||||
forkPoint = executeShellCommand(GIT_FORK_POINT_CMD);
|
||||
} catch (RuntimeException e) {
|
||||
if (e.getMessage() != null && e.getMessage().contains("not a git repository")) {
|
||||
logger.atInfo().log("Not in git repo: probably the internal-pr or -ci test.");
|
||||
return ImmutableList.of();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
String rootDir = executeShellCommand(GIT_GET_ROOT_DIR_CMD);
|
||||
String changedScriptsCommand = String.format(GET_CHANGED_SCRIPTS_CMD, forkPoint);
|
||||
ImmutableList<Path> changedPaths =
|
||||
CHANGED_FILENAMES_SPLITTER
|
||||
.splitToList(executeShellCommand(changedScriptsCommand, Optional.of(rootDir)))
|
||||
.stream()
|
||||
.map(pathStr -> rootDir + File.separator + pathStr)
|
||||
.map(Path::of)
|
||||
.collect(toImmutableList());
|
||||
if (changedPaths.isEmpty()) {
|
||||
logger.atInfo().log("There are no schema changes.");
|
||||
} else {
|
||||
logger.atInfo().log("Found %s new Flyway scripts", changedPaths.size());
|
||||
}
|
||||
return changedPaths;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
static String executeShellCommand(String command) throws IOException {
|
||||
return executeShellCommand(command, Optional.empty());
|
||||
}
|
||||
|
||||
static String executeShellCommand(String command, Optional<String> workingDir)
|
||||
throws IOException {
|
||||
ProcessBuilder processBuilder =
|
||||
new ProcessBuilder(SHELL_COMMAND_SPLITTER.splitToList(command).toArray(new String[0]));
|
||||
workingDir.map(File::new).ifPresent(processBuilder::directory);
|
||||
Process process = processBuilder.start();
|
||||
String output = new String(process.getInputStream().readAllBytes(), UTF_8);
|
||||
String error = new String(process.getErrorStream().readAllBytes(), UTF_8);
|
||||
try {
|
||||
process.waitFor(1, SECONDS);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
if (process.exitValue() != 0) {
|
||||
throw new RuntimeException(error);
|
||||
}
|
||||
return output.trim();
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -992,12 +992,12 @@ An EPP flow for login.
|
||||
|
||||
### Errors
|
||||
|
||||
* 2001
|
||||
* Generic XML syntax error that can be thrown by any flow.
|
||||
* 2002
|
||||
* Registrar is already logged in.
|
||||
* 2100
|
||||
* Specified protocol version is not implemented.
|
||||
* 2102
|
||||
* In-band password changes are not supported.
|
||||
* 2103
|
||||
* Specified extension is not implemented.
|
||||
* 2200
|
||||
|
||||
@@ -70,6 +70,27 @@ configurations {
|
||||
}
|
||||
}
|
||||
|
||||
// Custom resolution strategy to get around tricky dependency issues.
|
||||
configurations.all {
|
||||
// jackson-core is a transitive dependency that we cannot explicity specify
|
||||
// versions for, without triggering linter alerts. Even though the lockfiles
|
||||
// lock it to a lower version, Gradle will first try to resolve the
|
||||
// dependency tree based on what is available in the Maven repo before
|
||||
// applying any dependency locks. The newer version (v2.15) of jackson-core
|
||||
// results in a build failure for pre-7.6 Gradle versions during that stage.
|
||||
// This custom resolution strategy will modify the dependency tree as it was
|
||||
// being built and prevent Gradle from even trying v2.15.
|
||||
// See: https://github.com/FasterXML/jackson-core/issues/955
|
||||
// TODO: Remove the custom stragegy after we upgrade to Gradle 7.6+.
|
||||
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
|
||||
if (details.requested.group == 'com.fasterxml.jackson.core'
|
||||
&& details.requested.name == 'jackson-core'
|
||||
&& details.requested.version.startsWith('2.15')) {
|
||||
details.useVersion '2.14.2'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// compatibility with Java 8
|
||||
errorprone("com.google.errorprone:error_prone_core:2.3.4")
|
||||
|
||||
Reference in New Issue
Block a user