1
0
mirror of https://github.com/google/nomulus synced 2026-06-09 16:33:02 +00:00

Compare commits

...

7 Commits

Author SHA1 Message Date
sarahcaseybot f5839777d1 Use Jackson to create and read Tld YAML files (#2082)
* Use Jackson to create and read Tld YAML files

* Add getObjectMapper to TldYamlUtils

* revert lockfiles

* Fix optionals

* Add more tests and javadocs

* small fixes
2023-07-26 16:25:03 -04:00
Weimin Yu 43d325d2a5 Checks flyway deadlock risk for new schema chagnes (#2078)
* Checks flyway deadlock risk for new schema chagnes
2023-07-26 14:35:48 -04:00
Pavlo Tkach 9b17adcb28 Add Console Settings -> Security front-end (#2079) 2023-07-26 12:50:31 -04:00
Ben McIlwain 9873772150 Allow EPP password to be set during login flow (#2080)
This is part of the spec in RFC 5730 that we hadn't implemented until now. Note
that this requires changing LoginFlow to be transactional, but I don't think
that should cause any issues.
2023-07-25 18:15:45 -04:00
Lai Jiang 342051e11d Fix the build due to jackson-core incompatibility (#2085) 2023-07-25 11:09:38 -04:00
Ben McIlwain 5f5cb8df9f Remove unnecessary overload of AsyncTaskEnqueuer.enqueueAsyncResave() (#2083)
It was only called in one place (in actual production code), and it was just
slightly obscuring the fact that re-saves can be scheduled for multiple points
in the future in a way that wasn't amazingly helpful to understanding of the
system logic at the callsite.
2023-07-24 13:37:36 -04:00
gbrodman 311d5ac9b6 Fix ICANN reporting and add rdap-queries field (#2081)
This includes two changes, the second necessary for testing the first.
1. We add the rdap-queries field as mandated by the amendment to the
   registry agreement,
   https://itp.cdn.icann.org/en/files/registry-agreement/proposed-global-amendment-base-gtld-registry-agreement-12-04-2023-en.pdf.
   This is fairly similar to the whois-queries field where we just query
   the logs, but instead of searching for "whois" we search for "rdap".
2. BigQuery doesn't use MAX to refer to the bigger of two fields; MAX
   accepts an array as an argument. In order to do what we want (and to
   have the BigQuery statements succeed), we need to use GREATEST.
   Tested both versions in alpha and production BigQuery instances.
2023-07-21 14:28:14 -04:00
50 changed files with 1461 additions and 98 deletions
+1
View File
@@ -31,6 +31,7 @@ tmp/
local.properties
.settings/
.loadpath
.DS_Store
# Eclipse Core
.project
+3 -1
View File
@@ -2,6 +2,8 @@
"/console-api":
{
"target": "http://localhost:8080",
"secure": true
"secure": false,
"logLevel": "debug",
"changeOrigin": true
}
}
+1 -1
View File
@@ -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",
+5 -3
View File
@@ -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>
+6 -2
View File
@@ -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;
}
}
+3 -1
View File
@@ -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();
});
+2
View File
@@ -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() {
@@ -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);
@@ -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.
@@ -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));
}
@@ -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
@@ -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.
@@ -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
@@ -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
View File
@@ -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
+1
View File
@@ -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']
+1
View File
@@ -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
View File
@@ -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
+21
View File
@@ -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")