Compare commits
9 Commits
nomulus-20
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9544d70048 | ||
|
|
50a639937a | ||
|
|
72016b1e5f | ||
|
|
25fcef8a5b | ||
|
|
186dd80567 | ||
|
|
c52983fb61 | ||
|
|
8a3ab00e58 | ||
|
|
49df9c325a | ||
|
|
929dccbfe3 |
@@ -59,8 +59,6 @@ Nomulus has the following capabilities:
|
||||
implementation that works with BIND. If you are using Google Cloud DNS, you
|
||||
may need to understand its capabilities and provide your own
|
||||
multi-[AS](https://en.wikipedia.org/wiki/Autonomous_system_\(Internet\)) solution.
|
||||
* **[WHOIS](https://en.wikipedia.org/wiki/WHOIS)**: A text-based protocol that
|
||||
returns ownership and contact information on registered domain names.
|
||||
* **[Registration Data Access Protocol
|
||||
(RDAP)](https://en.wikipedia.org/wiki/Registration_Data_Access_Protocol)**:
|
||||
A JSON API that returns structured, machine-readable information about
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"outputPath": {
|
||||
"base": "staged/dist/",
|
||||
@@ -112,7 +112,7 @@
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"builder": "@angular/build:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "console-webapp:build:production"
|
||||
@@ -136,16 +136,18 @@
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"builder": "@angular/build:extract-i18n",
|
||||
"options": {
|
||||
"buildTarget": "console-webapp:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"builder": "@angular/build:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"polyfills": [
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"inlineStyleLanguage": "scss",
|
||||
@@ -183,5 +185,31 @@
|
||||
"schematicCollections": [
|
||||
"@angular-eslint/schematics"
|
||||
]
|
||||
},
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"type": "component"
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"type": "directive"
|
||||
},
|
||||
"@schematics/angular:service": {
|
||||
"type": "service"
|
||||
},
|
||||
"@schematics/angular:guard": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:interceptor": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:module": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:pipe": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:resolver": {
|
||||
"typeSeparator": "."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"/console-api":
|
||||
{
|
||||
"target": "http://[::1]:8080",
|
||||
"target": "http://localhost:8080",
|
||||
"secure": false,
|
||||
"logLevel": "debug",
|
||||
"changeOrigin": true
|
||||
|
||||
@@ -21,7 +21,7 @@ module.exports = function (config) {
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage'),
|
||||
require('@angular-devkit/build-angular/plugins/karma')
|
||||
|
||||
],
|
||||
client: {
|
||||
jasmine: {
|
||||
|
||||
11439
console-webapp/package-lock.json
generated
@@ -16,29 +16,29 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^19.1.4",
|
||||
"@angular/cdk": "^19.1.2",
|
||||
"@angular/common": "^19.1.4",
|
||||
"@angular/compiler": "^19.1.4",
|
||||
"@angular/core": "^19.1.4",
|
||||
"@angular/forms": "^19.1.4",
|
||||
"@angular/material": "^19.1.2",
|
||||
"@angular/platform-browser": "^19.1.4",
|
||||
"@angular/platform-browser-dynamic": "^19.1.4",
|
||||
"@angular/router": "^19.1.4",
|
||||
"@angular/animations": "^21.1.5",
|
||||
"@angular/cdk": "^21.1.5",
|
||||
"@angular/common": "^21.1.5",
|
||||
"@angular/compiler": "^21.1.5",
|
||||
"@angular/core": "^21.1.5",
|
||||
"@angular/forms": "^21.1.5",
|
||||
"@angular/material": "^21.1.5",
|
||||
"@angular/platform-browser": "^21.1.5",
|
||||
"@angular/platform-browser-dynamic": "^21.1.5",
|
||||
"@angular/router": "^21.1.5",
|
||||
"rxjs": "~7.5.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^19.1.5",
|
||||
"@angular-eslint/builder": "19.0.2",
|
||||
"@angular-eslint/eslint-plugin": "19.0.2",
|
||||
"@angular-eslint/eslint-plugin-template": "19.0.2",
|
||||
"@angular-eslint/schematics": "19.0.2",
|
||||
"@angular-eslint/template-parser": "19.0.2",
|
||||
"@angular/cli": "~19.1.5",
|
||||
"@angular/compiler-cli": "^19.1.4",
|
||||
"@angular/build": "^21.1.4",
|
||||
"@angular/cli": "~21.1.4",
|
||||
"@angular/compiler-cli": "^21.1.5",
|
||||
"@types/jasmine": "~4.0.0",
|
||||
"@types/node": "^18.19.74",
|
||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||
@@ -52,6 +52,6 @@
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.0.0",
|
||||
"prettier": "2.8.7",
|
||||
"typescript": "^5.7.3"
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
<div class="console-app mat-typography">
|
||||
<app-header (toggleNavOpen)="toggleSidenav()"></app-header>
|
||||
<div class="console-app__global-spinner">
|
||||
<mat-progress-bar
|
||||
mode="indeterminate"
|
||||
*ngIf="globalLoader.isLoading"
|
||||
></mat-progress-bar>
|
||||
@if (globalLoader.isLoading) {
|
||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||
}
|
||||
</div>
|
||||
<mat-sidenav-container class="console-app__container">
|
||||
<mat-sidenav-content class="console-app__content-wrapper">
|
||||
|
||||
@@ -143,13 +143,14 @@
|
||||
<ng-container matColumnDef="domainName">
|
||||
<mat-header-cell *matHeaderCellDef>Domain Name</mat-header-cell>
|
||||
<mat-cell *matCellDef="let element">
|
||||
@if (getOperationMessage(element.domainName)) {
|
||||
<mat-icon
|
||||
*ngIf="getOperationMessage(element.domainName)"
|
||||
[matTooltip]="getOperationMessage(element.domainName)"
|
||||
matTooltipPosition="above"
|
||||
class="primary-text"
|
||||
>info</mat-icon
|
||||
>
|
||||
}
|
||||
<span>{{ element.domainName }}</span>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
@@ -209,9 +210,9 @@
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
|
||||
|
||||
<!-- Row shown when there is no matching data. -->
|
||||
<mat-row *matNoDataRow>
|
||||
<mat-cell colspan="6">No domains found</mat-cell>
|
||||
</mat-row>
|
||||
<tr class="mat-row" *matNoDataRow>
|
||||
<td class="mat-cell" colspan="6">No domains found</td>
|
||||
</tr>
|
||||
</mat-table>
|
||||
<mat-paginator
|
||||
[length]="totalResults"
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
MatDialogRef,
|
||||
} from '@angular/material/dialog';
|
||||
import { RESTRICTED_ELEMENTS } from '../shared/directives/userLevelVisiblity.directive';
|
||||
import { CdkColumnDef } from '@angular/cdk/table';
|
||||
|
||||
interface DomainResponse {
|
||||
message: string;
|
||||
@@ -114,6 +115,7 @@ export class ReasonDialogComponent {
|
||||
templateUrl: './domainList.component.html',
|
||||
styleUrls: ['./domainList.component.scss'],
|
||||
standalone: false,
|
||||
providers: [CdkColumnDef],
|
||||
})
|
||||
export class DomainListComponent {
|
||||
public static PATH = 'domain-list';
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<p class="console-app__header">
|
||||
<mat-toolbar>
|
||||
@if (breakpointObserver.isMobileView()) {
|
||||
<button
|
||||
mat-icon-button
|
||||
aria-label="Open navigation menu"
|
||||
(click)="toggleNavPane()"
|
||||
*ngIf="breakpointObserver.isMobileView()"
|
||||
class="console-app__menu-btn"
|
||||
>
|
||||
<mat-icon>menu</mat-icon>
|
||||
</button>
|
||||
}
|
||||
<a
|
||||
[routerLink]="'/home'"
|
||||
routerLinkActive="active"
|
||||
@@ -65,7 +66,9 @@
|
||||
</svg>
|
||||
</a>
|
||||
<span class="spacer"></span>
|
||||
<app-registrar-selector *ngIf="!breakpointObserver.isMobileView()" />
|
||||
@if (!breakpointObserver.isMobileView()) {
|
||||
<app-registrar-selector />
|
||||
}
|
||||
<button
|
||||
class="console-app__header-user-icon"
|
||||
mat-mini-fab
|
||||
@@ -79,5 +82,7 @@
|
||||
<button mat-menu-item (click)="logOut()">Log out</button>
|
||||
</mat-menu>
|
||||
</mat-toolbar>
|
||||
<app-registrar-selector *ngIf="breakpointObserver.isMobileView()" />
|
||||
@if (breakpointObserver.isMobileView()) {
|
||||
<app-registrar-selector />
|
||||
}
|
||||
</p>
|
||||
|
||||
@@ -9,41 +9,36 @@
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<mat-list role="list">
|
||||
<ng-container *ngFor="let item of historyRecords; let last = last">
|
||||
<mat-list-item class="history-list__item">
|
||||
<mat-icon
|
||||
[ngClass]="getIconClass(item.type)"
|
||||
class="history-list__icon"
|
||||
>
|
||||
{{ getIconForType(item.type) }}
|
||||
</mat-icon>
|
||||
|
||||
<div class="history-list__content">
|
||||
<div class="history-list__description">
|
||||
<span class="history-list__description--main">{{
|
||||
item.type
|
||||
}}</span>
|
||||
<div>
|
||||
<mat-chip
|
||||
*ngIf="parseDescription(item.description).detail"
|
||||
class="history-list__chip"
|
||||
>
|
||||
{{ parseDescription(item.description).detail }}
|
||||
</mat-chip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="history-list__user">
|
||||
<b>User - {{ item.actingUser.emailAddress }}</b>
|
||||
@for (item of historyRecords; track item; let last = $last) {
|
||||
<mat-list-item class="history-list__item">
|
||||
<mat-icon
|
||||
[ngClass]="getIconClass(item.type)"
|
||||
class="history-list__icon"
|
||||
>
|
||||
{{ getIconForType(item.type) }}
|
||||
</mat-icon>
|
||||
<div class="history-list__content">
|
||||
<div class="history-list__description">
|
||||
<span class="history-list__description--main">{{ item.type }}</span>
|
||||
<div>
|
||||
@if (parseDescription(item.description).detail) {
|
||||
<mat-chip class="history-list__chip">
|
||||
{{ parseDescription(item.description).detail }}
|
||||
</mat-chip>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="history-list__timestamp">
|
||||
{{ item.modificationTime | date : "MMM d, y, h:mm a" }}
|
||||
</span>
|
||||
</mat-list-item>
|
||||
|
||||
<mat-divider *ngIf="!last"></mat-divider>
|
||||
</ng-container>
|
||||
<div class="history-list__user">
|
||||
<b>User - {{ item.actingUser.emailAddress }}</b>
|
||||
</div>
|
||||
</div>
|
||||
<span class="history-list__timestamp">
|
||||
{{ item.modificationTime | date : "MMM d, y, h:mm a" }}
|
||||
</span>
|
||||
</mat-list-item>
|
||||
@if (!last) {
|
||||
<mat-divider></mat-divider>
|
||||
} }
|
||||
</mat-list>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
@@ -17,6 +17,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { HomeComponent } from './home.component';
|
||||
import { MaterialModule } from '../material.module';
|
||||
import { AppModule } from '../app.module';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
|
||||
describe('HomeComponent', () => {
|
||||
let component: HomeComponent;
|
||||
@@ -26,6 +29,11 @@ describe('HomeComponent', () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MaterialModule, AppModule],
|
||||
declarations: [HomeComponent],
|
||||
providers: [
|
||||
BackendService,
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(HomeComponent);
|
||||
|
||||
@@ -12,9 +12,11 @@
|
||||
[class.active]="router.url.includes(node.path)"
|
||||
[elementId]="getElementId(node)"
|
||||
>
|
||||
<mat-icon class="console-app__nav-icon" *ngIf="node.iconName">
|
||||
@if (node.iconName) {
|
||||
<mat-icon class="console-app__nav-icon">
|
||||
{{ node.iconName }}
|
||||
</mat-icon>
|
||||
}
|
||||
{{ node.title }}
|
||||
</mat-tree-node>
|
||||
<mat-nested-tree-node
|
||||
@@ -34,9 +36,11 @@
|
||||
{{ treeControl.isExpanded(node) ? "expand_more" : "chevron_right" }}
|
||||
</mat-icon>
|
||||
</button>
|
||||
<mat-icon class="console-app__nav-icon" *ngIf="node.iconName">
|
||||
@if (node.iconName) {
|
||||
<mat-icon class="console-app__nav-icon">
|
||||
{{ node.iconName }}
|
||||
</mat-icon>
|
||||
}
|
||||
{{ node.title }}
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
<h1 class="mat-headline-4">OT&E Status Check</h1>
|
||||
@if(registrarId() === null) {
|
||||
<h1>Missing registrarId param</h1>
|
||||
} @else if(isOte()) {
|
||||
<h1 *ngIf="oteStatusResponse().length">
|
||||
} @else if(isOte()) { @if (oteStatusResponse().length) {
|
||||
<h1>
|
||||
Status:
|
||||
<span>{{ oteStatusUnfinished().length ? "Unfinished" : "Completed" }}</span>
|
||||
</h1>
|
||||
}
|
||||
<div class="console-app__ote-status">
|
||||
@if(oteStatusCompleted().length) {
|
||||
<div class="console-app__ote-status_completed">
|
||||
<h1>Completed</h1>
|
||||
<div *ngFor="let entry of oteStatusCompleted()">
|
||||
<mat-icon>check_box</mat-icon>{{ entry.description }}
|
||||
</div>
|
||||
@for (entry of oteStatusCompleted(); track entry) {
|
||||
<div><mat-icon>check_box</mat-icon>{{ entry.description }}</div>
|
||||
}
|
||||
</div>
|
||||
} @if(oteStatusUnfinished().length) {
|
||||
<div class="console-app__ote-status_unfinished">
|
||||
<h1>Unfinished</h1>
|
||||
<div *ngFor="let entry of oteStatusUnfinished()">
|
||||
@for (entry of oteStatusUnfinished(); track entry) {
|
||||
<div>
|
||||
<mat-icon>check_box_outline_blank</mat-icon>{{ entry.description }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import { MaterialModule } from '../material.module';
|
||||
import { SnackBarModule } from '../snackbar.module';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { take } from 'rxjs';
|
||||
|
||||
@@ -31,7 +31,7 @@ export interface OteStatusResponse {
|
||||
|
||||
@Component({
|
||||
selector: 'app-ote-status',
|
||||
imports: [MaterialModule, SnackBarModule, CommonModule],
|
||||
imports: [MaterialModule, SnackBarModule],
|
||||
templateUrl: './oteStatus.component.html',
|
||||
styleUrls: ['./oteStatus.component.scss'],
|
||||
})
|
||||
|
||||
@@ -11,9 +11,8 @@
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
<div class="spacer"></div>
|
||||
@if(!inEdit && !registrarNotFound) {
|
||||
@if(!inEdit && !registrarNotFound) { @if (oteButtonVisible) {
|
||||
<button
|
||||
*ngIf="oteButtonVisible"
|
||||
mat-stroked-button
|
||||
(click)="checkOteStatus()"
|
||||
aria-label="Check OT&E account status"
|
||||
@@ -21,6 +20,7 @@
|
||||
>
|
||||
Check OT&E Status
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
@@ -39,10 +39,11 @@
|
||||
<h1>Registrar not found</h1>
|
||||
} @else {
|
||||
<h1>{{ registrarInEdit.registrarId }}</h1>
|
||||
<h2 *ngIf="registrarInEdit.registrarName !== registrarInEdit.registrarId">
|
||||
@if (registrarInEdit.registrarName !== registrarInEdit.registrarId) {
|
||||
<h2>
|
||||
{{ registrarInEdit.registrarName }}
|
||||
</h2>
|
||||
@if(inEdit) {
|
||||
} @if(inEdit) {
|
||||
<form (ngSubmit)="saveAndClose()">
|
||||
<div>
|
||||
<mat-form-field appearance="outline">
|
||||
@@ -60,15 +61,14 @@
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Onboarded TLDs: </mat-label>
|
||||
<mat-chip-grid #chipGrid aria-label="Enter TLD">
|
||||
<mat-chip-row
|
||||
*ngFor="let tld of registrarInEdit.allowedTlds"
|
||||
(removed)="removeTLD(tld)"
|
||||
>
|
||||
@for (tld of registrarInEdit.allowedTlds; track tld) {
|
||||
<mat-chip-row (removed)="removeTLD(tld)">
|
||||
{{ tld }}
|
||||
<button matChipRemove aria-label="'remove ' + tld">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
</button>
|
||||
</mat-chip-row>
|
||||
}
|
||||
</mat-chip-grid>
|
||||
<input
|
||||
placeholder="New tld..."
|
||||
|
||||
@@ -5,15 +5,16 @@
|
||||
<div class="console-app__registrars-header">
|
||||
<h1 class="mat-headline-4" forceFocus>Registrars</h1>
|
||||
<div class="spacer"></div>
|
||||
@if (oteButtonVisible) {
|
||||
<button
|
||||
mat-stroked-button
|
||||
*ngIf="oteButtonVisible"
|
||||
(click)="createOteAccount()"
|
||||
aria-label="Generate OT&E accounts"
|
||||
[elementId]="getElementIdForOteBlock()"
|
||||
>
|
||||
Create OT&E accounts
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
class="console-app__registrars-new"
|
||||
mat-flat-button
|
||||
@@ -43,10 +44,8 @@
|
||||
class="console-app__registrars-table"
|
||||
matSort
|
||||
>
|
||||
<ng-container
|
||||
*ngFor="let column of columns"
|
||||
[matColumnDef]="column.columnDef"
|
||||
>
|
||||
@for (column of columns; track column) {
|
||||
<ng-container [matColumnDef]="column.columnDef">
|
||||
<mat-header-cell *matHeaderCellDef>
|
||||
{{ column.header }}
|
||||
</mat-header-cell>
|
||||
@@ -55,6 +54,7 @@
|
||||
[innerHTML]="column.cell(row)"
|
||||
></mat-cell>
|
||||
</ng-container>
|
||||
}
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
|
||||
@@ -22,13 +22,12 @@
|
||||
</div>
|
||||
} @else {
|
||||
<mat-table [dataSource]="dataSource" class="mat-elevation-z0">
|
||||
<ng-container
|
||||
*ngFor="let column of columns"
|
||||
[matColumnDef]="column.columnDef"
|
||||
>
|
||||
@for (column of columns; track column) {
|
||||
<ng-container [matColumnDef]="column.columnDef">
|
||||
<mat-header-cell *matHeaderCellDef> {{ column.header }} </mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" [innerHTML]="column.cell(row)"></mat-cell>
|
||||
</ng-container>
|
||||
}
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
<div
|
||||
class="console-app__contact"
|
||||
*ngIf="contactService.contactInEdit"
|
||||
cdkTrapFocus
|
||||
[cdkTrapFocusAutoCapture]="true"
|
||||
>
|
||||
@if (contactService.contactInEdit) {
|
||||
<div class="console-app__contact" cdkTrapFocus [cdkTrapFocusAutoCapture]="true">
|
||||
<div class="console-app__contact-controls">
|
||||
<button
|
||||
mat-icon-button
|
||||
@@ -32,7 +28,6 @@
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if(isEditing || contactService.isContactNewView) {
|
||||
<h1>Contact Details</h1>
|
||||
<form (ngSubmit)="save($event)">
|
||||
@@ -46,7 +41,6 @@
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Primary account email: </mat-label>
|
||||
<input
|
||||
@@ -64,7 +58,6 @@
|
||||
"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Phone: </mat-label>
|
||||
<input
|
||||
@@ -74,7 +67,6 @@
|
||||
placeholder="+0.0000000000"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Fax: </mat-label>
|
||||
<input
|
||||
@@ -84,7 +76,6 @@
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h1>Contact Type</h1>
|
||||
<p class="console-app__contact-required">
|
||||
@@ -92,21 +83,18 @@
|
||||
(primary contact can't be updated)
|
||||
</p>
|
||||
<div class="">
|
||||
<ng-container
|
||||
*ngFor="let contactType of contactTypeToTextMap | keyvalue"
|
||||
@for (contactType of contactTypeToTextMap | keyvalue; track contactType)
|
||||
{ @if (shouldDisplayCheckbox(contactType.key)) {
|
||||
<mat-checkbox
|
||||
[checked]="checkboxIsChecked(contactType.key)"
|
||||
(change)="checkboxOnChange($event, contactType.key)"
|
||||
[disabled]="checkboxIsDisabled(contactType.key)"
|
||||
>
|
||||
<mat-checkbox
|
||||
*ngIf="shouldDisplayCheckbox(contactType.key)"
|
||||
[checked]="checkboxIsChecked(contactType.key)"
|
||||
(change)="checkboxOnChange($event, contactType.key)"
|
||||
[disabled]="checkboxIsDisabled(contactType.key)"
|
||||
>
|
||||
{{ contactType.value }}
|
||||
</mat-checkbox>
|
||||
</ng-container>
|
||||
{{ contactType.value }}
|
||||
</mat-checkbox>
|
||||
} }
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h1>RDAP Preferences</h1>
|
||||
<div>
|
||||
@@ -116,7 +104,6 @@
|
||||
>Show in Registrar RDAP record as admin contact</mat-checkbox
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<mat-checkbox
|
||||
[(ngModel)]="contactService.contactInEdit.visibleInRdapAsTech"
|
||||
@@ -124,7 +111,6 @@
|
||||
>Show in Registrar RDAP record as technical contact</mat-checkbox
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<mat-checkbox
|
||||
[(ngModel)]="contactService.contactInEdit.visibleInDomainRdapAsAbuse"
|
||||
@@ -198,15 +184,13 @@
|
||||
</mat-list-item>
|
||||
} @if(contactService.contactInEdit.visibleInRdapAsTech) {
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item
|
||||
role="listitem"
|
||||
*ngIf="contactService.contactInEdit.visibleInRdapAsTech"
|
||||
>
|
||||
@if (contactService.contactInEdit.visibleInRdapAsTech) {
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-value"
|
||||
>Show in Registrar RDAP record as technical contact</span
|
||||
>
|
||||
</mat-list-item>
|
||||
} @if(contactService.contactInEdit.visibleInDomainRdapAsAbuse) {
|
||||
} } @if(contactService.contactInEdit.visibleInDomainRdapAsAbuse) {
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-value"
|
||||
@@ -220,3 +204,4 @@
|
||||
</mat-card>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@if (registrarInEdit) {
|
||||
<div
|
||||
class="console-app__rdap-edit"
|
||||
*ngIf="registrarInEdit"
|
||||
cdkTrapFocus
|
||||
[cdkTrapFocusAutoCapture]="true"
|
||||
>
|
||||
@@ -12,7 +12,6 @@
|
||||
>
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
|
||||
<div class="console-app__rdap-edit-controls">
|
||||
<span>
|
||||
General registrar information for your RDAP record. This information is
|
||||
@@ -20,10 +19,8 @@
|
||||
</span>
|
||||
<div class="spacer"></div>
|
||||
</div>
|
||||
|
||||
<div class="console-app__rdap-edit">
|
||||
<h1>Personal info</h1>
|
||||
|
||||
<form (ngSubmit)="save($event)">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Email: </mat-label>
|
||||
@@ -34,7 +31,6 @@
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Phone: </mat-label>
|
||||
<input
|
||||
@@ -44,7 +40,6 @@
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Fax: </mat-label>
|
||||
<input
|
||||
@@ -54,7 +49,6 @@
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Street Address (line 1): </mat-label>
|
||||
<input
|
||||
@@ -64,7 +58,6 @@
|
||||
[(ngModel)]="registrarInEdit.localizedAddress.street![0]"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Street Address (line 2): </mat-label>
|
||||
<input
|
||||
@@ -74,7 +67,6 @@
|
||||
[(ngModel)]="registrarInEdit.localizedAddress.street![1]"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>City: </mat-label>
|
||||
<input
|
||||
@@ -84,7 +76,6 @@
|
||||
[(ngModel)]="(registrarInEdit.localizedAddress || {}).city"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>State or Province: </mat-label>
|
||||
<input
|
||||
@@ -94,7 +85,6 @@
|
||||
[(ngModel)]="(registrarInEdit.localizedAddress || {}).state"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Country: </mat-label>
|
||||
<input
|
||||
@@ -104,7 +94,6 @@
|
||||
[(ngModel)]="(registrarInEdit.localizedAddress || {}).countryCode"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Postal code: </mat-label>
|
||||
<input
|
||||
@@ -114,7 +103,6 @@
|
||||
[(ngModel)]="(registrarInEdit.localizedAddress || {}).zip"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
@@ -126,3 +114,4 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
[formGroup]="passwordUpdateForm"
|
||||
(submitResults)="save($event)"
|
||||
/>
|
||||
@if(userDataService.userData()?.isAdmin) {
|
||||
<div class="settings-security__reset-password-field">
|
||||
<h2>Need to reset your EPP password?</h2>
|
||||
<button
|
||||
@@ -33,5 +32,4 @@
|
||||
Reset EPP password via email
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,7 @@ import { RegistrarService } from 'src/app/registrar/registrar.service';
|
||||
import { SecurityService } from './security.service';
|
||||
import { UserDataService } from 'src/app/shared/services/userData.service';
|
||||
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { MaterialModule } from 'src/app/material.module';
|
||||
import { filter, switchMap, take } from 'rxjs';
|
||||
import { BackendService } from 'src/app/shared/services/backend.service';
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
<button mat-button color="warn" (click)="onSave()">Confirm</button>
|
||||
</mat-dialog-actions>
|
||||
`,
|
||||
imports: [CommonModule, MaterialModule],
|
||||
imports: [MaterialModule],
|
||||
})
|
||||
export class ResetEppPasswordComponent {
|
||||
constructor(public dialogRef: MatDialogRef<ResetEppPasswordComponent>) {}
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
>
|
||||
</mat-list-item>
|
||||
<mat-divider></mat-divider>
|
||||
@for (item of dataSource.ipAddressAllowList; track item.value) {
|
||||
@for (item of dataSource.ipAddressAllowList; track $index){
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-value">{{ item.value }}</span>
|
||||
</mat-list-item>
|
||||
|
||||
@@ -12,7 +12,13 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import {
|
||||
ComponentFixture,
|
||||
fakeAsync,
|
||||
TestBed,
|
||||
tick,
|
||||
waitForAsync,
|
||||
} from '@angular/core/testing';
|
||||
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
@@ -30,43 +36,32 @@ import { MOCK_REGISTRAR_SERVICE } from 'src/testdata/registrar/registrar.service
|
||||
describe('SecurityComponent', () => {
|
||||
let component: SecurityComponent;
|
||||
let fixture: ComponentFixture<SecurityComponent>;
|
||||
let fetchSecurityDetailsSpy: Function;
|
||||
let saveSpy: Function;
|
||||
let securityServiceStub: Partial<SecurityService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const securityServiceSpy = jasmine.createSpyObj(SecurityService, [
|
||||
'fetchSecurityDetails',
|
||||
'saveChanges',
|
||||
]);
|
||||
|
||||
fetchSecurityDetailsSpy =
|
||||
securityServiceSpy.fetchSecurityDetails.and.returnValue(of());
|
||||
|
||||
saveSpy = securityServiceSpy.saveChanges.and.returnValue(of());
|
||||
securityServiceStub = {
|
||||
isEditingSecurity: false,
|
||||
isEditingPassword: false,
|
||||
saveChanges: jasmine.createSpy('saveChanges').and.returnValue(of({})),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [SecurityEditComponent, SecurityComponent],
|
||||
imports: [MaterialModule, BrowserAnimationsModule, FormsModule],
|
||||
providers: [
|
||||
BackendService,
|
||||
SecurityService,
|
||||
{ provide: SecurityService, useValue: securityServiceStub },
|
||||
{ provide: RegistrarService, useValue: MOCK_REGISTRAR_SERVICE },
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
})
|
||||
.overrideComponent(SecurityComponent, {
|
||||
set: {
|
||||
providers: [
|
||||
{ provide: SecurityService, useValue: securityServiceSpy },
|
||||
],
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
}).compileComponents();
|
||||
|
||||
saveSpy = securityServiceStub.saveChanges as jasmine.Spy;
|
||||
|
||||
fixture = TestBed.createComponent(SecurityComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
@@ -93,17 +88,36 @@ describe('SecurityComponent', () => {
|
||||
});
|
||||
}));
|
||||
|
||||
it('should remove ip', waitForAsync(() => {
|
||||
component.dataSource.ipAddressAllowList =
|
||||
component.dataSource.ipAddressAllowList?.splice(1);
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
let listElems: Array<HTMLElement> = Array.from(
|
||||
fixture.nativeElement.querySelectorAll('span.console-app__list-value')
|
||||
);
|
||||
expect(listElems.map((e) => e.textContent)).toContain(
|
||||
'No IP addresses on file.'
|
||||
);
|
||||
it('should remove ip', fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const editBtn = fixture.nativeElement.querySelector(
|
||||
'button[aria-label="Edit security settings"]'
|
||||
);
|
||||
editBtn.click();
|
||||
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
|
||||
const removeIpBtn = fixture.nativeElement.querySelector(
|
||||
'.console-app__removeIp'
|
||||
);
|
||||
removeIpBtn.click();
|
||||
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
|
||||
const saveBtn = fixture.nativeElement.querySelector(
|
||||
'.settings-security__edit-save'
|
||||
);
|
||||
saveBtn.click();
|
||||
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(saveSpy).toHaveBeenCalledWith({
|
||||
ipAddressAllowList: [],
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -119,21 +133,34 @@ describe('SecurityComponent', () => {
|
||||
expect(component.securityService.isEditingPassword).toBeTrue();
|
||||
});
|
||||
|
||||
it('should call save', waitForAsync(async () => {
|
||||
component.editSecurity();
|
||||
await fixture.whenStable();
|
||||
it('should call save', fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const editBtn = fixture.nativeElement.querySelector(
|
||||
'button[aria-label="Edit security settings"]'
|
||||
);
|
||||
editBtn.click();
|
||||
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
|
||||
const el = fixture.nativeElement.querySelector(
|
||||
'.console-app__clientCertificateValue'
|
||||
);
|
||||
el.value = 'test';
|
||||
el.dispatchEvent(new Event('input'));
|
||||
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
fixture.nativeElement
|
||||
.querySelector('.settings-security__edit-save')
|
||||
.click();
|
||||
expect(saveSpy).toHaveBeenCalledOnceWith({
|
||||
|
||||
const saveBtn = fixture.nativeElement.querySelector(
|
||||
'.settings-security__edit-save'
|
||||
);
|
||||
saveBtn.click();
|
||||
|
||||
tick();
|
||||
expect(saveSpy).toHaveBeenCalledWith({
|
||||
ipAddressAllowList: [{ value: '123.123.123.123' }],
|
||||
clientCertificate: 'test',
|
||||
});
|
||||
|
||||
@@ -23,9 +23,11 @@
|
||||
<button
|
||||
matSuffix
|
||||
mat-icon-button
|
||||
class="console-app__removeIp"
|
||||
[attr.aria-label]="'Remove IP entry ' + ip.value"
|
||||
(click)="removeIpEntry(ip)"
|
||||
[disabled]="isUpdating"
|
||||
type="button"
|
||||
>
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
required
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
<mat-error *ngIf="hasError('oldPassword') as errorText">{{
|
||||
errorText
|
||||
}}</mat-error>
|
||||
@if (hasError('oldPassword'); as errorText) {
|
||||
<mat-error>{{ errorText }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
}
|
||||
@@ -30,9 +30,9 @@
|
||||
required
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<mat-error *ngIf="hasError('newPassword') as errorText">{{
|
||||
errorText
|
||||
}}</mat-error>
|
||||
@if (hasError('newPassword'); as errorText) {
|
||||
<mat-error>{{ errorText }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="console-app__password-input-form-field">
|
||||
@@ -45,9 +45,9 @@
|
||||
required
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<mat-error *ngIf="hasError('newPasswordRepeat') as errorText">{{
|
||||
errorText
|
||||
}}</mat-error>
|
||||
@if (hasError('newPasswordRepeat'); as errorText) {
|
||||
<mat-error>{{ errorText }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -29,10 +29,9 @@ export const DISABLED_ELEMENTS_PER_ROLE = {
|
||||
RESTRICTED_ELEMENTS.REGISTRAR_ELEMENT,
|
||||
RESTRICTED_ELEMENTS.OTE,
|
||||
RESTRICTED_ELEMENTS.SUSPEND,
|
||||
RESTRICTED_ELEMENTS.ACTIVITY_PER_USER,
|
||||
],
|
||||
SUPPORT_LEAD: [],
|
||||
SUPPORT_AGENT: [RESTRICTED_ELEMENTS.ACTIVITY_PER_USER],
|
||||
SUPPORT_AGENT: [],
|
||||
};
|
||||
|
||||
@Directive({
|
||||
|
||||
@@ -50,15 +50,17 @@
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="isNewUser" class="console-app__user-details-save-password">
|
||||
@if (isNewUser) {
|
||||
<div class="console-app__user-details-save-password">
|
||||
<mat-icon>priority_high</mat-icon>
|
||||
Please save the password. For your security, we do not store passwords in a
|
||||
recoverable format.
|
||||
</div>
|
||||
|
||||
<p *ngIf="isLoading">
|
||||
} @if (isLoading) {
|
||||
<p>
|
||||
<mat-progress-bar mode="query"></mat-progress-bar>
|
||||
</p>
|
||||
}
|
||||
|
||||
<mat-card appearance="outlined">
|
||||
<mat-card-content>
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, computed } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { SelectedRegistrarModule } from '../app.module';
|
||||
@@ -31,7 +30,6 @@ import { UserEditFormComponent } from './userEditForm.component';
|
||||
FormsModule,
|
||||
MaterialModule,
|
||||
SnackBarModule,
|
||||
CommonModule,
|
||||
SelectedRegistrarModule,
|
||||
UserEditFormComponent,
|
||||
],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<div class="console-app__user-edit">
|
||||
<form (ngSubmit)="saveEdit($event)" #form>
|
||||
<p *ngIf="isNew()">
|
||||
@if (isNew()) {
|
||||
<p>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label
|
||||
>User name prefix:
|
||||
@@ -19,6 +20,7 @@
|
||||
/>
|
||||
</mat-form-field>
|
||||
</p>
|
||||
}
|
||||
<p>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label
|
||||
@@ -44,7 +46,6 @@
|
||||
Save
|
||||
</button>
|
||||
</form>
|
||||
@if(userDataService.userData()?.isAdmin) {
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
@@ -53,5 +54,4 @@
|
||||
>
|
||||
Reset registry lock password
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
@@ -50,7 +49,7 @@ import { HttpErrorResponse } from '@angular/common/http';
|
||||
<button mat-button color="warn" (click)="onSave()">Confirm</button>
|
||||
</mat-dialog-actions>
|
||||
`,
|
||||
imports: [CommonModule, MaterialModule],
|
||||
imports: [MaterialModule],
|
||||
})
|
||||
export class ResetRegistryLockPasswordComponent {
|
||||
constructor(
|
||||
@@ -72,7 +71,7 @@ export class ResetRegistryLockPasswordComponent {
|
||||
selector: 'app-user-edit-form',
|
||||
templateUrl: './userEditForm.component.html',
|
||||
styleUrls: ['./userEditForm.component.scss'],
|
||||
imports: [FormsModule, MaterialModule, CommonModule],
|
||||
imports: [FormsModule, MaterialModule],
|
||||
providers: [],
|
||||
})
|
||||
export class UserEditFormComponent {
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Component, effect } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
@@ -35,7 +34,6 @@ import { UserEditFormComponent } from './userEditForm.component';
|
||||
FormsModule,
|
||||
MaterialModule,
|
||||
SnackBarModule,
|
||||
CommonModule,
|
||||
SelectedRegistrarModule,
|
||||
UsersListComponent,
|
||||
UserEditFormComponent,
|
||||
|
||||
@@ -5,15 +5,14 @@
|
||||
class="console-app__users-table"
|
||||
matSort
|
||||
>
|
||||
<ng-container
|
||||
*ngFor="let column of columns"
|
||||
[matColumnDef]="column.columnDef"
|
||||
>
|
||||
@for (column of columns; track column) {
|
||||
<ng-container [matColumnDef]="column.columnDef">
|
||||
<mat-header-cell *matHeaderCellDef>
|
||||
{{ column.header }}
|
||||
</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" [innerHTML]="column.cell(row)"></mat-cell>
|
||||
</ng-container>
|
||||
}
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
effect,
|
||||
@@ -43,7 +42,7 @@ export const columns = [
|
||||
selector: 'app-users-list',
|
||||
templateUrl: './usersList.component.html',
|
||||
styleUrls: ['./usersList.component.scss'],
|
||||
imports: [MaterialModule, CommonModule],
|
||||
imports: [MaterialModule],
|
||||
providers: [],
|
||||
})
|
||||
export class UsersListComponent {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { enableProdMode } from '@angular/core';
|
||||
import { enableProdMode, provideZoneChangeDetection } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
@@ -23,5 +23,7 @@ if (environment.production || environment.sandbox) {
|
||||
}
|
||||
|
||||
platformBrowserDynamic()
|
||||
.bootstrapModule(AppModule)
|
||||
.bootstrapModule(AppModule, {
|
||||
applicationProviders: [provideZoneChangeDetection()],
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
|
||||
@@ -14,14 +14,10 @@
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "es2020",
|
||||
"lib": [
|
||||
"es2020",
|
||||
"dom"
|
||||
],
|
||||
"useDefineForClassFields": false
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
|
||||
@@ -76,7 +76,7 @@ import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
|
||||
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
|
||||
import google.registry.model.tld.Tld;
|
||||
import google.registry.model.transfer.DomainTransferData;
|
||||
import google.registry.model.transfer.TransferData.TransferServerApproveEntity;
|
||||
import google.registry.model.transfer.DomainTransferData.TransferServerApproveEntity;
|
||||
import google.registry.model.transfer.TransferResponse.DomainTransferResponse;
|
||||
import google.registry.model.transfer.TransferStatus;
|
||||
import jakarta.inject.Inject;
|
||||
|
||||
@@ -36,8 +36,7 @@ import google.registry.model.poll.PollMessage;
|
||||
import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
|
||||
import google.registry.model.tld.Tld;
|
||||
import google.registry.model.transfer.DomainTransferData;
|
||||
import google.registry.model.transfer.TransferData;
|
||||
import google.registry.model.transfer.TransferData.TransferServerApproveEntity;
|
||||
import google.registry.model.transfer.DomainTransferData.TransferServerApproveEntity;
|
||||
import google.registry.model.transfer.TransferResponse.DomainTransferResponse;
|
||||
import google.registry.model.transfer.TransferStatus;
|
||||
import google.registry.persistence.VKey;
|
||||
@@ -51,7 +50,7 @@ import org.joda.time.DateTime;
|
||||
*/
|
||||
public final class DomainTransferUtils {
|
||||
|
||||
/** Sets up {@link TransferData} for a domain with links to entities for server approval. */
|
||||
/** Sets up {@link DomainTransferData} for a domain with links to entities for server approval. */
|
||||
public static DomainTransferData createPendingTransferData(
|
||||
String domainRepoId,
|
||||
Long historyId,
|
||||
@@ -179,7 +178,7 @@ public final class DomainTransferUtils {
|
||||
/** Create a poll message for the gaining client in a transfer. */
|
||||
public static PollMessage createGainingTransferPollMessage(
|
||||
String targetId,
|
||||
TransferData transferData,
|
||||
DomainTransferData transferData,
|
||||
@Nullable DateTime extendedRegistrationExpirationTime,
|
||||
DateTime now,
|
||||
HistoryEntryId domainHistoryId) {
|
||||
@@ -202,7 +201,7 @@ public final class DomainTransferUtils {
|
||||
/** Create a poll message for the losing client in a transfer. */
|
||||
public static PollMessage createLosingTransferPollMessage(
|
||||
String targetId,
|
||||
TransferData transferData,
|
||||
DomainTransferData transferData,
|
||||
@Nullable DateTime extendedRegistrationExpirationTime,
|
||||
HistoryEntryId domainHistoryId) {
|
||||
return new PollMessage.OneTime.Builder()
|
||||
@@ -216,10 +215,10 @@ public final class DomainTransferUtils {
|
||||
.build();
|
||||
}
|
||||
|
||||
/** Create a {@link DomainTransferResponse} off of the info in a {@link TransferData}. */
|
||||
/** Create a {@link DomainTransferResponse} off of the info in a {@link DomainTransferData}. */
|
||||
static DomainTransferResponse createTransferResponse(
|
||||
String targetId,
|
||||
TransferData transferData,
|
||||
DomainTransferData transferData,
|
||||
@Nullable DateTime extendedRegistrationExpirationTime) {
|
||||
return new DomainTransferResponse.Builder()
|
||||
.setDomainName(targetId)
|
||||
|
||||
@@ -34,7 +34,6 @@ import com.google.common.collect.ImmutableSet;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import google.registry.config.RegistryConfig;
|
||||
import google.registry.model.eppcommon.StatusValue;
|
||||
import google.registry.model.transfer.TransferData;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.util.NonFinalForTesting;
|
||||
import jakarta.persistence.Access;
|
||||
@@ -207,27 +206,6 @@ public abstract class EppResource extends UpdateAutoTimestampEntity implements B
|
||||
/** EppResources that are loaded via foreign keys should implement this marker interface. */
|
||||
public interface ForeignKeyedEppResource {}
|
||||
|
||||
/** An interface for resources that have transfer data. */
|
||||
public interface ResourceWithTransferData<T extends TransferData> {
|
||||
T getTransferData();
|
||||
|
||||
/**
|
||||
* The time that this resource was last transferred.
|
||||
*
|
||||
* <p>Can be null if the resource has never been transferred.
|
||||
*/
|
||||
DateTime getLastTransferTime();
|
||||
}
|
||||
|
||||
/** An interface for builders of resources that have transfer data. */
|
||||
public interface BuilderWithTransferData<
|
||||
T extends TransferData, B extends BuilderWithTransferData<T, B>> {
|
||||
B setTransferData(T transferData);
|
||||
|
||||
/** Set the time when this resource was transferred. */
|
||||
B setLastTransferTime(DateTime lastTransferTime);
|
||||
}
|
||||
|
||||
/** Abstract builder for {@link EppResource} types. */
|
||||
public abstract static class Builder<T extends EppResource, B extends Builder<T, B>>
|
||||
extends GenericBuilder<T, B> {
|
||||
|
||||
@@ -29,7 +29,6 @@ import google.registry.model.poll.PendingActionNotificationResponse.DomainPendin
|
||||
import google.registry.model.poll.PollMessage;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.model.transfer.DomainTransferData;
|
||||
import google.registry.model.transfer.TransferData;
|
||||
import google.registry.model.transfer.TransferResponse;
|
||||
import google.registry.model.transfer.TransferResponse.DomainTransferResponse;
|
||||
import google.registry.model.transfer.TransferStatus;
|
||||
@@ -79,7 +78,7 @@ public final class ResourceTransferUtils {
|
||||
if (!domain.getStatusValues().contains(StatusValue.PENDING_TRANSFER)) {
|
||||
return;
|
||||
}
|
||||
TransferData oldTransferData = domain.getTransferData();
|
||||
DomainTransferData oldTransferData = domain.getTransferData();
|
||||
tm().delete(oldTransferData.getServerApproveEntities());
|
||||
tm().put(
|
||||
new PollMessage.OneTime.Builder()
|
||||
@@ -99,8 +98,8 @@ public final class ResourceTransferUtils {
|
||||
* Turn a domain into a builder with its pending transfer resolved.
|
||||
*
|
||||
* <p>This removes the {@link StatusValue#PENDING_TRANSFER} status, sets the {@link
|
||||
* TransferStatus}, clears all the server-approve fields on the {@link TransferData}, and sets the
|
||||
* expiration time of the last pending transfer to now.
|
||||
* TransferStatus}, clears all the server-approve fields on the {@link DomainTransferData}, and
|
||||
* sets the expiration time of the last pending transfer to now.
|
||||
*/
|
||||
private static Domain.Builder resolvePendingTransfer(
|
||||
Domain domain, TransferStatus transferStatus, DateTime now) {
|
||||
@@ -125,9 +124,9 @@ public final class ResourceTransferUtils {
|
||||
* Resolve a pending transfer by awarding it to the gaining client.
|
||||
*
|
||||
* <p>This removes the {@link StatusValue#PENDING_TRANSFER} status, sets the {@link
|
||||
* TransferStatus}, clears all the server-approve fields on the {@link TransferData}, sets the new
|
||||
* client id, and sets the last transfer time and the expiration time of the last pending transfer
|
||||
* to now.
|
||||
* TransferStatus}, clears all the server-approve fields on the {@link DomainTransferData}, sets
|
||||
* the new client id, and sets the last transfer time and the expiration time of the last pending
|
||||
* transfer to now.
|
||||
*/
|
||||
public static Domain approvePendingTransfer(
|
||||
Domain domain, TransferStatus transferStatus, DateTime now) {
|
||||
@@ -143,9 +142,9 @@ public final class ResourceTransferUtils {
|
||||
* Resolve a pending transfer by denying it.
|
||||
*
|
||||
* <p>This removes the {@link StatusValue#PENDING_TRANSFER} status, sets the {@link
|
||||
* TransferStatus}, clears all the server-approve fields on the {@link TransferData}, sets the
|
||||
* expiration time of the last pending transfer to now, sets the last EPP update time to now, and
|
||||
* sets the last EPP update client id to the given client id.
|
||||
* TransferStatus}, clears all the server-approve fields on the {@link DomainTransferData}, sets
|
||||
* the expiration time of the last pending transfer to now, sets the last EPP update time to now,
|
||||
* and sets the last EPP update client id to the given client id.
|
||||
*/
|
||||
public static Domain denyPendingTransfer(
|
||||
Domain domain, TransferStatus transferStatus, DateTime now, String lastEppUpdateRegistrarId) {
|
||||
|
||||
@@ -26,7 +26,7 @@ import google.registry.model.UnsafeSerializable;
|
||||
import google.registry.model.annotations.IdAllocation;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
|
||||
import google.registry.model.transfer.TransferData.TransferServerApproveEntity;
|
||||
import google.registry.model.transfer.DomainTransferData.TransferServerApproveEntity;
|
||||
import google.registry.persistence.VKey;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.EnumType;
|
||||
|
||||
@@ -43,7 +43,6 @@ import com.google.common.collect.Sets;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import google.registry.flows.ResourceFlowUtils;
|
||||
import google.registry.model.EppResource;
|
||||
import google.registry.model.EppResource.ResourceWithTransferData;
|
||||
import google.registry.model.billing.BillingRecurrence;
|
||||
import google.registry.model.domain.launch.LaunchNotice;
|
||||
import google.registry.model.domain.rgp.GracePeriodStatus;
|
||||
@@ -96,8 +95,7 @@ import org.joda.time.Interval;
|
||||
@MappedSuperclass
|
||||
@Embeddable
|
||||
@Access(AccessType.FIELD)
|
||||
public class DomainBase extends EppResource
|
||||
implements ResourceWithTransferData<DomainTransferData> {
|
||||
public class DomainBase extends EppResource {
|
||||
|
||||
/** The max number of years that a domain can be registered for, as set by ICANN policy. */
|
||||
public static final int MAX_REGISTRATION_YEARS = 10;
|
||||
@@ -319,12 +317,10 @@ public class DomainBase extends EppResource
|
||||
return Optional.ofNullable(autorenewEndTime.equals(END_OF_TIME) ? null : autorenewEndTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainTransferData getTransferData() {
|
||||
return Optional.ofNullable(transferData).orElse(DomainTransferData.EMPTY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DateTime getLastTransferTime() {
|
||||
return lastTransferTime;
|
||||
}
|
||||
@@ -605,7 +601,7 @@ public class DomainBase extends EppResource
|
||||
|
||||
/** A builder for constructing {@link Domain}, since it is immutable. */
|
||||
public static class Builder<T extends DomainBase, B extends Builder<T, B>>
|
||||
extends EppResource.Builder<T, B> implements BuilderWithTransferData<DomainTransferData, B> {
|
||||
extends EppResource.Builder<T, B> {
|
||||
|
||||
public Builder() {}
|
||||
|
||||
@@ -783,13 +779,11 @@ public class DomainBase extends EppResource
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
@Override
|
||||
public B setTransferData(DomainTransferData transferData) {
|
||||
getInstance().transferData = transferData;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
@Override
|
||||
public B setLastTransferTime(DateTime lastTransferTime) {
|
||||
getInstance().lastTransferTime = lastTransferTime;
|
||||
return thisCastToDerived();
|
||||
|
||||
@@ -123,8 +123,7 @@ public class EppXmlTransformer {
|
||||
|
||||
public static byte[] marshal(EppOutput root, ValidationMode validation) throws XmlException {
|
||||
byte[] bytes = marshal(OUTPUT_TRANSFORMER, root, validation);
|
||||
if (!RegistryEnvironment.PRODUCTION.equals(RegistryEnvironment.get())
|
||||
&& hasFeeExtension(root)) {
|
||||
if (hasFeeExtension(root)) {
|
||||
return FeeExtensionXmlTagNormalizer.normalize(new String(bytes, UTF_8)).getBytes(UTF_8);
|
||||
}
|
||||
return bytes;
|
||||
|
||||
@@ -24,7 +24,6 @@ import static google.registry.util.DomainNameUtils.canonicalizeHostname;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.model.EppResource;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.transfer.TransferData;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.persistence.converter.InetAddressSetUserType;
|
||||
import jakarta.persistence.Access;
|
||||
@@ -41,8 +40,8 @@ import org.joda.time.DateTime;
|
||||
/**
|
||||
* A persistable Host resource including mutable and non-mutable fields.
|
||||
*
|
||||
* <p>A host's {@link TransferData} is stored on the superordinate domain. Non-subordinate hosts
|
||||
* don't carry a full set of TransferData; all they have is lastTransferTime.
|
||||
* <p>A host's full transfer data is stored on the superordinate domain. Non-subordinate hosts don't
|
||||
* carry a full set of TransferData; all they have is lastTransferTime.
|
||||
*
|
||||
* <p>This class deliberately does not include an {@link jakarta.persistence.Id} so that any
|
||||
* foreign-keyed fields can refer to the proper parent entity's ID, whether we're storing this in
|
||||
|
||||
@@ -32,14 +32,12 @@ import google.registry.model.domain.DomainRenewData;
|
||||
import google.registry.model.eppoutput.EppResponse.ResponseData;
|
||||
import google.registry.model.host.Host;
|
||||
import google.registry.model.host.HostHistory;
|
||||
import google.registry.model.poll.PendingActionNotificationResponse.ContactPendingActionNotificationResponse;
|
||||
import google.registry.model.poll.PendingActionNotificationResponse.DomainPendingActionNotificationResponse;
|
||||
import google.registry.model.poll.PendingActionNotificationResponse.HostPendingActionNotificationResponse;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
|
||||
import google.registry.model.transfer.TransferData.TransferServerApproveEntity;
|
||||
import google.registry.model.transfer.DomainTransferData.TransferServerApproveEntity;
|
||||
import google.registry.model.transfer.TransferResponse;
|
||||
import google.registry.model.transfer.TransferResponse.ContactTransferResponse;
|
||||
import google.registry.model.transfer.TransferResponse.DomainTransferResponse;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.persistence.WithVKey;
|
||||
@@ -406,12 +404,8 @@ public abstract class PollMessage extends ImmutableObject
|
||||
if (pendingActionNotificationResponse != null) {
|
||||
// Promote the pending action notification response to its specialized type.
|
||||
if (contactId != null) {
|
||||
pendingActionNotificationResponse =
|
||||
ContactPendingActionNotificationResponse.create(
|
||||
pendingActionNotificationResponse.nameOrId.value,
|
||||
pendingActionNotificationResponse.getActionResult(),
|
||||
pendingActionNotificationResponse.getTrid(),
|
||||
pendingActionNotificationResponse.processedDate);
|
||||
// Contacts are no longer supported
|
||||
pendingActionNotificationResponse = null;
|
||||
} else if (domainName != null) {
|
||||
pendingActionNotificationResponse =
|
||||
DomainPendingActionNotificationResponse.create(
|
||||
@@ -432,16 +426,8 @@ public abstract class PollMessage extends ImmutableObject
|
||||
// The transferResponse is currently an unspecialized TransferResponse instance, create the
|
||||
// appropriate subclass so that the value is consistently specialized
|
||||
if (contactId != null) {
|
||||
transferResponse =
|
||||
new ContactTransferResponse.Builder()
|
||||
.setContactId(contactId)
|
||||
.setGainingRegistrarId(transferResponse.getGainingRegistrarId())
|
||||
.setLosingRegistrarId(transferResponse.getLosingRegistrarId())
|
||||
.setTransferStatus(transferResponse.getTransferStatus())
|
||||
.setTransferRequestTime(transferResponse.getTransferRequestTime())
|
||||
.setPendingTransferExpirationTime(
|
||||
transferResponse.getPendingTransferExpirationTime())
|
||||
.build();
|
||||
// Contacts are no longer supported
|
||||
transferResponse = null;
|
||||
} else if (domainName != null) {
|
||||
transferResponse =
|
||||
new DomainTransferResponse.Builder()
|
||||
@@ -488,9 +474,6 @@ public abstract class PollMessage extends ImmutableObject
|
||||
|
||||
// Set identifier fields based on the type of the notification response.
|
||||
if (instance.pendingActionNotificationResponse
|
||||
instanceof ContactPendingActionNotificationResponse) {
|
||||
instance.contactId = instance.pendingActionNotificationResponse.nameOrId.value;
|
||||
} else if (instance.pendingActionNotificationResponse
|
||||
instanceof DomainPendingActionNotificationResponse) {
|
||||
instance.domainName = instance.pendingActionNotificationResponse.nameOrId.value;
|
||||
} else if (instance.pendingActionNotificationResponse
|
||||
@@ -507,9 +490,7 @@ public abstract class PollMessage extends ImmutableObject
|
||||
.orElse(null);
|
||||
|
||||
// Set the identifier according to the TransferResponse type.
|
||||
if (instance.transferResponse instanceof ContactTransferResponse) {
|
||||
instance.contactId = ((ContactTransferResponse) instance.transferResponse).getContactId();
|
||||
} else if (instance.transferResponse instanceof DomainTransferResponse response) {
|
||||
if (instance.transferResponse instanceof DomainTransferResponse response) {
|
||||
instance.domainName = response.getDomainName();
|
||||
instance.extendedRegistrationExpirationTime =
|
||||
response.getExtendedRegistrationExpirationTime();
|
||||
|
||||
@@ -25,7 +25,7 @@ import jakarta.xml.bind.annotation.XmlElement;
|
||||
import jakarta.xml.bind.annotation.XmlTransient;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Fields common to {@link TransferData} and {@link TransferResponse}. */
|
||||
/** Fields common to {@link DomainTransferData} and {@link TransferResponse}. */
|
||||
@XmlTransient
|
||||
@MappedSuperclass
|
||||
public abstract class BaseTransferObject extends ImmutableObject implements UnsafeSerializable {
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
// Copyright 2020 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.transfer;
|
||||
|
||||
import jakarta.persistence.Embeddable;
|
||||
|
||||
/** Transfer data for contact. */
|
||||
@Embeddable
|
||||
public class ContactTransferData extends TransferData {
|
||||
public static final ContactTransferData EMPTY = new ContactTransferData();
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return EMPTY.equals(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Builder createEmptyBuilder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder asBuilder() {
|
||||
return new Builder(clone(this));
|
||||
}
|
||||
|
||||
public static class Builder extends TransferData.Builder<ContactTransferData, Builder> {
|
||||
/** Create a {@link Builder} wrapping a new instance. */
|
||||
public Builder() {}
|
||||
|
||||
/** Create a {@link Builder} wrapping the given instance. */
|
||||
private Builder(ContactTransferData instance) {
|
||||
super(instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,14 +14,20 @@
|
||||
|
||||
package google.registry.model.transfer;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static google.registry.util.CollectionUtils.isNullOrEmpty;
|
||||
import static google.registry.util.CollectionUtils.nullToEmpty;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.model.Buildable;
|
||||
import google.registry.model.billing.BillingCancellation;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.billing.BillingRecurrence;
|
||||
import google.registry.model.domain.Period;
|
||||
import google.registry.model.domain.Period.Unit;
|
||||
import google.registry.model.eppcommon.Trid;
|
||||
import google.registry.model.poll.PollMessage;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.util.NullIgnoringCollectionBuilder;
|
||||
@@ -36,9 +42,41 @@ import org.joda.time.DateTime;
|
||||
|
||||
/** Transfer data for domain. */
|
||||
@Embeddable
|
||||
public class DomainTransferData extends TransferData {
|
||||
public class DomainTransferData extends BaseTransferObject implements Buildable {
|
||||
public static final DomainTransferData EMPTY = new DomainTransferData();
|
||||
|
||||
/** The transaction id of the most recent transfer request (or null if there never was one). */
|
||||
@Embedded
|
||||
@AttributeOverrides({
|
||||
@AttributeOverride(
|
||||
name = "serverTransactionId",
|
||||
column = @Column(name = "transfer_server_txn_id")),
|
||||
@AttributeOverride(
|
||||
name = "clientTransactionId",
|
||||
column = @Column(name = "transfer_client_txn_id"))
|
||||
})
|
||||
Trid transferRequestTrid;
|
||||
|
||||
@Column(name = "transfer_repo_id")
|
||||
String repoId;
|
||||
|
||||
@Column(name = "transfer_history_entry_id")
|
||||
Long historyEntryId;
|
||||
|
||||
// The pollMessageId1 and pollMessageId2 are used to store the IDs for gaining and losing poll
|
||||
// messages in Cloud SQL.
|
||||
//
|
||||
// In addition, there may be a third poll message for the autorenew poll message on domain
|
||||
// transfer if applicable.
|
||||
@Column(name = "transfer_poll_message_id_1")
|
||||
Long pollMessageId1;
|
||||
|
||||
@Column(name = "transfer_poll_message_id_2")
|
||||
Long pollMessageId2;
|
||||
|
||||
@Column(name = "transfer_poll_message_id_3")
|
||||
Long pollMessageId3;
|
||||
|
||||
/**
|
||||
* The period to extend the registration upon completion of the transfer.
|
||||
*
|
||||
@@ -107,15 +145,19 @@ public class DomainTransferData extends TransferData {
|
||||
@Column(name = "transfer_autorenew_poll_message_history_id")
|
||||
Long serverApproveAutorenewPollMessageHistoryId;
|
||||
|
||||
@Override
|
||||
public Builder copyConstantFieldsToBuilder() {
|
||||
return ((Builder) super.copyConstantFieldsToBuilder()).setTransferPeriod(transferPeriod);
|
||||
}
|
||||
|
||||
public Period getTransferPeriod() {
|
||||
return transferPeriod;
|
||||
}
|
||||
|
||||
public Long getHistoryEntryId() {
|
||||
return historyEntryId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Trid getTransferRequestTrid() {
|
||||
return transferRequestTrid;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public DateTime getTransferredRegistrationExpirationTime() {
|
||||
return transferredRegistrationExpirationTime;
|
||||
@@ -141,12 +183,13 @@ public class DomainTransferData extends TransferData {
|
||||
return serverApproveAutorenewPollMessageHistoryId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableSet<VKey<? extends TransferServerApproveEntity>> getServerApproveEntities() {
|
||||
ImmutableSet.Builder<VKey<? extends TransferServerApproveEntity>> builder =
|
||||
new ImmutableSet.Builder<>();
|
||||
builder.addAll(super.getServerApproveEntities());
|
||||
return NullIgnoringCollectionBuilder.create(builder)
|
||||
.add(pollMessageId1 != null ? VKey.create(PollMessage.class, pollMessageId1) : null)
|
||||
.add(pollMessageId2 != null ? VKey.create(PollMessage.class, pollMessageId2) : null)
|
||||
.add(pollMessageId3 != null ? VKey.create(PollMessage.class, pollMessageId3) : null)
|
||||
.add(serverApproveBillingEvent)
|
||||
.add(serverApproveAutorenewEvent)
|
||||
.add(billingCancellationId)
|
||||
@@ -154,16 +197,10 @@ public class DomainTransferData extends TransferData {
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return EMPTY.equals(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Builder createEmptyBuilder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder asBuilder() {
|
||||
return new Builder(clone(this));
|
||||
@@ -186,7 +223,72 @@ public class DomainTransferData extends TransferData {
|
||||
}
|
||||
}
|
||||
|
||||
public static class Builder extends TransferData.Builder<DomainTransferData, Builder> {
|
||||
/**
|
||||
* Returns a fresh Builder populated only with the constant fields of this TransferData, i.e.
|
||||
* those that are fixed and unchanging throughout the transfer process.
|
||||
*
|
||||
* <p>These fields are:
|
||||
*
|
||||
* <ul>
|
||||
* <li>transferRequestTrid
|
||||
* <li>transferRequestTime
|
||||
* <li>gainingClientId
|
||||
* <li>losingClientId
|
||||
* <li>transferPeriod
|
||||
* </ul>
|
||||
*/
|
||||
public Builder copyConstantFieldsToBuilder() {
|
||||
return new Builder()
|
||||
.setTransferPeriod(transferPeriod)
|
||||
.setTransferRequestTrid(transferRequestTrid)
|
||||
.setTransferRequestTime(transferRequestTime)
|
||||
.setGainingRegistrarId(gainingClientId)
|
||||
.setLosingRegistrarId(losingClientId);
|
||||
}
|
||||
|
||||
/** Maps serverApproveEntities set to the individual fields. */
|
||||
static void mapServerApproveEntitiesToFields(
|
||||
Set<VKey<? extends TransferServerApproveEntity>> serverApproveEntities,
|
||||
DomainTransferData transferData) {
|
||||
if (isNullOrEmpty(serverApproveEntities)) {
|
||||
transferData.pollMessageId1 = null;
|
||||
transferData.pollMessageId2 = null;
|
||||
transferData.pollMessageId3 = null;
|
||||
return;
|
||||
}
|
||||
ImmutableList<Long> sortedPollMessageIds = getSortedPollMessageIds(serverApproveEntities);
|
||||
if (!sortedPollMessageIds.isEmpty()) {
|
||||
transferData.pollMessageId1 = sortedPollMessageIds.get(0);
|
||||
}
|
||||
if (sortedPollMessageIds.size() >= 2) {
|
||||
transferData.pollMessageId2 = sortedPollMessageIds.get(1);
|
||||
}
|
||||
if (sortedPollMessageIds.size() >= 3) {
|
||||
transferData.pollMessageId3 = sortedPollMessageIds.get(2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets poll message IDs from the given serverApproveEntities and sorts the IDs in natural order.
|
||||
*/
|
||||
private static ImmutableList<Long> getSortedPollMessageIds(
|
||||
Set<VKey<? extends TransferServerApproveEntity>> serverApproveEntities) {
|
||||
return nullToEmpty(serverApproveEntities).stream()
|
||||
.filter(vKey -> PollMessage.class.isAssignableFrom(vKey.getKind()))
|
||||
.map(vKey -> (long) vKey.getKey())
|
||||
.sorted()
|
||||
.collect(toImmutableList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Marker interface for objects that are written in anticipation of a server approval, and
|
||||
* therefore need to be deleted under any other outcome.
|
||||
*/
|
||||
public interface TransferServerApproveEntity {
|
||||
VKey<? extends TransferServerApproveEntity> createVKey();
|
||||
}
|
||||
|
||||
public static class Builder extends BaseTransferObject.Builder<DomainTransferData, Builder> {
|
||||
/** Create a {@link Builder} wrapping a new instance. */
|
||||
public Builder() {}
|
||||
|
||||
@@ -195,6 +297,20 @@ public class DomainTransferData extends TransferData {
|
||||
super(instance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainTransferData build() {
|
||||
if (getInstance().pollMessageId1 != null) {
|
||||
checkState(getInstance().repoId != null, "Repo id undefined");
|
||||
checkState(getInstance().historyEntryId != null, "History entry undefined");
|
||||
}
|
||||
return super.build();
|
||||
}
|
||||
|
||||
public Builder setTransferRequestTrid(Trid transferRequestTrid) {
|
||||
getInstance().transferRequestTrid = transferRequestTrid;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setTransferPeriod(Period transferPeriod) {
|
||||
getInstance().transferPeriod = transferPeriod;
|
||||
return this;
|
||||
@@ -223,12 +339,13 @@ public class DomainTransferData extends TransferData {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder setServerApproveEntities(
|
||||
String repoId,
|
||||
Long historyId,
|
||||
ImmutableSet<VKey<? extends TransferServerApproveEntity>> serverApproveEntities) {
|
||||
super.setServerApproveEntities(repoId, historyId, serverApproveEntities);
|
||||
getInstance().repoId = repoId;
|
||||
getInstance().historyEntryId = historyId;
|
||||
mapServerApproveEntitiesToFields(serverApproveEntities, getInstance());
|
||||
mapBillingCancellationEntityToField(serverApproveEntities, getInstance());
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
// Copyright 2017 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.transfer;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static google.registry.util.CollectionUtils.isNullOrEmpty;
|
||||
import static google.registry.util.CollectionUtils.nullToEmpty;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.model.Buildable;
|
||||
import google.registry.model.EppResource;
|
||||
import google.registry.model.eppcommon.Trid;
|
||||
import google.registry.model.poll.PollMessage;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.util.NullIgnoringCollectionBuilder;
|
||||
import jakarta.persistence.AttributeOverride;
|
||||
import jakarta.persistence.AttributeOverrides;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Embedded;
|
||||
import jakarta.persistence.MappedSuperclass;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Common transfer data for {@link EppResource} types. Only applies to domains and contacts; hosts
|
||||
* are implicitly transferred with their superordinate domain.
|
||||
*/
|
||||
@MappedSuperclass
|
||||
public abstract class TransferData extends BaseTransferObject implements Buildable {
|
||||
|
||||
/** The transaction id of the most recent transfer request (or null if there never was one). */
|
||||
@Embedded
|
||||
@AttributeOverrides({
|
||||
@AttributeOverride(
|
||||
name = "serverTransactionId",
|
||||
column = @Column(name = "transfer_server_txn_id")),
|
||||
@AttributeOverride(
|
||||
name = "clientTransactionId",
|
||||
column = @Column(name = "transfer_client_txn_id"))
|
||||
})
|
||||
Trid transferRequestTrid;
|
||||
|
||||
@Column(name = "transfer_repo_id")
|
||||
String repoId;
|
||||
|
||||
@Column(name = "transfer_history_entry_id")
|
||||
Long historyEntryId;
|
||||
|
||||
// The pollMessageId1 and pollMessageId2 are used to store the IDs for gaining and losing poll
|
||||
// messages in Cloud SQL.
|
||||
//
|
||||
// In addition, there may be a third poll message for the autorenew poll message on domain
|
||||
// transfer if applicable.
|
||||
@Column(name = "transfer_poll_message_id_1")
|
||||
Long pollMessageId1;
|
||||
|
||||
@Column(name = "transfer_poll_message_id_2")
|
||||
Long pollMessageId2;
|
||||
|
||||
@Column(name = "transfer_poll_message_id_3")
|
||||
Long pollMessageId3;
|
||||
|
||||
public abstract boolean isEmpty();
|
||||
|
||||
public Long getHistoryEntryId() {
|
||||
return historyEntryId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Trid getTransferRequestTrid() {
|
||||
return transferRequestTrid;
|
||||
}
|
||||
|
||||
public ImmutableSet<VKey<? extends TransferServerApproveEntity>> getServerApproveEntities() {
|
||||
return NullIgnoringCollectionBuilder.create(
|
||||
new ImmutableSet.Builder<VKey<? extends TransferServerApproveEntity>>())
|
||||
.add(pollMessageId1 != null ? VKey.create(PollMessage.class, pollMessageId1) : null)
|
||||
.add(pollMessageId2 != null ? VKey.create(PollMessage.class, pollMessageId2) : null)
|
||||
.add(pollMessageId3 != null ? VKey.create(PollMessage.class, pollMessageId3) : null)
|
||||
.getBuilder()
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract Builder<?, ?> asBuilder();
|
||||
|
||||
protected abstract Builder<?, ?> createEmptyBuilder();
|
||||
|
||||
/**
|
||||
* Returns a fresh Builder populated only with the constant fields of this TransferData, i.e.
|
||||
* those that are fixed and unchanging throughout the transfer process.
|
||||
*
|
||||
* <p>These fields are:
|
||||
*
|
||||
* <ul>
|
||||
* <li>transferRequestTrid
|
||||
* <li>transferRequestTime
|
||||
* <li>gainingClientId
|
||||
* <li>losingClientId
|
||||
* <li>transferPeriod
|
||||
* </ul>
|
||||
*/
|
||||
public Builder<?, ?> copyConstantFieldsToBuilder() {
|
||||
Builder<?, ?> newBuilder = createEmptyBuilder();
|
||||
newBuilder
|
||||
.setTransferRequestTrid(transferRequestTrid)
|
||||
.setTransferRequestTime(transferRequestTime)
|
||||
.setGainingRegistrarId(gainingClientId)
|
||||
.setLosingRegistrarId(losingClientId);
|
||||
return newBuilder;
|
||||
}
|
||||
|
||||
/** Maps serverApproveEntities set to the individual fields. */
|
||||
static void mapServerApproveEntitiesToFields(
|
||||
Set<VKey<? extends TransferServerApproveEntity>> serverApproveEntities,
|
||||
TransferData transferData) {
|
||||
if (isNullOrEmpty(serverApproveEntities)) {
|
||||
transferData.pollMessageId1 = null;
|
||||
transferData.pollMessageId2 = null;
|
||||
transferData.pollMessageId3 = null;
|
||||
return;
|
||||
}
|
||||
ImmutableList<Long> sortedPollMessageIds = getSortedPollMessageIds(serverApproveEntities);
|
||||
if (sortedPollMessageIds.size() >= 1) {
|
||||
transferData.pollMessageId1 = sortedPollMessageIds.get(0);
|
||||
}
|
||||
if (sortedPollMessageIds.size() >= 2) {
|
||||
transferData.pollMessageId2 = sortedPollMessageIds.get(1);
|
||||
}
|
||||
if (sortedPollMessageIds.size() >= 3) {
|
||||
transferData.pollMessageId3 = sortedPollMessageIds.get(2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets poll message IDs from the given serverApproveEntities and sorted the IDs in natural order.
|
||||
*/
|
||||
private static ImmutableList<Long> getSortedPollMessageIds(
|
||||
Set<VKey<? extends TransferServerApproveEntity>> serverApproveEntities) {
|
||||
return nullToEmpty(serverApproveEntities).stream()
|
||||
.filter(vKey -> PollMessage.class.isAssignableFrom(vKey.getKind()))
|
||||
.map(vKey -> (long) vKey.getKey())
|
||||
.sorted()
|
||||
.collect(toImmutableList());
|
||||
}
|
||||
|
||||
/** Builder for {@link TransferData} because it is immutable. */
|
||||
public abstract static class Builder<T extends TransferData, B extends Builder<T, B>>
|
||||
extends BaseTransferObject.Builder<T, B> {
|
||||
|
||||
/** Create a {@link Builder} wrapping a new instance. */
|
||||
protected Builder() {}
|
||||
|
||||
/** Create a {@link Builder} wrapping the given instance. */
|
||||
protected Builder(T instance) {
|
||||
super(instance);
|
||||
}
|
||||
|
||||
public B setTransferRequestTrid(Trid transferRequestTrid) {
|
||||
getInstance().transferRequestTrid = transferRequestTrid;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B setServerApproveEntities(
|
||||
String repoId,
|
||||
Long historyId,
|
||||
ImmutableSet<VKey<? extends TransferServerApproveEntity>> serverApproveEntities) {
|
||||
getInstance().repoId = repoId;
|
||||
getInstance().historyEntryId = historyId;
|
||||
mapServerApproveEntitiesToFields(serverApproveEntities, getInstance());
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
@Override
|
||||
public T build() {
|
||||
if (getInstance().pollMessageId1 != null) {
|
||||
checkState(getInstance().repoId != null, "Repo id undefined");
|
||||
checkState(getInstance().historyEntryId != null, "History entry undefined");
|
||||
}
|
||||
return super.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marker interface for objects that are written in anticipation of a server approval, and
|
||||
* therefore need to be deleted under any other outcome.
|
||||
*/
|
||||
public interface TransferServerApproveEntity {
|
||||
VKey<? extends TransferServerApproveEntity> createVKey();
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ import google.registry.model.domain.secdns.DomainDsData;
|
||||
import google.registry.model.eppcommon.StatusValue;
|
||||
import google.registry.model.rde.RdeMode;
|
||||
import google.registry.model.transfer.DomainTransferData;
|
||||
import google.registry.model.transfer.TransferData;
|
||||
import google.registry.util.Idn;
|
||||
import google.registry.xjc.domain.XjcDomainNsType;
|
||||
import google.registry.xjc.domain.XjcDomainStatusType;
|
||||
@@ -221,7 +220,7 @@ final class DomainToXjcConverter {
|
||||
&& !Strings.isNullOrEmpty(model.getTransferData().getLosingRegistrarId());
|
||||
}
|
||||
|
||||
/** Converts {@link TransferData} to {@link XjcRdeDomainTransferDataType}. */
|
||||
/** Converts {@link DomainTransferData} to {@link XjcRdeDomainTransferDataType}. */
|
||||
private static XjcRdeDomainTransferDataType convertTransferData(DomainTransferData model) {
|
||||
XjcRdeDomainTransferDataType bean = new XjcRdeDomainTransferDataType();
|
||||
bean.setTrStatus(
|
||||
|
||||
@@ -65,7 +65,7 @@ public class BulkDomainTransferCommand extends ConfirmingCommand implements Comm
|
||||
|
||||
@Parameter(
|
||||
names = {"-d", "--domain_names_file"},
|
||||
description = "A file with a list of newline-delimited domain names to create tokens for")
|
||||
description = "A file with a list of newline-delimited domain names to transfer")
|
||||
private String domainNamesFile;
|
||||
|
||||
@Parameter(
|
||||
@@ -82,7 +82,7 @@ public class BulkDomainTransferCommand extends ConfirmingCommand implements Comm
|
||||
|
||||
@Parameter(
|
||||
names = {"--reason"},
|
||||
description = "Reason to transfer the domains",
|
||||
description = "Reason to transfer the domains, possibly a bug number",
|
||||
required = true)
|
||||
private String reason;
|
||||
|
||||
|
||||
@@ -61,10 +61,6 @@ public class PasswordResetRequestAction extends ConsoleApiAction {
|
||||
|
||||
@Override
|
||||
protected void postHandler(User user) {
|
||||
// Temporary flag when testing email sending etc
|
||||
if (!user.getUserRoles().isAdmin()) {
|
||||
setFailedResponse("", HttpServletResponse.SC_FORBIDDEN);
|
||||
}
|
||||
tm().transact(() -> performRequest(user));
|
||||
consoleApiParams.response().setStatus(HttpServletResponse.SC_OK);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import static google.registry.ui.server.console.PasswordResetRequestAction.check
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import google.registry.model.console.ConsolePermission;
|
||||
import google.registry.model.console.ConsoleUpdateHistory;
|
||||
import google.registry.model.console.PasswordResetRequest;
|
||||
import google.registry.model.console.User;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
@@ -59,11 +60,6 @@ public class PasswordResetVerifyAction extends ConsoleApiAction {
|
||||
|
||||
@Override
|
||||
protected void getHandler(User user) {
|
||||
// Temporary flag when testing email sending etc
|
||||
if (!user.getUserRoles().isAdmin()) {
|
||||
setFailedResponse("", HttpServletResponse.SC_FORBIDDEN);
|
||||
return;
|
||||
}
|
||||
PasswordResetRequest request = tm().transact(() -> loadAndValidateResetRequest(user));
|
||||
ImmutableMap<String, ?> result =
|
||||
ImmutableMap.of("type", request.getType(), "registrarId", request.getRegistrarId());
|
||||
@@ -73,11 +69,6 @@ public class PasswordResetVerifyAction extends ConsoleApiAction {
|
||||
|
||||
@Override
|
||||
protected void postHandler(User user) {
|
||||
// Temporary flag when testing email sending etc
|
||||
if (!user.getUserRoles().isAdmin()) {
|
||||
setFailedResponse("", HttpServletResponse.SC_FORBIDDEN);
|
||||
return;
|
||||
}
|
||||
checkArgument(!Strings.isNullOrEmpty(newPassword.orElse(null)), "Password must be provided");
|
||||
tm().transact(
|
||||
() -> {
|
||||
@@ -87,6 +78,16 @@ public class PasswordResetVerifyAction extends ConsoleApiAction {
|
||||
case REGISTRY_LOCK -> handleRegistryLockPasswordReset(request);
|
||||
}
|
||||
tm().put(request.asBuilder().setFulfillmentTime(tm().getTransactionTime()).build());
|
||||
|
||||
finishAndPersistConsoleUpdateHistory(
|
||||
new ConsoleUpdateHistory.Builder()
|
||||
.setType(ConsoleUpdateHistory.Type.EPP_PASSWORD_UPDATE)
|
||||
.setDescription(
|
||||
String.format(
|
||||
"%s%s%s",
|
||||
request.getRegistrarId(),
|
||||
ConsoleUpdateHistory.DESCRIPTION_SEPARATOR,
|
||||
"Password reset fulfilled via verification code")));
|
||||
});
|
||||
consoleApiParams.response().setStatus(HttpServletResponse.SC_OK);
|
||||
}
|
||||
@@ -110,6 +111,11 @@ public class PasswordResetVerifyAction extends ConsoleApiAction {
|
||||
PasswordResetRequest request =
|
||||
tm().loadByKeyIfPresent(VKey.create(PasswordResetRequest.class, verificationCode))
|
||||
.orElseThrow(this::createVerificationCodeException);
|
||||
|
||||
if (request.getFulfillmentTime().isPresent()) {
|
||||
throw new IllegalArgumentException("This reset request has already been used.");
|
||||
}
|
||||
|
||||
ConsolePermission requiredVerifyPermission =
|
||||
switch (request.getType()) {
|
||||
case EPP -> ConsolePermission.MANAGE_USERS;
|
||||
|
||||
@@ -235,10 +235,7 @@ public class RdePipelineTest {
|
||||
persistHostHistory(host1);
|
||||
Domain helloDomain =
|
||||
persistEppResource(
|
||||
newDomain("hello.soy")
|
||||
.asBuilder()
|
||||
.addNameserver(host1.createVKey())
|
||||
.build());
|
||||
newDomain("hello.soy").asBuilder().addNameserver(host1.createVKey()).build());
|
||||
persistDomainHistory(helloDomain);
|
||||
persistHostHistory(persistActiveHost("not-used-subordinate.hello.soy"));
|
||||
Host host2 = persistActiveHost("ns1.hello.soy");
|
||||
|
||||
@@ -41,7 +41,7 @@ import google.registry.model.eppcommon.StatusValue;
|
||||
import google.registry.model.host.Host;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.model.tld.Tld;
|
||||
import google.registry.model.transfer.TransferData;
|
||||
import google.registry.model.transfer.DomainTransferData;
|
||||
import google.registry.model.transfer.TransferStatus;
|
||||
import google.registry.persistence.transaction.JpaTransactionManagerExtension;
|
||||
import org.joda.time.DateTime;
|
||||
@@ -161,7 +161,8 @@ abstract class DomainTransferFlowTestCase<F extends Flow, R extends EppResource>
|
||||
.build();
|
||||
}
|
||||
|
||||
void assertTransferFailed(Domain domain, TransferStatus status, TransferData oldTransferData) {
|
||||
void assertTransferFailed(
|
||||
Domain domain, TransferStatus status, DomainTransferData oldTransferData) {
|
||||
assertAboutDomains()
|
||||
.that(domain)
|
||||
.doesNotHaveStatusValue(StatusValue.PENDING_TRANSFER)
|
||||
|
||||
@@ -54,7 +54,7 @@ import google.registry.model.poll.PollMessage;
|
||||
import google.registry.model.reporting.DomainTransactionRecord;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.model.tld.Tld;
|
||||
import google.registry.model.transfer.TransferData;
|
||||
import google.registry.model.transfer.DomainTransferData;
|
||||
import google.registry.model.transfer.TransferResponse;
|
||||
import google.registry.model.transfer.TransferStatus;
|
||||
import org.joda.time.DateTime;
|
||||
@@ -90,7 +90,7 @@ class DomainTransferRejectFlowTest
|
||||
assertMutatingFlow(true);
|
||||
DateTime originalExpirationTime = domain.getRegistrationExpirationTime();
|
||||
ImmutableSet<GracePeriod> originalGracePeriods = domain.getGracePeriods();
|
||||
TransferData originalTransferData = domain.getTransferData();
|
||||
DomainTransferData originalTransferData = domain.getTransferData();
|
||||
runFlowAssertResponse(loadFile(expectedXmlFilename));
|
||||
// Transfer should have been rejected. Verify correct fields were set.
|
||||
domain = reloadResourceByForeignKey();
|
||||
|
||||
@@ -29,8 +29,8 @@ import org.joda.time.DateTime;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link TransferData}. */
|
||||
public class TransferDataTest {
|
||||
/** Unit tests for {@link DomainTransferData}. */
|
||||
public class DomainTransferDataTest {
|
||||
|
||||
private final DateTime now = DateTime.now(UTC);
|
||||
|
||||
@@ -34,7 +34,6 @@ import java.util.Optional;
|
||||
import javax.annotation.Nullable;
|
||||
import org.joda.time.Duration;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Tests for {@link PasswordResetVerifyAction}. */
|
||||
@@ -111,28 +110,24 @@ public class PasswordResetVerifyActionTest extends ConsoleActionBaseTestCase {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled("Enable when testing is done in sandbox and isAdmin check is removed")
|
||||
void testFailure_get_epp_badPermission() throws Exception {
|
||||
createAction(createTechUser(), "GET", verificationCode, null).run();
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled("Enable when testing is done in sandbox and isAdmin check is removed")
|
||||
void testFailure_get_lock_badPermission() throws Exception {
|
||||
createAction(createAccountManager(), "GET", verificationCode, null).run();
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled("Enable when testing is done in sandbox and isAdmin check is removed")
|
||||
void testFailure_post_epp_badPermission() throws Exception {
|
||||
createAction(createTechUser(), "POST", verificationCode, "newPassword").run();
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled("Enable when testing is done in sandbox and isAdmin check is removed")
|
||||
void testFailure_post_lock_badPermission() throws Exception {
|
||||
createAction(createAccountManager(), "POST", verificationCode, "newPassword").run();
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN);
|
||||
|
||||
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
@@ -218,3 +218,5 @@ V217__drop_contact_fks_pollmessage.sql
|
||||
V218__tld_drop_allowedregistrantcontactids.sql
|
||||
V219__domain_history_package_token_idx.sql
|
||||
V220__domain_package_token_idx.sql
|
||||
V221__remove_contact_history.sql
|
||||
V222__remove_contact.sql
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
-- Copyright 2026 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.
|
||||
|
||||
DROP TABLE IF EXISTS "ContactHistory";
|
||||
15
db/src/main/resources/sql/flyway/V222__remove_contact.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
-- Copyright 2026 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.
|
||||
|
||||
DROP TABLE IF EXISTS "Contact";
|
||||
@@ -206,20 +206,20 @@
|
||||
tech_contact text,
|
||||
tld text,
|
||||
transfer_billing_cancellation_id bigint,
|
||||
transfer_history_entry_id bigint,
|
||||
transfer_poll_message_id_1 bigint,
|
||||
transfer_poll_message_id_2 bigint,
|
||||
transfer_poll_message_id_3 bigint,
|
||||
transfer_repo_id text,
|
||||
transfer_billing_recurrence_id bigint,
|
||||
transfer_autorenew_poll_message_id bigint,
|
||||
transfer_autorenew_poll_message_history_id bigint,
|
||||
transfer_billing_event_id bigint,
|
||||
transfer_renew_period_unit text check (transfer_renew_period_unit in ('YEARS','MONTHS')),
|
||||
transfer_renew_period_value integer,
|
||||
transfer_registration_expiration_time timestamp(6) with time zone,
|
||||
transfer_history_entry_id bigint,
|
||||
transfer_poll_message_id_1 bigint,
|
||||
transfer_poll_message_id_2 bigint,
|
||||
transfer_poll_message_id_3 bigint,
|
||||
transfer_repo_id text,
|
||||
transfer_client_txn_id text,
|
||||
transfer_server_txn_id text,
|
||||
transfer_registration_expiration_time timestamp(6) with time zone,
|
||||
transfer_gaining_registrar_id text,
|
||||
transfer_losing_registrar_id text,
|
||||
transfer_pending_expiration_time timestamp(6) with time zone,
|
||||
@@ -278,20 +278,20 @@
|
||||
tech_contact text,
|
||||
tld text,
|
||||
transfer_billing_cancellation_id bigint,
|
||||
transfer_history_entry_id bigint,
|
||||
transfer_poll_message_id_1 bigint,
|
||||
transfer_poll_message_id_2 bigint,
|
||||
transfer_poll_message_id_3 bigint,
|
||||
transfer_repo_id text,
|
||||
transfer_billing_recurrence_id bigint,
|
||||
transfer_autorenew_poll_message_id bigint,
|
||||
transfer_autorenew_poll_message_history_id bigint,
|
||||
transfer_billing_event_id bigint,
|
||||
transfer_renew_period_unit text check (transfer_renew_period_unit in ('YEARS','MONTHS')),
|
||||
transfer_renew_period_value integer,
|
||||
transfer_registration_expiration_time timestamp(6) with time zone,
|
||||
transfer_history_entry_id bigint,
|
||||
transfer_poll_message_id_1 bigint,
|
||||
transfer_poll_message_id_2 bigint,
|
||||
transfer_poll_message_id_3 bigint,
|
||||
transfer_repo_id text,
|
||||
transfer_client_txn_id text,
|
||||
transfer_server_txn_id text,
|
||||
transfer_registration_expiration_time timestamp(6) with time zone,
|
||||
transfer_gaining_registrar_id text,
|
||||
transfer_losing_registrar_id text,
|
||||
transfer_pending_expiration_time timestamp(6) with time zone,
|
||||
|
||||
@@ -287,150 +287,6 @@ CREATE TABLE public."ConsoleUpdateHistory" (
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- Name: Contact; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE TABLE public."Contact" (
|
||||
repo_id text NOT NULL,
|
||||
creation_registrar_id text NOT NULL,
|
||||
creation_time timestamp with time zone NOT NULL,
|
||||
current_sponsor_registrar_id text NOT NULL,
|
||||
deletion_time timestamp with time zone,
|
||||
last_epp_update_registrar_id text,
|
||||
last_epp_update_time timestamp with time zone,
|
||||
statuses text[],
|
||||
auth_info_repo_id text,
|
||||
auth_info_value text,
|
||||
contact_id text,
|
||||
disclose_types_addr text[],
|
||||
disclose_show_email boolean,
|
||||
disclose_show_fax boolean,
|
||||
disclose_mode_flag boolean,
|
||||
disclose_types_name text[],
|
||||
disclose_types_org text[],
|
||||
disclose_show_voice boolean,
|
||||
email text,
|
||||
fax_phone_extension text,
|
||||
fax_phone_number text,
|
||||
addr_i18n_city text,
|
||||
addr_i18n_country_code text,
|
||||
addr_i18n_state text,
|
||||
addr_i18n_street_line1 text,
|
||||
addr_i18n_street_line2 text,
|
||||
addr_i18n_street_line3 text,
|
||||
addr_i18n_zip text,
|
||||
addr_i18n_name text,
|
||||
addr_i18n_org text,
|
||||
addr_i18n_type text,
|
||||
last_transfer_time timestamp with time zone,
|
||||
addr_local_city text,
|
||||
addr_local_country_code text,
|
||||
addr_local_state text,
|
||||
addr_local_street_line1 text,
|
||||
addr_local_street_line2 text,
|
||||
addr_local_street_line3 text,
|
||||
addr_local_zip text,
|
||||
addr_local_name text,
|
||||
addr_local_org text,
|
||||
addr_local_type text,
|
||||
search_name text,
|
||||
voice_phone_extension text,
|
||||
voice_phone_number text,
|
||||
transfer_poll_message_id_1 bigint,
|
||||
transfer_poll_message_id_2 bigint,
|
||||
transfer_client_txn_id text,
|
||||
transfer_server_txn_id text,
|
||||
transfer_gaining_registrar_id text,
|
||||
transfer_losing_registrar_id text,
|
||||
transfer_pending_expiration_time timestamp with time zone,
|
||||
transfer_request_time timestamp with time zone,
|
||||
transfer_status text,
|
||||
update_timestamp timestamp with time zone,
|
||||
transfer_history_entry_id bigint,
|
||||
transfer_repo_id text,
|
||||
transfer_poll_message_id_3 bigint,
|
||||
last_update_time_via_epp timestamp with time zone
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- Name: ContactHistory; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE TABLE public."ContactHistory" (
|
||||
history_revision_id bigint NOT NULL,
|
||||
history_by_superuser boolean NOT NULL,
|
||||
history_registrar_id text,
|
||||
history_modification_time timestamp with time zone NOT NULL,
|
||||
history_reason text,
|
||||
history_requested_by_registrar boolean,
|
||||
history_client_transaction_id text,
|
||||
history_server_transaction_id text,
|
||||
history_type text NOT NULL,
|
||||
history_xml_bytes bytea,
|
||||
auth_info_repo_id text,
|
||||
auth_info_value text,
|
||||
contact_id text,
|
||||
disclose_types_addr text[],
|
||||
disclose_show_email boolean,
|
||||
disclose_show_fax boolean,
|
||||
disclose_mode_flag boolean,
|
||||
disclose_types_name text[],
|
||||
disclose_types_org text[],
|
||||
disclose_show_voice boolean,
|
||||
email text,
|
||||
fax_phone_extension text,
|
||||
fax_phone_number text,
|
||||
addr_i18n_city text,
|
||||
addr_i18n_country_code text,
|
||||
addr_i18n_state text,
|
||||
addr_i18n_street_line1 text,
|
||||
addr_i18n_street_line2 text,
|
||||
addr_i18n_street_line3 text,
|
||||
addr_i18n_zip text,
|
||||
addr_i18n_name text,
|
||||
addr_i18n_org text,
|
||||
addr_i18n_type text,
|
||||
last_transfer_time timestamp with time zone,
|
||||
addr_local_city text,
|
||||
addr_local_country_code text,
|
||||
addr_local_state text,
|
||||
addr_local_street_line1 text,
|
||||
addr_local_street_line2 text,
|
||||
addr_local_street_line3 text,
|
||||
addr_local_zip text,
|
||||
addr_local_name text,
|
||||
addr_local_org text,
|
||||
addr_local_type text,
|
||||
search_name text,
|
||||
transfer_poll_message_id_1 bigint,
|
||||
transfer_poll_message_id_2 bigint,
|
||||
transfer_client_txn_id text,
|
||||
transfer_server_txn_id text,
|
||||
transfer_gaining_registrar_id text,
|
||||
transfer_losing_registrar_id text,
|
||||
transfer_pending_expiration_time timestamp with time zone,
|
||||
transfer_request_time timestamp with time zone,
|
||||
transfer_status text,
|
||||
voice_phone_extension text,
|
||||
voice_phone_number text,
|
||||
creation_registrar_id text,
|
||||
creation_time timestamp with time zone,
|
||||
current_sponsor_registrar_id text,
|
||||
deletion_time timestamp with time zone,
|
||||
last_epp_update_registrar_id text,
|
||||
last_epp_update_time timestamp with time zone,
|
||||
statuses text[],
|
||||
contact_repo_id text NOT NULL,
|
||||
update_timestamp timestamp with time zone,
|
||||
transfer_history_entry_id bigint,
|
||||
transfer_repo_id text,
|
||||
transfer_poll_message_id_3 bigint,
|
||||
last_update_time_via_epp timestamp with time zone
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- Name: Cursor; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
@@ -1596,22 +1452,6 @@ ALTER TABLE ONLY public."ConsoleUpdateHistory"
|
||||
ADD CONSTRAINT "ConsoleUpdateHistory_pkey" PRIMARY KEY (revision_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: ContactHistory ContactHistory_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public."ContactHistory"
|
||||
ADD CONSTRAINT "ContactHistory_pkey" PRIMARY KEY (contact_repo_id, history_revision_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: Contact Contact_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public."Contact"
|
||||
ADD CONSTRAINT "Contact_pkey" PRIMARY KEY (repo_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: Cursor Cursor_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
@@ -2112,13 +1952,6 @@ CREATE INDEX idx1dyqmqb61xbnj7mt7bk27ds25 ON public."DomainTransactionRecord" US
|
||||
CREATE INDEX idx1iy7njgb7wjmj9piml4l2g0qi ON public."HostHistory" USING btree (history_registrar_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: idx1p3esngcwwu6hstyua6itn6ff; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX idx1p3esngcwwu6hstyua6itn6ff ON public."Contact" USING btree (search_name);
|
||||
|
||||
|
||||
--
|
||||
-- Name: idx1rcgkdd777bpvj0r94sltwd5y; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@@ -2147,13 +1980,6 @@ CREATE INDEX idx3d1mucv7axrhud8w8jl4vsu62 ON public."RegistrarUpdateHistory" USI
|
||||
CREATE INDEX idx3y3k7m2bkgahm9sixiohgyrga ON public."Domain" USING btree (transfer_billing_event_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: idx3y752kr9uh4kh6uig54vemx0l; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX idx3y752kr9uh4kh6uig54vemx0l ON public."Contact" USING btree (creation_time);
|
||||
|
||||
|
||||
--
|
||||
-- Name: idx4ytbe5f3b39trsd4okx5ijhs4; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@@ -2280,13 +2106,6 @@ CREATE INDEX idx8nr0ke9mrrx4ewj6pd2ag4rmr ON public."Domain" USING btree (creati
|
||||
CREATE INDEX idx9g3s7mjv1yn4t06nqid39whss ON public."AllocationToken" USING btree (token_type);
|
||||
|
||||
|
||||
--
|
||||
-- Name: idx9q53px6r302ftgisqifmc6put; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX idx9q53px6r302ftgisqifmc6put ON public."ContactHistory" USING btree (history_type);
|
||||
|
||||
|
||||
--
|
||||
-- Name: idx_console_update_history_acting_user; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@@ -2364,13 +2183,6 @@ CREATE INDEX idxbgssjudpm428mrv0xfpvgifps ON public."GracePeriod" USING btree (b
|
||||
CREATE INDEX idxbjacjlm8ianc4kxxvamnu94k5 ON public."UserUpdateHistory" USING btree (history_acting_user);
|
||||
|
||||
|
||||
--
|
||||
-- Name: idxbn8t4wp85fgxjl8q4ctlscx55; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX idxbn8t4wp85fgxjl8q4ctlscx55 ON public."Contact" USING btree (current_sponsor_registrar_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: idxcclyb3n5gbex8u8m9fjlujitw; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@@ -2455,13 +2267,6 @@ CREATE INDEX idxhlqqd5uy98cjyos72d81x9j95 ON public."DelegationSignerData" USING
|
||||
CREATE INDEX idxhmv411mdqo5ibn4vy7ykxpmlv ON public."BillingEvent" USING btree (allocation_token);
|
||||
|
||||
|
||||
--
|
||||
-- Name: idxhp33wybmb6tbpr1bq7ttwk8je; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX idxhp33wybmb6tbpr1bq7ttwk8je ON public."ContactHistory" USING btree (history_registrar_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: idxhteajcrxmq4o8rsys8kevyiqr; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@@ -2595,13 +2400,6 @@ CREATE INDEX idxm6k18dusy2lfi5y81k8g256sa ON public."RegistrarUpdateHistory" USI
|
||||
CREATE INDEX idxmk1d2ngdtfkg6odmw7l5ejisw ON public."DomainDsDataHistory" USING btree (domain_repo_id, domain_history_revision_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: idxn1f711wicdnooa2mqb7g1m55o; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX idxn1f711wicdnooa2mqb7g1m55o ON public."Contact" USING btree (deletion_time);
|
||||
|
||||
|
||||
--
|
||||
-- Name: idxn898pb9mwcg359cdwvolb11ck; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@@ -2630,20 +2428,6 @@ CREATE INDEX idxnjhib7v6fj7dhj5qydkefkl2u ON public."Domain" USING btree (lordn_
|
||||
CREATE INDEX idxnuyqo6hrtuvbcmuecf7vkfmle ON public."PollMessage" USING btree (domain_repo_id, domain_history_revision_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: idxo1xdtpij2yryh0skxe9v91sep; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX idxo1xdtpij2yryh0skxe9v91sep ON public."ContactHistory" USING btree (creation_time);
|
||||
|
||||
|
||||
--
|
||||
-- Name: idxoqd7n4hbx86hvlgkilq75olas; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX idxoqd7n4hbx86hvlgkilq75olas ON public."Contact" USING btree (contact_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: idxoqttafcywwdn41um6kwlt0n8b; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@@ -2756,13 +2540,6 @@ CREATE INDEX idxsfci08jgsymxy6ovh4k7r358c ON public."Domain" USING btree (billin
|
||||
CREATE INDEX idxsu1nam10cjes9keobapn5jvxj ON public."DomainHistory" USING btree (history_type);
|
||||
|
||||
|
||||
--
|
||||
-- Name: idxsudwswtwqnfnx2o1hx4s0k0g5; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX idxsudwswtwqnfnx2o1hx4s0k0g5 ON public."ContactHistory" USING btree (history_modification_time);
|
||||
|
||||
|
||||
--
|
||||
-- Name: idxtmlqd31dpvvd2g1h9i7erw6aj; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@@ -2833,14 +2610,6 @@ CREATE INDEX spec11threatmatch_registrar_id_idx ON public."Spec11ThreatMatch" US
|
||||
CREATE INDEX spec11threatmatch_tld_idx ON public."Spec11ThreatMatch" USING btree (tld);
|
||||
|
||||
|
||||
--
|
||||
-- Name: Contact fk1sfyj7o7954prbn1exk7lpnoe; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public."Contact"
|
||||
ADD CONSTRAINT fk1sfyj7o7954prbn1exk7lpnoe FOREIGN KEY (creation_registrar_id) REFERENCES public."Registrar"(registrar_id) DEFERRABLE INITIALLY DEFERRED;
|
||||
|
||||
|
||||
--
|
||||
-- Name: Domain fk2jc69qyg2tv9hhnmif6oa1cx1; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
@@ -2881,14 +2650,6 @@ ALTER TABLE ONLY public."ClaimsEntry"
|
||||
ADD CONSTRAINT fk6sc6at5hedffc0nhdcab6ivuq FOREIGN KEY (revision_id) REFERENCES public."ClaimsList"(revision_id) DEFERRABLE INITIALLY DEFERRED;
|
||||
|
||||
|
||||
--
|
||||
-- Name: Contact fk93c185fx7chn68uv7nl6uv2s0; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public."Contact"
|
||||
ADD CONSTRAINT fk93c185fx7chn68uv7nl6uv2s0 FOREIGN KEY (current_sponsor_registrar_id) REFERENCES public."Registrar"(registrar_id) DEFERRABLE INITIALLY DEFERRED;
|
||||
|
||||
|
||||
--
|
||||
-- Name: BillingCancellation fk_billing_cancellation_billing_event_id; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
@@ -2953,30 +2714,6 @@ ALTER TABLE ONLY public."ConsoleUpdateHistory"
|
||||
ADD CONSTRAINT fk_console_update_history_acting_user FOREIGN KEY (acting_user) REFERENCES public."User"(email_address);
|
||||
|
||||
|
||||
--
|
||||
-- Name: ContactHistory fk_contact_history_registrar_id; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public."ContactHistory"
|
||||
ADD CONSTRAINT fk_contact_history_registrar_id FOREIGN KEY (history_registrar_id) REFERENCES public."Registrar"(registrar_id) DEFERRABLE INITIALLY DEFERRED;
|
||||
|
||||
|
||||
--
|
||||
-- Name: Contact fk_contact_transfer_gaining_registrar_id; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public."Contact"
|
||||
ADD CONSTRAINT fk_contact_transfer_gaining_registrar_id FOREIGN KEY (transfer_gaining_registrar_id) REFERENCES public."Registrar"(registrar_id) DEFERRABLE INITIALLY DEFERRED;
|
||||
|
||||
|
||||
--
|
||||
-- Name: Contact fk_contact_transfer_losing_registrar_id; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public."Contact"
|
||||
ADD CONSTRAINT fk_contact_transfer_losing_registrar_id FOREIGN KEY (transfer_losing_registrar_id) REFERENCES public."Registrar"(registrar_id) DEFERRABLE INITIALLY DEFERRED;
|
||||
|
||||
|
||||
--
|
||||
-- Name: Domain fk_domain_billing_recurrence_id; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
@@ -3257,14 +2994,6 @@ ALTER TABLE ONLY public."Domain"
|
||||
ADD CONSTRAINT fkjc0r9r5y1lfbt4gpbqw4wsuvq FOREIGN KEY (last_epp_update_registrar_id) REFERENCES public."Registrar"(registrar_id) DEFERRABLE INITIALLY DEFERRED;
|
||||
|
||||
|
||||
--
|
||||
-- Name: Contact fkmb7tdiv85863134w1wogtxrb2; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public."Contact"
|
||||
ADD CONSTRAINT fkmb7tdiv85863134w1wogtxrb2 FOREIGN KEY (last_epp_update_registrar_id) REFERENCES public."Registrar"(registrar_id) DEFERRABLE INITIALLY DEFERRED;
|
||||
|
||||
|
||||
--
|
||||
-- Name: PremiumEntry fko0gw90lpo1tuee56l0nb6y6g5; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
# Admin tool
|
||||
|
||||
Nomulus includes a command-line registry administration tool that is invoked
|
||||
using the `nomulus` command. It has the ability to view and change a large
|
||||
number of things in a live Nomulus environment, including creating registrars,
|
||||
updating premium and reserved lists, running an EPP command from a given XML
|
||||
file, and performing various backend tasks like re-running RDE if the most
|
||||
Nomulus includes a command-line registry administration tool. It has the ability
|
||||
to view and change a large number of things in a live Nomulus environment,
|
||||
including creating registrars, running arbitrary EPP commands from given XML
|
||||
files, and performing various backend tasks like re-running RDE if the most
|
||||
recent export failed. Its code lives inside the tools package
|
||||
(`java/google/registry/tools`), and is compiled by building the `nomulus` target
|
||||
in the Bazel BUILD file in that package.
|
||||
(`core/src/main/java/google/registry/tools`), and is compiled by building the
|
||||
`nomulus` Gradle target in the `core` project, e.g. `./gradlew core:nomulus`.
|
||||
|
||||
The tool connects to the Google Cloud Platform project (identified by project
|
||||
ID) that was configured in your implementation of `RegistryConfig` when the tool
|
||||
@@ -21,18 +20,25 @@ ID is also "acme-registry", and the project ID for the sandbox environment is
|
||||
|
||||
## Build the tool
|
||||
|
||||
To build the `nomulus` tool, execute the following `bazel build` command inside
|
||||
any directory of the codebase. You must rebuild the tool any time that you edit
|
||||
configuration or make database schema changes.
|
||||
To build the `nomulus` tool's jarfile, execute the following Gradle command
|
||||
inside the project's home directory: `./gradlew core:nomulus`. You must rebuild
|
||||
the tool any time that you edit configuration or make database schema changes.
|
||||
Note that proper project configuration is necessary for building the tool --
|
||||
this includes the specialized configuration such as GCP project names.
|
||||
|
||||
It's recommended that you alias the compiled jarfile located at
|
||||
`core/build/libs/nomulus.jar` (or add it to your shell path) so that you can run
|
||||
it easily, e.g.
|
||||
|
||||
```shell
|
||||
$ bazel build //java/google/registry/tools:nomulus
|
||||
$ alias nomulus="java -jar core/build/libs/nomulus.jar"
|
||||
```
|
||||
|
||||
It's recommended that you alias the compiled binary located at
|
||||
`bazel-genfiles/java/google/registry/nomulus` (or add it to your shell path) so
|
||||
that you can run it easily. The rest of this guide assumes that it has been
|
||||
aliased to `nomulus`.
|
||||
The rest of this guide assumes that it has been aliased to `nomulus`.
|
||||
|
||||
Note: for Google Registry employees, the nomulus tool is built as part of the
|
||||
weekly deployment process and the nomulus jarfile is located at
|
||||
`/google/data/ro/teams/domain-registry/tools/live/nomulus.jar`
|
||||
|
||||
## Running the tool
|
||||
|
||||
@@ -56,33 +62,27 @@ metadata contained within the code to yield documentation.
|
||||
|
||||
## Local and server-side commands
|
||||
|
||||
There are two broad ways that commands are implemented: some that send requests
|
||||
to `ToolsServlet` to execute the action on the server (these commands implement
|
||||
`ServerSideCommand`), and others that execute the command locally using the
|
||||
[Remote API](https://cloud.google.com/appengine/docs/java/tools/remoteapi)
|
||||
(these commands implement `RemoteApiCommand`). Server-side commands take more
|
||||
work to implement because they require both a client and a server-side
|
||||
component.
|
||||
However, they are fully capable of doing anything that is possible with App
|
||||
Engine, including running a large MapReduce, because they execute on the tools
|
||||
service in the App Engine cloud.
|
||||
There are two broad ways that commands are implemented: some send requests to
|
||||
the backend server to execute the action on the server (these commands implement
|
||||
`CommandWithConnection`), and others that execute the command locally using
|
||||
access to the database. Commands that send requests to the backend server are
|
||||
more work to implement because they require both a client-side and server-side
|
||||
component, but they are more powerful -- even running Flow pipelines or other
|
||||
long-running intensive jobs.
|
||||
|
||||
Local commands, by contrast, are easier to implement, because there is only a
|
||||
local component to write, but they aren't as powerful. A general rule of thumb
|
||||
for making this determination is to use a local command if possible, or a
|
||||
server-side command otherwise.
|
||||
Local commands are easier to implement (because there is only a local component
|
||||
to write) but they aren't as powerful. As a rule of thumb, use a local command
|
||||
if possible.
|
||||
|
||||
## Common tool patterns
|
||||
|
||||
All tools ultimately implement the `Command` interface located in the `tools`
|
||||
package. If you use an integrated development environment (IDE) such as IntelliJ
|
||||
to view the type hierarchy of that interface, you'll see all of the commands
|
||||
that exist, as well as how a lot of them are grouped using sub-interfaces or
|
||||
abstract classes that provide additional functionality. The most common patterns
|
||||
that are used by a large number of other tools are:
|
||||
to view the type hierarchy of that interface, you'll see all the commands that
|
||||
exist, as well as how a lot of them are grouped using sub-interfaces or abstract
|
||||
classes that provide additional functionality. The most common patterns that are
|
||||
used by a large number of other tools are:
|
||||
|
||||
* **`BigqueryCommand`** -- Provides a connection to BigQuery for tools that
|
||||
need it.
|
||||
* **`ConfirmingCommand`** -- Provides the methods `prompt()` and `execute()`
|
||||
to override. `prompt()` outputs a message (usually what the command is going
|
||||
to do) and prompts the user to confirm execution of the command, and then
|
||||
@@ -90,10 +90,9 @@ that are used by a large number of other tools are:
|
||||
* **`EppToolCommand`** -- Commands that work by executing EPP commands against
|
||||
the server, usually by filling in a template with parameters that were
|
||||
passed on the command-line.
|
||||
* **`MutatingEppToolCommand`** -- A sub-class of `EppToolCommand` that
|
||||
provides a `--dry_run` flag, that, if passed, will display the output from
|
||||
the server of what the command would've done without actually committing
|
||||
those changes.
|
||||
* **`MutatingEppToolCommand`** -- A subclass of `EppToolCommand` that provides
|
||||
a `--dry_run` flag, that, if passed, will display the output from the server
|
||||
of what the command would've done without actually committing those changes.
|
||||
* **`GetEppResourceCommand`** -- Gets individual EPP resources from the server
|
||||
and outputs them.
|
||||
* **`ListObjectsCommand`** -- Lists all objects of a specific type from the
|
||||
|
||||
@@ -1,153 +1,97 @@
|
||||
# Architecture
|
||||
|
||||
This document contains information on the overall architecture of Nomulus on
|
||||
[Google Cloud Platform](https://cloud.google.com/). It covers the App Engine
|
||||
architecture as well as other Cloud Platform services used by Nomulus.
|
||||
[Google Cloud Platform](https://cloud.google.com/).
|
||||
|
||||
## App Engine
|
||||
Nomulus was originally built for App Engine, but the modern architecture now
|
||||
uses Google Kubernetes Engine (GKE) for better flexibility and control over
|
||||
networking, running as a series of Java-based microservices within GKE pods.
|
||||
|
||||
[Google App Engine](https://cloud.google.com/appengine/) is a cloud computing
|
||||
platform that runs web applications in the form of servlets. Nomulus consists of
|
||||
Java servlets that process web requests. These servlets use other features
|
||||
provided by App Engine, including task queues and cron jobs, as explained
|
||||
below.
|
||||
In addition, because GKE (and standard HTTP load balancers) typically handle
|
||||
HTTP(s) traffic, Nomulus uses a custom proxy to handle raw TCP traffic required
|
||||
for EPP (Port 700). This proxy can run as a GKE sidecar or a standalone cluster.
|
||||
For more information on the proxy, see [the proxy setup guide](proxy-setup.md).
|
||||
|
||||
### Services
|
||||
### Workloads
|
||||
|
||||
Nomulus contains three [App Engine
|
||||
services](https://cloud.google.com/appengine/docs/python/an-overview-of-app-engine),
|
||||
which were previously called modules in earlier versions of App Engine. The
|
||||
services are: default (also called front-end), backend, and tools. Each service
|
||||
runs independently in a lot of ways, including that they can be upgraded
|
||||
individually, their log outputs are separate, and their servers and configured
|
||||
scaling are separate as well.
|
||||
Nomulus contains four Kubernetes
|
||||
[workloads](https://kubernetes.io/docs/concepts/workloads/). Each workload is
|
||||
fairly independent as one would expect, including scaling.
|
||||
|
||||
Once you have your app deployed and running, the default service can be accessed
|
||||
at `https://project-id.appspot.com`, substituting whatever your App Engine app
|
||||
is named for "project-id". Note that that is the URL for the production instance
|
||||
of your app; other environments will have the environment name appended with a
|
||||
hyphen in the hostname, e.g. `https://project-id-sandbox.appspot.com`.
|
||||
The four workloads are referred to as `frontend`, `backend`, `console`, and
|
||||
`pubapi`.
|
||||
|
||||
The URL for the backend service is `https://backend-dot-project-id.appspot.com`
|
||||
and the URL for the tools service is `https://tools-dot-project-id.appspot.com`.
|
||||
The reason that the dot is escaped rather than forming subdomains is because the
|
||||
SSL certificate for `appspot.com` is only valid for `*.appspot.com` (no double
|
||||
wild-cards).
|
||||
Each workload's URL is created by prefixing the name of the workload to the base
|
||||
domain, e.g. `https://pubapi.mydomain.example`. Requests to each workload are
|
||||
all handled by the
|
||||
[RegistryServlet](https://github.com/google/nomulus/blob/master/core/src/main/java/google/registry/module/RegistryServlet.java)
|
||||
|
||||
#### Default service
|
||||
#### Frontend workload
|
||||
|
||||
The default service is responsible for all registrar-facing
|
||||
The frontend workload is responsible for all registrar-facing
|
||||
[EPP](https://en.wikipedia.org/wiki/Extensible_Provisioning_Protocol) command
|
||||
traffic, all user-facing WHOIS and RDAP traffic, and the admin and registrar web
|
||||
consoles, and is thus the most important service. If the service has any
|
||||
problems and goes down or stops servicing requests in a timely manner, it will
|
||||
begin to impact users immediately. Requests to the default service are handled
|
||||
by the `FrontendServlet`, which provides all of the endpoints exposed in
|
||||
`FrontendRequestComponent`.
|
||||
traffic. If the workload has any problems or goes down, it will begin to impact
|
||||
users immediately.
|
||||
|
||||
#### Backend service
|
||||
#### PubApi workload
|
||||
|
||||
The backend service is responsible for executing all regularly scheduled
|
||||
background tasks (using cron) as well as all asynchronous tasks. Requests to the
|
||||
backend service are handled by the `BackendServlet`, which provides all of the
|
||||
endpoints exposed in `BackendRequestComponent`. These include tasks for
|
||||
generating/exporting RDE, syncing the trademark list from TMDB, exporting
|
||||
backups, writing out DNS updates, handling asynchronous contact and host
|
||||
deletions, writing out commit logs, exporting metrics to BigQuery, and many
|
||||
more. Issues in the backend service will not immediately be apparent to end
|
||||
users, but the longer it is down, the more obvious it will become that
|
||||
user-visible tasks such as DNS and deletion are not being handled in a timely
|
||||
manner.
|
||||
The PubApi (Public API) workload is responsible for all public traffic to the
|
||||
registry. In practice, this primarily consists of RDAP traffic. This is split
|
||||
into a separate workload so that public users (without authentication) will have
|
||||
a harder time impacting intra-registry or registrar-registry actions.
|
||||
|
||||
The backend service is also where scheduled and automatically invoked MapReduces
|
||||
run, which includes some of the aforementioned tasks such as RDE and
|
||||
asynchronous resource deletion. Consequently, the backend service should be
|
||||
sized to support not just the normal ongoing DNS load but also the load incurred
|
||||
by MapReduces, both scheduled (such as RDE) and on-demand (asynchronous
|
||||
contact/host deletion).
|
||||
#### Backend workload
|
||||
|
||||
#### BSA service
|
||||
The backend workload is responsible for executing all regularly scheduled
|
||||
background tasks (using cron) as well as all asynchronous tasks. These include
|
||||
tasks for generating/exporting RDE, syncing the trademark list from TMDB,
|
||||
exporting backups, writing out DNS updates, syncing BSA data,
|
||||
generating/exporting ICANN activity data, and many more. Issues in the backend
|
||||
workload will not immediately be apparent to end users, but the longer it is
|
||||
down, the more obvious it will become that user-visible tasks such as DNS and
|
||||
deletion are not being handled in a timely manner.
|
||||
|
||||
The bsa service is responsible for business logic behind Nomulus and BSA
|
||||
functionality. Requests to the backend service are handled by the `BsaServlet`,
|
||||
which provides all of the endpoints exposed in `BsaRequestComponent`. These
|
||||
include tasks for downloading, processing and uploading BSA data.
|
||||
The backend workload is also where scheduled and automatically-invoked BEAM
|
||||
pipelines run, which includes some of the aforementioned tasks such as RDE.
|
||||
Consequently, the backend workload should be sized to support not just the
|
||||
normal ongoing DNS load but also the load incurred by BEAM pipelines, both
|
||||
scheduled (such as RDE) and on-demand (started by registry employees).
|
||||
|
||||
The backend workload also supports handling of manually-performed actions using
|
||||
the `nomulus` command-line tool, which provides administrative-level
|
||||
functionality for developers and tech support employees of the registry.
|
||||
|
||||
#### Tools service
|
||||
### Cloud Tasks queues
|
||||
|
||||
The tools service is responsible for servicing requests from the `nomulus`
|
||||
command line tool, which provides administrative-level functionality for
|
||||
developers and tech support employees of the registry. It is thus the least
|
||||
critical of the three services. Requests to the tools service are handled by the
|
||||
`ToolsServlet`, which provides all of the endpoints exposed in
|
||||
`ToolsRequestComponent`. Some example functionality that this service provides
|
||||
includes the server-side code to update premium lists, run EPP commands from the
|
||||
tool, and manually modify contacts/hosts/domains/and other resources. Problems
|
||||
with the tools service are not visible to users.
|
||||
|
||||
The tools service also runs ad-hoc MapReduces, like those invoked via `nomulus`
|
||||
tool subcommands like `generate_zone_files` and by manually hitting URLs under
|
||||
https://tools-dot-project-id.appspot.com, like
|
||||
`/_dr/task/refreshDnsForAllDomains`.
|
||||
|
||||
### Task queues
|
||||
|
||||
App Engine [task
|
||||
queues](https://cloud.google.com/appengine/docs/java/taskqueue/) provide an
|
||||
GCP's [Cloud Tasks](https://docs.cloud.google.com/tasks/docs) provides an
|
||||
asynchronous way to enqueue tasks and then execute them on some kind of
|
||||
schedule. There are two types of queues, push queues and pull queues. Tasks in
|
||||
push queues are always executing up to some throttlable limit. Tasks in pull
|
||||
queues remain there until the queue is polled by code that is running for some
|
||||
other reason. Essentially, push queues run their own tasks while pull queues
|
||||
just enqueue data that is used by something else. Many other parts of App Engine
|
||||
are implemented using task queues. For example, [App Engine
|
||||
cron](https://cloud.google.com/appengine/docs/java/config/cron) adds tasks to
|
||||
push queues at regularly scheduled intervals, and the [MapReduce
|
||||
framework](https://cloud.google.com/appengine/docs/java/dataprocessing/) adds
|
||||
tasks for each phase of the MapReduce algorithm.
|
||||
schedule. Task queues are essential because by nature, GKE architecture does not
|
||||
support long-running background processes, and so queues are thus the
|
||||
fundamental building block that allows asynchronous and background execution of
|
||||
code that is not in response to incoming web requests.
|
||||
|
||||
Nomulus uses a particular pattern of paired push/pull queues that is worth
|
||||
explaining in detail. Push queues are essential because App Engine's
|
||||
architecture does not support long-running background processes, and so push
|
||||
queues are thus the fundamental building block that allows asynchronous and
|
||||
background execution of code that is not in response to incoming web requests.
|
||||
However, they also have limitations in that they do not allow batch processing
|
||||
or grouping. That's where the pull queue comes in. Regularly scheduled tasks in
|
||||
the push queue will, upon execution, poll the corresponding pull queue for a
|
||||
specified number of tasks and execute them in a batch. This allows the code to
|
||||
execute in the background while taking advantage of batch processing.
|
||||
The task queues used by Nomulus are configured in the `cloud-tasks-queue.xml`
|
||||
file. Note that many push queues have a direct one-to-one correspondence with
|
||||
entries in `cloud-scheduler-tasks-ENVIRONMENT.xml` because they need to be
|
||||
fanned-out on a per-TLD or other basis (see the Cron section below for more
|
||||
explanation). The exact queue that a given cron task will use is passed as the
|
||||
query string parameter "queue" in the url specification for the cron task.
|
||||
|
||||
The task queues used by Nomulus are configured in the `cloud-tasks-queue.xml`
|
||||
file. Note that many push queues have a direct one-to-one correspondence with
|
||||
entries in `cloud-scheduler-tasks.xml` because they need to be fanned-out on a
|
||||
per-TLD or other basis (see the Cron section below for more explanation).
|
||||
The exact queue that a given cron task will use is passed as the query string
|
||||
parameter "queue" in the url specification for the cron task.
|
||||
|
||||
Here are the task queues in use by the system. All are push queues unless
|
||||
explicitly marked as otherwise.
|
||||
Here are the task queues in use by the system:
|
||||
|
||||
* `brda` -- Queue for tasks to upload weekly Bulk Registration Data Access
|
||||
(BRDA) files to a location where they are available to ICANN. The
|
||||
`RdeStagingReducer` (part of the RDE MapReduce) creates these tasks at the
|
||||
end of generating an RDE dump.
|
||||
* `dns-pull` -- A pull queue to enqueue DNS modifications. Cron regularly runs
|
||||
`ReadDnsQueueAction`, which drains the queue, batches modifications by TLD,
|
||||
and writes the batches to `dns-publish` to be published to the configured
|
||||
`DnsWriter` for the TLD.
|
||||
(BRDA) files to a location where they are available to ICANN. The RDE
|
||||
pipeline creates these tasks at the end of generating an RDE dump.
|
||||
* `dns-publish` -- Queue for batches of DNS updates to be pushed to DNS
|
||||
writers.
|
||||
* `lordn-claims` and `lordn-sunrise` -- Pull queues for handling LORDN
|
||||
exports. Tasks are enqueued synchronously during EPP commands depending on
|
||||
whether the domain name in question has a claims notice ID.
|
||||
* `dns-refresh` -- Queues for reading and fanning out DNS refresh requests,
|
||||
using the `DnsRefreshRequest` SQL table as the source of data
|
||||
* `marksdb` -- Queue for tasks to verify that an upload to NORDN was
|
||||
successfully received and verified. These tasks are enqueued by
|
||||
`NordnUploadAction` following an upload and are executed by
|
||||
`NordnVerifyAction`.
|
||||
* `nordn` -- Cron queue used for NORDN exporting. Tasks are executed by
|
||||
`NordnUploadAction`, which pulls LORDN data from the `lordn-claims` and
|
||||
`lordn-sunrise` pull queues (above).
|
||||
`NordnUploadAction`
|
||||
* `rde-report` -- Queue for tasks to upload RDE reports to ICANN following
|
||||
successful upload of full RDE files to the escrow provider. Tasks are
|
||||
enqueued by `RdeUploadAction` and executed by `RdeReportAction`.
|
||||
@@ -157,28 +101,25 @@ explicitly marked as otherwise.
|
||||
* `retryable-cron-tasks` -- Catch-all cron queue for various cron tasks that
|
||||
run infrequently, such as exporting reserved terms.
|
||||
* `sheet` -- Queue for tasks to sync registrar updates to a Google Sheets
|
||||
spreadsheet. Tasks are enqueued by `RegistrarServlet` when changes are made
|
||||
to registrar fields and are executed by `SyncRegistrarsSheetAction`.
|
||||
spreadsheet, done by `SyncRegistrarsSheetAction`.
|
||||
|
||||
### Cron jobs
|
||||
### Scheduled cron jobs
|
||||
|
||||
Nomulus uses App Engine [cron
|
||||
jobs](https://cloud.google.com/appengine/docs/java/config/cron) to run periodic
|
||||
scheduled actions. These actions run as frequently as once per minute (in the
|
||||
case of syncing DNS updates) or as infrequently as once per month (in the case
|
||||
of RDE exports). Cron tasks are specified in `cron.xml` files, with one per
|
||||
environment. There are more tasks that run in Production than in other
|
||||
environments because tasks like uploading RDE dumps are only done for the live
|
||||
system. Cron tasks execute on the `backend` service.
|
||||
Nomulus uses [Cloud Scheduler](https://docs.cloud.google.com/scheduler/docs) to
|
||||
run periodic scheduled actions. These actions run as frequently as once per
|
||||
minute (in the case of syncing DNS updates) or as infrequently as once per month
|
||||
(in the case of RDE exports). Cron tasks are specified in
|
||||
`cloud-scheduler-tasks-{ENVIRONMENT}.xml` files, with one per environment. There
|
||||
are more tasks that run in Production than in other environments because tasks
|
||||
like uploading RDE dumps are only done for the live system.
|
||||
|
||||
Most cron tasks use the `TldFanoutAction` which is accessed via the
|
||||
`/_dr/cron/fanout` URL path. This action, which is run by the BackendServlet on
|
||||
the backend service, fans out a given cron task for each TLD that exists in the
|
||||
registry system, using the queue that is specified in the `cron.xml` entry.
|
||||
Because some tasks may be computationally intensive and could risk spiking
|
||||
system latency if all start executing immediately at the same time, there is a
|
||||
`jitterSeconds` parameter that spreads out tasks over the given number of
|
||||
seconds. This is used with DNS updates and commit log deletion.
|
||||
`/_dr/cron/fanout` URL path. This action fans out a given cron task for each TLD
|
||||
that exists in the registry system, using the queue that is specified in the XML
|
||||
entry. Because some tasks may be computationally intensive and could risk
|
||||
spiking system latency if all start executing immediately at the same time,
|
||||
there is a `jitterSeconds` parameter that spreads out tasks over the given
|
||||
number of seconds. This is used with DNS updates and commit log deletion.
|
||||
|
||||
The reason the `TldFanoutAction` exists is that a lot of tasks need to be done
|
||||
separately for each TLD, such as RDE exports and NORDN uploads. It's simpler to
|
||||
@@ -192,8 +133,7 @@ tasks retry in the face of transient errors.
|
||||
The full list of URL parameters to `TldFanoutAction` that can be specified in
|
||||
cron.xml is:
|
||||
|
||||
* `endpoint` -- The path of the action that should be executed (see
|
||||
`web.xml`).
|
||||
* `endpoint` -- The path of the action that should be executed
|
||||
* `queue` -- The cron queue to enqueue tasks in.
|
||||
* `forEachRealTld` -- Specifies that the task should be run in each TLD of
|
||||
type `REAL`. This can be combined with `forEachTestTld`.
|
||||
@@ -218,14 +158,14 @@ Each environment is thus completely independent.
|
||||
The different environments are specified in `RegistryEnvironment`. Most
|
||||
correspond to a separate App Engine app except for `UNITTEST` and `LOCAL`, which
|
||||
by their nature do not use real environments running in the cloud. The
|
||||
recommended naming scheme for the App Engine apps that has the best possible
|
||||
compatibility with the codebase and thus requires the least configuration is to
|
||||
pick a name for the production app and then suffix it for the other
|
||||
environments. E.g., if the production app is to be named 'registry-platform',
|
||||
then the sandbox app would be named 'registry-platform-sandbox'.
|
||||
recommended project naming scheme that has the best possible compatibility with
|
||||
the codebase and thus requires the least configuration is to pick a name for the
|
||||
production app and then suffix it for the other environments. E.g., if the
|
||||
production app is to be named 'registry-platform', then the sandbox app would be
|
||||
named 'registry-platform-sandbox'.
|
||||
|
||||
The full list of environments supported out-of-the-box, in descending order from
|
||||
real to not, is:
|
||||
real to not-real, is:
|
||||
|
||||
* `PRODUCTION` -- The real production environment that is actually running
|
||||
live TLDs. Since Nomulus is a shared registry platform, there need only ever
|
||||
@@ -270,28 +210,28 @@ of experience running a production registry using this codebase.
|
||||
|
||||
## Cloud SQL
|
||||
|
||||
To be filled.
|
||||
Nomulus uses [GCP Cloud SQL](https://cloud.google.com/sql) (Postgres) to store
|
||||
information. For more information, see the
|
||||
[DB project README file.](../db/README.md)
|
||||
|
||||
## Cloud Storage buckets
|
||||
|
||||
Nomulus uses [Cloud Storage](https://cloud.google.com/storage/) for bulk storage
|
||||
of large flat files that aren't suitable for Cloud SQL. These files include
|
||||
backups, RDE exports, and reports. Each bucket name must be unique across all of
|
||||
Google Cloud Storage, so we use the common recommended pattern of prefixing all
|
||||
buckets with the name of the App Engine app (which is itself globally unique).
|
||||
Most of the bucket names are configurable, but the defaults are as follows, with
|
||||
PROJECT standing in as a placeholder for the App Engine app name:
|
||||
of large flat files that aren't suitable for SQL. These files include backups,
|
||||
RDE exports, and reports. Each bucket name must be unique across all of Google
|
||||
Cloud Storage, so we use the common recommended pattern of prefixing all buckets
|
||||
with the name of the project (which is itself globally unique). Most of the
|
||||
bucket names are configurable, but the most important / relevant defaults are:
|
||||
|
||||
* `PROJECT-billing` -- Monthly invoice files for each registrar.
|
||||
* `PROJECT-commits` -- Daily exports of commit logs that are needed for
|
||||
potentially performing a restore.
|
||||
* `PROJECT-bsa` -- BSA data and output
|
||||
* `PROJECT-domain-lists` -- Daily exports of all registered domain names per
|
||||
TLD.
|
||||
* `PROJECT-gcs-logs` -- This bucket is used at Google to store the GCS access
|
||||
logs and storage data. This bucket is not required by the Registry system,
|
||||
but can provide useful logging information. For instructions on setup, see
|
||||
the [Cloud Storage
|
||||
documentation](https://cloud.google.com/storage/docs/access-logs).
|
||||
the
|
||||
[Cloud Storage documentation](https://cloud.google.com/storage/docs/access-logs).
|
||||
* `PROJECT-icann-brda` -- This bucket contains the weekly ICANN BRDA files.
|
||||
There is no lifecycle expiration; we keep a history of all the files. This
|
||||
bucket must exist for the BRDA process to function.
|
||||
@@ -301,9 +241,3 @@ PROJECT standing in as a placeholder for the App Engine app name:
|
||||
regularly uploaded to the escrow provider. Lifecycle is set to 90 days. The
|
||||
bucket must exist.
|
||||
* `PROJECT-reporting` -- Contains monthly ICANN reporting files.
|
||||
* `PROJECT.appspot.com` -- Temporary MapReduce files are stored here. By
|
||||
default, the App Engine MapReduce library places its temporary files in a
|
||||
bucket named {project}.appspot.com. This bucket must exist. To keep
|
||||
temporary files from building up, a 90-day or 180-day lifecycle should be
|
||||
applied to the bucket, depending on how long you want to be able to go back
|
||||
and debug MapReduce problems.
|
||||
|
||||
@@ -8,61 +8,41 @@ requests will be authorized to invoke the action.
|
||||
## Authentication and authorization properties
|
||||
|
||||
The `auth` attribute is an enumeration. Each value of the enumeration
|
||||
corresponds to a triplet of properties:
|
||||
corresponds to a pair of properties:
|
||||
|
||||
* the *authentication methods* allowed by the action
|
||||
* the *minimum authentication level* which is authorized to run the action
|
||||
* the *user policy* for the action
|
||||
* the *minimum authentication level* which is authorized to run the action
|
||||
* the *user policy* for the action
|
||||
|
||||
### Authentication methods
|
||||
### Authentication Levels
|
||||
|
||||
Authentication methods are ways whereby the request can authenticate itself to
|
||||
the system. In the code, an *authentication mechanism* is a class which handles
|
||||
a particular authentication method. There are currently three methods:
|
||||
There exist three levels of authentication level:
|
||||
|
||||
* `INTERNAL`: used by requests generated from App Engine task queues; these
|
||||
requests do not have a user, because they are system-generated, so
|
||||
authentication consists solely of verifying that the request did indeed
|
||||
come from a task queue
|
||||
* `NONE`: no authentication was found
|
||||
* `APP`: the request was authenticated, but no user was present
|
||||
* `USER`: the request was authenticated with a specific user
|
||||
|
||||
* `API`: authentication using an API; the Nomulus release ships with one API
|
||||
authentication mechanism, OAuth 2, but you can write additional custom
|
||||
mechanisms to handle other protocols if needed
|
||||
`NONE` and `USER` are fairly straightforward results (either no authentication
|
||||
was present, or a user was present), but `APP` is a bit of a special case. It
|
||||
exists for requests coming from service accounts, Cloud Scheduler, or the
|
||||
proxy -- requests which are authenticated but don't necessarily come from any
|
||||
one particular "user" per se. That being said, authorized users *can* manually
|
||||
run these tasks; it's just that service accounts can too.
|
||||
|
||||
* `LEGACY`: authentication using the standard App Engine `UserService` API,
|
||||
which authenticates based on cookies and XSRF tokens
|
||||
|
||||
The details of the associated authentication mechanism classes are given later.
|
||||
|
||||
### Authentication levels
|
||||
|
||||
Each authentication method listed above can authenticate at one of three levels:
|
||||
|
||||
* `NONE`: no authentication was found
|
||||
* `APP`: the request was authenticated, but no user was present
|
||||
* `USER`: the request was authenticated with a specific user
|
||||
|
||||
For instance, `INTERNAL` authentication never returns an authentication level of
|
||||
`USER`, because internal requests generated from App Engine task queues do not
|
||||
execute as a particular end user account. `LEGACY` authentication, on the other
|
||||
hand, never returns an authentication level of `APP`, because authentication is
|
||||
predicated on identifying the user, so the only possible answers are `NONE` and
|
||||
`USER`.
|
||||
|
||||
Each action has a minimum request authentication level. Some actions are
|
||||
completely open to the public, and have a minimum level of `NONE`. Some require
|
||||
authentication but not a user, and have a minimum level of `APP`. And some
|
||||
cannot function properly without knowing the exact user, and have a minimum
|
||||
level of `USER`.
|
||||
Each action has a minimum request authentication level. Some actions (e.g. RDAP)
|
||||
are completely open to the public, and have a minimum level of `NONE`. Some
|
||||
require authentication but not necessarily a user, and have a minimum level of
|
||||
`APP`. And some cannot function properly without knowing the exact user, and
|
||||
have a minimum level of `USER`.
|
||||
|
||||
### User policy
|
||||
|
||||
The user policy indicates what kind of user is authorized to execute the action.
|
||||
There are three possible values:
|
||||
There are two possible values:
|
||||
|
||||
* `IGNORED`: the user information is ignored
|
||||
* `PUBLIC`: an authenticated user is required, but any user will do
|
||||
* `ADMIN`: there must be an authenticated user with admin privileges
|
||||
* `PUBLIC`: an authenticated user is required, but any user will do
|
||||
(authorization is done at a later state)
|
||||
* `ADMIN`: there must be an authenticated user with admin privileges (this
|
||||
includes service accounts)
|
||||
|
||||
Note that the user policy applies only to the automatic checking done by the
|
||||
framework before invoking the action. The action itself may do more checking.
|
||||
@@ -73,10 +53,6 @@ whether a user was found. If not, it issues a redirect to the login page.
|
||||
Likewise, other pages of the registrar console have a user policy of `PUBLIC`,
|
||||
meaning that any logged-in user can access the page. However, the code then
|
||||
looks up the user to make sure he or she is associated with a registrar.
|
||||
Admins can be granted permission to the registrar console by configuring a
|
||||
special registrar for internal admin use, using the `registryAdminClientId`
|
||||
setting. See the [global configuration
|
||||
guide](./configuration.md#global-configuration) for more details.
|
||||
|
||||
Also note that the user policy only applies when there is actually a user. Some
|
||||
actions can be executed either by an admin user or by an internal request coming
|
||||
@@ -87,64 +63,41 @@ require that there be a user, set the minimum authentication level to `USER`.
|
||||
|
||||
### Allowed authentication and authorization values
|
||||
|
||||
Not all triplets of the authentication method, minimum level and user policy
|
||||
make sense. A master enumeration lists all the valid triplets. They are:
|
||||
There are three pairs of authentication level + user policy that are used in
|
||||
Nomulus (or even make sense). These are:
|
||||
|
||||
* `AUTH_PUBLIC_ANONYMOUS`: Allow all access, and don't attempt to authenticate.
|
||||
The only authentication method is `INTERNAL`, with a minimum level of
|
||||
`NONE`. Internal requests will be flagged as such, but everything else
|
||||
passes the authorization check with a value of `NOT_AUTHENTICATED`.
|
||||
|
||||
* `AUTH_PUBLIC`: Allow all access, but attempt to authenticate the user. All
|
||||
three authentication methods are specified, with a minimum level of `NONE`
|
||||
and a user policy of `PUBLIC`. If the user can be authenticated by any
|
||||
means, the identity is passed to the request. But if not, the request still
|
||||
passes the authorization check, with a value of `NOT_AUTHENTICATED`.
|
||||
|
||||
* `AUTH_PUBLIC_LOGGED_IN`: Allow access only by authenticated users. The
|
||||
`API` and `LEGACY` authentication methods are supported, but not `INTERNAL`,
|
||||
because that does not identify a user. The minimum level is `USER`, with a
|
||||
user policy of `PUBLIC`. Only requests with a user authenticated via either
|
||||
the legacy, cookie-based method or an API method (e.g. OAuth 2) are
|
||||
authorized to run the action.
|
||||
|
||||
* `AUTH_INTERNAL_OR_ADMIN`: Allow access only by admin users or internal
|
||||
requests. This is appropriate for actions that should only be accessed by
|
||||
someone trusted (as opposed to anyone with a Google login). This currently
|
||||
allows only the `INTERNAL` and `API` methods, meaning that an admin user
|
||||
cannot authenticate themselves via the legacy authentication mechanism,
|
||||
which is used only for the registrar console. The minimum level is `APP`,
|
||||
because we don't require a user for internal requests, but the user policy
|
||||
is `ADMIN`, meaning that if there *is* a user, it needs to be an admin.
|
||||
|
||||
* `AUTH_PUBLIC_OR_INTERNAL`: Allows anyone access, as long as they use OAuth to
|
||||
authenticate. Also allows access from App Engine task-queue. Note that OAuth
|
||||
client ID still needs to be allow-listed in the config file for OAuth-based
|
||||
authentication to succeed. This is mainly used by the proxy.
|
||||
* `AUTH_PUBLIC`: Allow all access and don't attempt to authenticate. This is
|
||||
used for completely public endpoints such as RDAP.
|
||||
* `AUTH_PUBLIC_LOGGED_IN`: Allow access only by users authenticated with some
|
||||
type of OAuth token. This allows all users (`UserPolicy.PUBLIC`) but
|
||||
requires that a particular user exists and is logged in (`AuthLevel.USER`).
|
||||
This is used primarily for the registrar console.
|
||||
* `AUTH_ADMIN`: Allow access only by admin users or internal requests
|
||||
(including Cloud Scheduler tasks). This is appropriate for actions that
|
||||
should only be accessed by someone trusted (as opposed to anyone with a
|
||||
Google login). This permits app-internal authentication (`AuthLevel.APP`)
|
||||
but if a user is present, it must be an admin (`UserPolicy.ADMIN`). This is
|
||||
used by many automated requests, as well as the proxy.
|
||||
|
||||
### Action setting golden files
|
||||
|
||||
To make sure that the authentication and authorization settings are correct for
|
||||
all actions, a unit test uses reflection to compare all defined actions for a
|
||||
specific service to a golden file containing the correct settings. These files
|
||||
are:
|
||||
To make sure that the authentication and authorization settings are correct and
|
||||
expected for all actions, a unit test uses reflection to compare all defined
|
||||
actions for a specific service to a
|
||||
[golden file](https://github.com/google/nomulus/blob/master/core/src/test/resources/google/registry/module/routing.txt)
|
||||
containing the correct settings.
|
||||
|
||||
* `frontend_routing.txt` for the default (frontend) service
|
||||
* `backend_routing.txt` for the backend service
|
||||
* `tools_routing.txt` for the tools service
|
||||
Each line in the file lists a path, the class that handles that path, the
|
||||
allowable HTTP methods (meaning GET and POST, as opposed to the authentication
|
||||
methods described above), the value of the `automaticallyPrintOk` attribute (not
|
||||
relevant for purposes of this document), and the two authentication and
|
||||
authorization settings described above. Whenever actions are added, or their
|
||||
attributes are modified, the golden file needs to be updated.
|
||||
|
||||
Each of these files consists of lines listing a path, the class that handles
|
||||
that path, the allowable HTTP methods (meaning GET and POST, as opposed to the
|
||||
authentication methods described above), the value of the `automaticallyPrintOk`
|
||||
attribute (not relevant for purposes of this document), and the three
|
||||
authentication and authorization settings described above. Whenever actions are
|
||||
added, or their attributes are modified, the golden files need to be updated.
|
||||
|
||||
The golden files also serve as a convenient place to check out how things are
|
||||
set up. For instance, the tools actions are, for the most part, accessible to
|
||||
admins and internal requests only. The backend actions are mostly accessible
|
||||
only to internal requests. And the frontend actions are a grab-bag; some are
|
||||
open to the public, some to any user, some only to admins, etc.
|
||||
The golden file also serves as a convenient place to check out how things are
|
||||
set up. For instance, the backend actions are accessible to admins and internal
|
||||
requests only, the pubapi requests are open to the public, and console requests
|
||||
require an authenticated user.
|
||||
|
||||
### Example
|
||||
|
||||
@@ -156,11 +109,12 @@ body rather than the URL itself (which could be logged). Therefore, the class
|
||||
definition looks like:
|
||||
|
||||
```java
|
||||
|
||||
@Action(
|
||||
path = "/_dr/epp",
|
||||
method = Method.POST,
|
||||
auth = Auth.AUTH_INTERNAL_OR_ADMIN
|
||||
)
|
||||
service = Action.Service.FRONTEND,
|
||||
path = "/_dr/epp",
|
||||
method = Method.POST,
|
||||
auth = Auth.AUTH_ADMIN)
|
||||
public class EppTlsAction implements Runnable {
|
||||
...
|
||||
```
|
||||
@@ -169,8 +123,8 @@ and the corresponding line in frontend_routing.txt (including the header line)
|
||||
is:
|
||||
|
||||
```shell
|
||||
PATH CLASS METHODS OK AUTH_METHODS MIN USER_POLICY
|
||||
/_dr/epp EppTlsAction POST n INTERNAL,API APP ADMIN
|
||||
SERVICE PATH CLASS METHODS OK MIN USER_POLICY
|
||||
FRONTEND /_dr/epp EppTlsAction POST n APP ADMIN
|
||||
```
|
||||
|
||||
## Implementation
|
||||
@@ -178,16 +132,12 @@ PATH CLASS METHODS OK AUTH_METHODS MIN USER_POLICY
|
||||
The code implementing the authentication and authorization framework is
|
||||
contained in the `google.registry.request.auth` package. The main method is
|
||||
`authorize()`, in `RequestAuthenticator`. This method takes the auth settings
|
||||
and an HTTP request, and tries to authenticate and authorize the request using
|
||||
any of the specified methods, returning the result of its attempts. Note that
|
||||
failed authorization (in which case `authorize()` returns `Optional.absent()`)
|
||||
is different from the case where nothing can be authenticated, but the action
|
||||
does not require any; in that case, `authorize()` succeeds, returning the
|
||||
special result AuthResult.NOT_AUTHENTICATED.
|
||||
|
||||
There are separate classes (described below) for the mechanism which handles
|
||||
each authentication method. The list of allowable API authentication mechanisms
|
||||
(by default, just OAuth 2) is configured in `AuthModule`.
|
||||
and an HTTP request, and tries to authenticate and authorize the request,
|
||||
returning the result of its attempts. Note that failed authorization (in which
|
||||
case `authorize()` returns `Optional.absent()`) is different from the case where
|
||||
nothing can be authenticated, but the action does not require any; in that case,
|
||||
`authorize()` succeeds, returning the special result
|
||||
AuthResult.NOT_AUTHENTICATED.
|
||||
|
||||
The ultimate caller of `authorize()` is
|
||||
`google.registry.request.RequestHandler`, which is responsible for routing
|
||||
@@ -196,83 +146,51 @@ appropriate action, and making sure that the incoming HTTP method is appropriate
|
||||
for the action, it calls `authorize()`, and rejects the request if authorization
|
||||
fails.
|
||||
|
||||
### `LegacyAuthenticationMechanism`
|
||||
### Authentication methods
|
||||
|
||||
Legacy authentication is straightforward, because the App Engine `UserService`
|
||||
API does all the work. Because the protocol might be vulnerable to an XSRF
|
||||
attack, the authentication mechanism issues and checks XSRF tokens as part
|
||||
of the process if the HTTP method is not GET or HEAD.
|
||||
Nomulus requests are authenticated via OIDC token authentication, though these
|
||||
tokens can be created and validated in two ways. In each case, the
|
||||
authentication mechanism converts an HTTP request to an authentication result,
|
||||
which consists of an authentication level, a possible user object, and a
|
||||
possible service account email.
|
||||
|
||||
### `OAuthAuthenticationMechanism`
|
||||
#### `IapOidcAuthenticationMechanism`
|
||||
|
||||
OAuth 2 authentication is performed using the App Engine `OAuthService` API.
|
||||
There are three Nomulus configuration values involved:
|
||||
Most requests, e.g. the registrar console or Nomulus CLI requests) are routed
|
||||
through GCP's
|
||||
[Identity-Aware Proxy](https://docs.cloud.google.com/iap/docs/concepts-overview).
|
||||
This forces the user to log in to some GAIA account (specifically, one that is
|
||||
given access to the project). We attempt to validate a provided IAP OIDC token
|
||||
with the IAP issuer URL (`https://cloud.google.com/iap`) and the proper IAP
|
||||
audience (`/projects/{projectId}/global/backendServices/{serviceId}`), where
|
||||
`projectId` refers to the GCP project, and `serviceId` refers to the service ID
|
||||
retrievable from the
|
||||
[IAP configuration page](https://pantheon.corp.google.com/security/iap).
|
||||
Ideally, this service ID corresponds to the HTTPS load balancer that distributes
|
||||
requests to the GKE pods.
|
||||
|
||||
* `availableOauthScopes` is the set of OAuth scopes passed to the service to
|
||||
be checked for their presence.
|
||||
Note: the local Nomulus CLI's
|
||||
[LoginCommand](https://github.com/google/nomulus/blob/master/core/src/main/java/google/registry/tools/LoginCommand.java)
|
||||
uses a special-case form of this where it saves long-lived IAP credentials
|
||||
locally.
|
||||
|
||||
* `requiredOauthScopes` is the set of OAuth scopes which must be present. This
|
||||
should be a subset of the available scopes. All scopes in this set must be
|
||||
present for authentication to succeed.
|
||||
#### `RegularOidcAuthenticationMechanism`
|
||||
|
||||
* `allowedOauthClientIds` is the set of allowable OAuth client IDs. Any client
|
||||
ID in this set is sufficient for successful authentication.
|
||||
Service account requests ( e.g.
|
||||
[Cloud Scheduler jobs](https://docs.cloud.google.com/scheduler/docs/schedule-run-cron-job))
|
||||
or requests coming through the proxy use a non-IAP OIDC token provided by the
|
||||
caller. These requests have a different issuer URL (
|
||||
`https://accounts.google.com`) and use the fairly standard OAuth bearer token
|
||||
architecture -- an `Authorization` HTTP header of the form "Bearer: XXXX".
|
||||
|
||||
The code looks for an `Authorization` HTTP header of the form "BEARER XXXX...",
|
||||
containing the access token. If it finds one, it calls `OAuthService` to
|
||||
validate the token, check that the scopes and client ID match, and retrieve the
|
||||
flag indicating whether the user is an admin.
|
||||
### Configuration
|
||||
|
||||
### `AppEngineInternalAuthenticationMechanism`
|
||||
|
||||
Detection of internal requests is a little hacky. App Engine uses a special HTTP
|
||||
header, `X-AppEngine-QueueName`, to indicate the queue from which the request
|
||||
originates. If this header is present, internal authentication succeeds. App
|
||||
Engine normally strips this header from external requests, so only internal
|
||||
requests will be authenticated.
|
||||
|
||||
App Engine has a special carve-out for admin users, who are allowed to specify
|
||||
headers which do not get stripped. So an admin user can use a command-line
|
||||
utility like `curl` to craft a request which appears to Nomulus to be an
|
||||
internal request. This has proven to be useful, facilitating the testing of
|
||||
actions which otherwise could only be run via a dummy cron job.
|
||||
|
||||
However, it only works if App Engine can authenticate the user as an admin via
|
||||
the `UserService` API. OAuth won't work, because authentication is performed by
|
||||
the Nomulus code, and the headers will already have been stripped by App Engine
|
||||
before the request is executed. Only the legacy, cookie-based method will work.
|
||||
|
||||
Be aware that App Engine defines an "admin user" as anyone with access to the
|
||||
App Engine project, even those with read-only access.
|
||||
|
||||
## Other topics
|
||||
|
||||
### OAuth 2 not supported for the registry console
|
||||
|
||||
Currently, OAuth 2 is only supported for requests which specify the
|
||||
`Authorization` HTTP header. The OAuth code reads this header and passes it to
|
||||
the Google OAuth server (no other authentication servers are currently
|
||||
supported) to verify the user's identity. This works fine for the `nomulus`
|
||||
command-line tool.
|
||||
|
||||
It doesn't work for browser-based interactions such as the registrar console.
|
||||
For that, we will (we think) need to redirect the user to the authentication
|
||||
server, and upon receiving the user back, fish out the code and convert it to a
|
||||
token which we store in a cookie. None of this is particularly hard, but for the
|
||||
moment it seems easier to stick with the legacy App Engine UserService API. Of
|
||||
course, contributions from the open-source community are welcome. :)
|
||||
|
||||
### Authorization via `web.xml`
|
||||
|
||||
Before the modern authentication and authorization framework described in this
|
||||
document was put in place, Nomulus used to be protected by directives in the
|
||||
`web.xml` file which allowed only logged-in users to access most endpoints. This
|
||||
had the advantage of being very easy to implement, but it came with some
|
||||
drawbacks, the primary one being lack of support for OAuth 2. App Engine's
|
||||
standard login detection works fine when using a browser, but does not handle
|
||||
cases where the request is coming from a standalone program such as the
|
||||
`nomulus` command-line tool. By moving away from the `web.xml` approach, we
|
||||
gained more flexibility to support an array of authentication and authorization
|
||||
schemes, including custom ones developed by the Nomulus community, at the
|
||||
expense of having to perform the authentication and authorization ourselves in
|
||||
the code.
|
||||
The `auth` block of the configuration requires two fields: *
|
||||
`allowedServiceAccountEmails` is the list of service accounts that should be
|
||||
allowed to run tasks when internally authenticated. This will likely include
|
||||
whatever service account runs Nomulus in Google Kubernetes Engine, as well as
|
||||
the Cloud Scheduler service account. * `oauthClientId` is the OAuth client ID
|
||||
associated with IAP. This is retrievable from the
|
||||
[Clients page](https://pantheon.corp.google.com/auth/clients) of GCP after
|
||||
enabling the Identity-Aware Proxy. It should look something like
|
||||
`someNumbers-someNumbersAndLetters.apps.googleusercontent.com`
|
||||
|
||||
@@ -3,54 +3,46 @@
|
||||
This document contains information on the overall structure of the code, and how
|
||||
particularly important pieces of the system are implemented.
|
||||
|
||||
## Bazel build system
|
||||
## Gradle build system
|
||||
|
||||
[Bazel](https://www.bazel.io/) is used to build and test the Nomulus codebase.
|
||||
[Gradle](https://gradle.org/) is used to build and test the Nomulus codebase.
|
||||
|
||||
Bazel builds are described using [BUILD
|
||||
files](https://www.bazel.io/versions/master/docs/build-ref.html). A directory
|
||||
containing a BUILD file defines a package consisting of all files and
|
||||
directories underneath it, except those directories which themselves also
|
||||
contain BUILD files. A package contains targets. Most targets in the codebase
|
||||
are of the type `java_library`, which generates `JAR` files, or `java_test`,
|
||||
which runs tests.
|
||||
Nomulus, for the most part, uses fairly standard Gradle task naming for building
|
||||
and running tests, with the various tasks defined in various `build.gradle`
|
||||
files.
|
||||
|
||||
The key to Bazel's ability to create reproducible builds is the requirement that
|
||||
each build target must declare its direct dependencies. Each of those
|
||||
dependencies is a target, which, in turn, must also declare its dependencies.
|
||||
This recursive description of a target's dependencies forms an acyclic graph
|
||||
that fully describes the targets which must be built in order to build any
|
||||
target in the graph.
|
||||
Dependencies and their version restrictions are defined in the
|
||||
`dependencies.gradle` file. Within each subproject's `build.gradle` file, the
|
||||
actual dependencies used by that subproject are listed along with the type of
|
||||
dependency (e.g. implementation, testImplementation). Versions of each
|
||||
dependency are locked to avoid frequent dependency churn, with the locked
|
||||
versions stored in the various `gradle.lockfile` files. To update these
|
||||
versions, run any Gradle command (e.g. `./gradlew build`) with the
|
||||
`--write-locks` argument.
|
||||
|
||||
A wrinkle in this system is managing external dependencies. Bazel was designed
|
||||
first and foremost to manage builds where all code lives in a single source
|
||||
repository and is compiled from `HEAD`. In order to mesh with other build and
|
||||
packaging schemes, such as libraries distributed as compiled `JAR`s, Bazel
|
||||
supports [external target
|
||||
declarations](https://www.bazel.io/versions/master/docs/external.html#transitive-dependencies).
|
||||
The Nomulus codebase uses external targets pulled in from Maven Central, these
|
||||
are declared in `java/google/registry/repositories.bzl`. The dependencies of
|
||||
these external targets are not managed by Bazel; you must manually add all of
|
||||
the dependencies or use the
|
||||
[generate_workspace](https://docs.bazel.build/versions/master/generate-workspace.html)
|
||||
tool to do it.
|
||||
### Generating WAR archives for deployment
|
||||
|
||||
### Generating EAR/WAR archives for deployment
|
||||
The `jetty` project is the main entry point for building the Nomulus WAR files,
|
||||
and one can use the `war` gradle task to build the base WAR file. The various
|
||||
deployment/release files use Docker to deploy this, in a system that is too
|
||||
Google-specialized to replicate directly here.
|
||||
|
||||
There are special build target types for generating `WAR` and `EAR` files for
|
||||
deploying Nomulus to GAE. These targets, `zip_file` and `registry_ear_file` respectively, are used in `java/google/registry/BUILD`. To generate archives suitable for deployment on GAE:
|
||||
## Subprojects
|
||||
|
||||
```shell
|
||||
$ bazel build java/google/registry:registry_ear
|
||||
...
|
||||
bazel-genfiles/java/google/registry/registry.ear
|
||||
INFO: Elapsed time: 0.216s, Critical Path: 0.00s
|
||||
# This will also generate the per-module WAR files:
|
||||
$ ls bazel-genfiles/java/google/registry/*.war
|
||||
bazel-genfiles/java/google/registry/registry_backend.war
|
||||
bazel-genfiles/java/google/registry/registry_default.war
|
||||
bazel-genfiles/java/google/registry/registry_tools.war
|
||||
```
|
||||
Within the Nomulus repository there are a few notable subprojects:
|
||||
|
||||
* `util` contains tools that don't depend on any of our other code, e.g.
|
||||
libraries or raw utilities
|
||||
* `db` contains database-related code, managing the schema and
|
||||
deployment/testing of the database.
|
||||
* `integration` contains tests to make sure that schema rollouts won't break
|
||||
Nomulus, that code versions and schema versions are cross-compatible
|
||||
* `console-webapp` contains the Typescript/HTML/CSS/Angular code for the
|
||||
registrar console frontend
|
||||
* `proxy` contains code for the EPP proxy, which relays port 700 requests to
|
||||
the core EPP services
|
||||
* `core` contains the bulk of the core Nomulus code, including request
|
||||
handling+serving, backend, actions, etc
|
||||
|
||||
## Cursors
|
||||
|
||||
@@ -72,8 +64,8 @@ The following cursor types are defined:
|
||||
* **`RDE_UPLOAD`** - RDE (thick) escrow deposit upload
|
||||
* **`RDE_UPLOAD_SFTP`** - Cursor that tracks the last time we talked to the
|
||||
escrow provider's SFTP server for a given TLD.
|
||||
* **`RECURRING_BILLING`** - Expansion of `BillingRecurrence` (renew) billing events
|
||||
into one-time `BillingEvent`s.
|
||||
* **`RECURRING_BILLING`** - Expansion of `BillingRecurrence` (renew) billing
|
||||
events into one-time `BillingEvent`s.
|
||||
* **`SYNC_REGISTRAR_SHEET`** - Tracks the last time the registrar spreadsheet
|
||||
was successfully synced.
|
||||
|
||||
@@ -82,16 +74,9 @@ next timestamp at which an operation should resume processing and a `CursorType`
|
||||
that identifies which operation the cursor is associated with. In many cases,
|
||||
there are multiple cursors per operation; for instance, the cursors related to
|
||||
RDE reporting, staging, and upload are per-TLD cursors. To accomplish this, each
|
||||
`Cursor` also has a scope, a `Key<ImmutableObject>` to which the particular
|
||||
cursor applies (this can be e.g. a `Registry` or any other `ImmutableObject` in
|
||||
the database, depending on the operation). If the `Cursor` applies to the entire
|
||||
registry environment, it is considered a global cursor and has a scope of
|
||||
`EntityGroupRoot.getCrossTldKey()`.
|
||||
|
||||
Cursors are singleton entities by type and scope. The id for a `Cursor` is a
|
||||
deterministic string that consists of the websafe string of the Key of the scope
|
||||
object concatenated with the name of the name of the cursor type, separated by
|
||||
an underscore.
|
||||
`Cursor` also has a scope, a string to which the particular cursor applies (this
|
||||
can be anything, but in practice is either a TLD or `GLOBAL` for cross-TLD
|
||||
cursors. Cursors are singleton entities by type and scope.
|
||||
|
||||
## Guava
|
||||
|
||||
@@ -101,8 +86,7 @@ idiomatic, well-tested, and performant add-ons to the JDK. There are several
|
||||
libraries in particular that you should familiarize yourself with, as they are
|
||||
used extensively throughout the codebase:
|
||||
|
||||
* [Immutable
|
||||
Collections](https://github.com/google/guava/wiki/ImmutableCollectionsExplained):
|
||||
* [Immutable Collections](https://github.com/google/guava/wiki/ImmutableCollectionsExplained):
|
||||
Immutable collections are a useful defensive programming technique. When an
|
||||
Immutable collection type is used as a parameter type, it immediately
|
||||
indicates that the given collection will not be modified in the method.
|
||||
@@ -144,11 +128,10 @@ as follows:
|
||||
|
||||
* `Domain` ([RFC 5731](https://tools.ietf.org/html/rfc5731))
|
||||
* `Host` ([RFC 5732](https://tools.ietf.org/html/rfc5732))
|
||||
* `Contact` ([RFC 5733](https://tools.ietf.org/html/rfc5733))
|
||||
|
||||
All `EppResource` entities use a Repository Object Identifier (ROID) as its
|
||||
unique id, in the format specified by [RFC
|
||||
5730](https://tools.ietf.org/html/rfc5730#section-2.8) and defined in
|
||||
unique id, in the format specified by
|
||||
[RFC 5730](https://tools.ietf.org/html/rfc5730#section-2.8) and defined in
|
||||
`EppResourceUtils.createRoid()`.
|
||||
|
||||
Each entity also tracks a number of timestamps related to its lifecycle (in
|
||||
@@ -164,12 +147,9 @@ the status of a resource at a given point in time.
|
||||
|
||||
## Foreign key indexes
|
||||
|
||||
Foreign key indexes provide a means of loading active instances of `EppResource`
|
||||
objects by their unique IDs:
|
||||
|
||||
* `Domain`: fully-qualified domain name
|
||||
* `Contact`: contact id
|
||||
* `Host`: fully-qualified host name
|
||||
`Domain` and `Host` each are foreign-keyed, meaning we often wish to query them
|
||||
by their foreign keys (fully-qualified domain name and fully-qualified host
|
||||
name, respectively).
|
||||
|
||||
Since all `EppResource` entities are indexed on ROID (which is also unique, but
|
||||
not as useful as the resource's name), the `ForeignKeyUtils` provides a way to
|
||||
@@ -203,10 +183,9 @@ events that are recorded as history entries, including:
|
||||
|
||||
The full list is captured in the `HistoryEntry.Type` enum.
|
||||
|
||||
Each `HistoryEntry` has a parent `Key<EppResource>`, the EPP resource that was
|
||||
mutated by the event. A `HistoryEntry` will also contain the complete EPP XML
|
||||
command that initiated the mutation, stored as a byte array to be agnostic of
|
||||
encoding.
|
||||
Each `HistoryEntry` has a reference to a singular EPP resource that was mutated
|
||||
by the event. A `HistoryEntry` will also contain the complete EPP XML command
|
||||
that initiated the mutation, stored as a byte array to be agnostic of encoding.
|
||||
|
||||
A `HistoryEntry` also captures other event metadata, such as the `DateTime` of
|
||||
the change, whether the change was created by a superuser, and the ID of the
|
||||
@@ -215,9 +194,9 @@ registrar that sent the command.
|
||||
## Poll messages
|
||||
|
||||
Poll messages are the mechanism by which EPP handles asynchronous communication
|
||||
between the registry and registrars. Refer to [RFC 5730 Section
|
||||
2.9.2.3](https://tools.ietf.org/html/rfc5730#section-2.9.2.3) for their protocol
|
||||
specification.
|
||||
between the registry and registrars. Refer to
|
||||
[RFC 5730 Section 2.9.2.3](https://tools.ietf.org/html/rfc5730#section-2.9.2.3)
|
||||
for their protocol specification.
|
||||
|
||||
Poll messages are stored by the system as entities in the database. All poll
|
||||
messages have an event time at which they become active; any poll request before
|
||||
@@ -245,8 +224,9 @@ poll messages are ACKed (and thus deleted) in `PollAckFlow`.
|
||||
## Billing events
|
||||
|
||||
Billing events capture all events in a domain's lifecycle for which a registrar
|
||||
will be charged. A `BillingEvent` will be created for the following reasons (the
|
||||
full list of which is represented by `BillingEvent.Reason`):
|
||||
will be charged. A one-time `BillingEvent` will (or can) be created for the
|
||||
following reasons (the full list of which is represented by
|
||||
`BillingBase.Reason`):
|
||||
|
||||
* Domain creates
|
||||
* Domain renewals
|
||||
@@ -254,19 +234,19 @@ full list of which is represented by `BillingEvent.Reason`):
|
||||
* Server status changes
|
||||
* Domain transfers
|
||||
|
||||
A `BillingBase` can also contain one or more `BillingBase.Flag` flags that
|
||||
provide additional metadata about the billing event (e.g. the application phase
|
||||
during which the domain was applied for).
|
||||
|
||||
All `BillingBase` entities contain a parent `VKey<HistoryEntry>` to identify the
|
||||
mutation that spawned the `BillingBase`.
|
||||
|
||||
There are 4 types of billing events, all of which extend the abstract
|
||||
`BillingBase` base class:
|
||||
|
||||
* **`BillingEvent`**, a one-time billing event.
|
||||
* **`BillingRecurrence`**, a recurring billing event (used for events such as domain
|
||||
renewals).
|
||||
* **`BillingCancellation`**, which represents the cancellation of either a `OneTime`
|
||||
or `BillingRecurrence` billing event. This is implemented as a distinct event to
|
||||
preserve the immutability of billing events.
|
||||
* **`BillingRecurrence`**, a recurring billing event (used for events such as
|
||||
domain renewals).
|
||||
* **`BillingCancellation`**, which represents the cancellation of either a
|
||||
`BillingEvent` or `BillingRecurrence` billing event. This is implemented as
|
||||
a distinct event to preserve the immutability of billing events.
|
||||
|
||||
A `BillingBase` can also contain one or more `BillingBase.Flag` flags that
|
||||
provide additional metadata about the billing event (e.g. the application phase
|
||||
during which the domain was applied for).
|
||||
|
||||
All `BillingBase` entities contain reference to a given ROID (`EppResource`
|
||||
reference) to identify the mutation that spawned the `BillingBase`.
|
||||
|
||||
@@ -1,82 +1,28 @@
|
||||
# Coding FAQ
|
||||
|
||||
This file is a motley assortment of informational emails generated in response
|
||||
to questions from development partners.
|
||||
|
||||
## How do I mock Google Cloud Storage in tests?
|
||||
|
||||
AppEngine's GCS client automatically switches over to a local implementation,
|
||||
[`GcsServiceFactory`](https://github.com/GoogleCloudPlatform/appengine-gcs-client/blob/master/java/src/main/java/com/google/appengine/tools/cloudstorage/GcsServiceFactory.java#L61),
|
||||
for tests.
|
||||
|
||||
So rather than mocking GCS-related stuff at all, just use the fake local
|
||||
implementation. This is what our tests should be doing; see
|
||||
[`ExportCommitLogDiffActionTest`](https://github.com/google/nomulus/blob/master/javatests/google/registry/backup/ExportCommitLogDiffActionTest.java#L70).
|
||||
|
||||
Very rarely there have been cases where we've needed something beyond that (e.g.
|
||||
to test against GCS being eventually consistent). In that case, rather than
|
||||
mocking GcsUtils, you'd need to create a real instance of it but pass in a
|
||||
mocked-out GcsService instance. Those are a bit of a pain to make since
|
||||
GcsServiceImpl itself is also final (and not code we control), but you could
|
||||
roll your own implementation by hand, or cheat and use a reflective proxy, as we
|
||||
do in
|
||||
[`GcsDiffFileListerTest`](https://github.com/google/domain-registry/blob/master/javatests/google/registry/backup/GcsDiffFileListerTest.java#L112).
|
||||
|
||||
## How do I test authentication on the SDK Development Server?
|
||||
|
||||
*Can someone explain how `GaeUserIdConverter.convertEmailAddressToGaeUserId()`
|
||||
actually does the conversion? I see it's doing a save/load(/delete) of a
|
||||
`GaeUserIdConverter`, which contains a `User`. Does Objectify do some magic on
|
||||
load to look up the real GAE user ID from the email address? In trying to get
|
||||
the registry to run in the SDK Development Server, I am seeing the wrong user ID
|
||||
when adding a new RegistryContact using the command line tool.*
|
||||
|
||||
The [App Engine development
|
||||
server](https://cloud.google.com/appengine/docs/python/tools/using-local-server)
|
||||
is not particularly robust; it appears that it always returns userId
|
||||
185804764220139124118 for any authenticated user, as per [this StackOverflow
|
||||
thread](http://stackoverflow.com/questions/30524328/what-user-is-provided-by-app-engine-devserver).
|
||||
|
||||
For testing purposes, it might suffice to just create a RegistrarContact with
|
||||
that userId by hand somehow, so that you can log in. In the longer term, if we
|
||||
switch to Google Sign-In, this specific problem would go away, but based on the
|
||||
above, it looks like OAuthService doesn't really work on the dev server either.
|
||||
|
||||
So for testing actual "real" authentication, you'd want to use an alpha instance
|
||||
rather than the development server. We don't use the development server very
|
||||
much internally for this reason.
|
||||
|
||||
## Do you support RDAP?
|
||||
|
||||
We provide an implementation of the Registry Data Access Protocol (RDAP) which provides
|
||||
similar data to the outdated WHOIS protocol, but in a structured format. The
|
||||
standard is defined in STD 95 and its RFCs:
|
||||
We provide an implementation of the Registry Data Access Protocol (RDAP) which
|
||||
provides similar data to the outdated WHOIS protocol, but in a structured
|
||||
format. The standard is defined in STD 95 and its RFCs:
|
||||
|
||||
* [RFC 7480: HTTP Usage in the Registration Data Access Protocol
|
||||
(RDAP)](https://tools.ietf.org/html/rfc7480)
|
||||
* [RFC 7480: HTTP Usage in the Registration Data Access Protocol (RDAP)](https://tools.ietf.org/html/rfc7480)
|
||||
* [RFC 7481: Security Services for the Registration Data Access Protocol
|
||||
(RDAP)](https://tools.ietf.org/html/rfc7481)
|
||||
* [RFC 9082: Registration Data Access Protocol (RDAP) Query
|
||||
Format](https://tools.ietf.org/html/rfc9082)
|
||||
* [RFC 9083: JSON Responses for the Registration Data Access Protocol
|
||||
(RDAP)](https://tools.ietf.org/html/rfc9083)
|
||||
* [RFC 9224: Finding the Authoritative Registration Data (RDAP)
|
||||
Service](https://tools.ietf.org/html/rfc9224)
|
||||
* [RFC 9082: Registration Data Access Protocol (RDAP) Query Format](https://tools.ietf.org/html/rfc9082)
|
||||
* [RFC 9083: JSON Responses for the Registration Data Access Protocol (RDAP)](https://tools.ietf.org/html/rfc9083)
|
||||
* [RFC 9224: Finding the Authoritative Registration Data (RDAP) Service](https://tools.ietf.org/html/rfc9224)
|
||||
|
||||
If you access this endpoint on a running Nomulus system:
|
||||
|
||||
`https://{PROJECT-ID}.appspot.com/rdap/domains?name=ex*`
|
||||
`https://pubapi.{SERVER_URL}/rdap/domains?name=ex*`
|
||||
|
||||
it should search for all domains that start with "ex", returning the results in
|
||||
JSON format. This functionality is still under development, so it is quite
|
||||
possible that the format of returned data will change over time, but the basic
|
||||
structure should be the same, as defined by RFCs 7480 through 7484. Request
|
||||
paths which ought to mostly work (though no guarantees yet):
|
||||
JSON format. Request paths which ought to work:
|
||||
|
||||
```
|
||||
/rdap/domain/abc.tld
|
||||
/rdap/nameserver/ns1.abc.tld
|
||||
/rdap/entity/ROID
|
||||
/rdap/entity/registrar-iana-identifier
|
||||
/rdap/domains?name=abc.tld
|
||||
/rdap/domains?name=abc*
|
||||
@@ -86,8 +32,6 @@ paths which ought to mostly work (though no guarantees yet):
|
||||
/rdap/domains?nsIp=1.2.3.4
|
||||
/rdap/nameservers?name=ns*.abc.tld
|
||||
/rdap/nameservers?ip=1.2.3.4
|
||||
/rdap/entities?fn=John*
|
||||
/rdap/entities?handle=ROI*
|
||||
/rdap/entities?handle=registrar-iana-identifier
|
||||
```
|
||||
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
There are multiple different kinds of configuration that go into getting a
|
||||
working registry system up and running. Broadly speaking, configuration works in
|
||||
two ways -- globally, for the entire sytem, and per-TLD. Global configuration is
|
||||
managed by editing code and deploying a new version, whereas per-TLD
|
||||
configuration is data that lives in the database in `Tld` entities, and is
|
||||
updated by running `nomulus` commands without having to deploy a new version.
|
||||
two ways -- globally, for the entire system, and per-TLD. Global configuration
|
||||
is managed by editing code and deploying a new version, whereas per-TLD
|
||||
configuration is data that lives in the database in `Tld` entities, and
|
||||
[is updated](operational-procedures/modifying-tlds.md) without having to deploy
|
||||
a new version.
|
||||
|
||||
## Initial configuration
|
||||
|
||||
@@ -23,40 +24,14 @@ Before getting into the details of configuration, it's important to note that a
|
||||
lot of configuration is environment-dependent. It is common to see `switch`
|
||||
statements that operate on the current `RegistryEnvironment`, and return
|
||||
different values for different environments. This is especially pronounced in
|
||||
the `UNITTEST` and `LOCAL` environments, which don't run on App Engine at all.
|
||||
As an example, some timeouts may be long in production and short in unit tests.
|
||||
the `UNITTEST` and `LOCAL` environments, which don't run on GCP at all. As an
|
||||
example, some timeouts may be long in production and short in unit tests.
|
||||
|
||||
See the [Architecture documentation](./architecture.md) for more details on
|
||||
environments as used by Nomulus.
|
||||
|
||||
## App Engine configuration
|
||||
|
||||
App Engine configuration isn't covered in depth in this document as it is
|
||||
thoroughly documented in the [App Engine configuration docs][app-engine-config].
|
||||
The main files of note that come pre-configured in Nomulus are:
|
||||
|
||||
* `cron.xml` -- Configuration of cronjobs
|
||||
* `web.xml` -- Configuration of URL paths on the webserver
|
||||
* `appengine-web.xml` -- Overall App Engine settings including number and type
|
||||
of instances
|
||||
* `cloud-scheduler-tasks.xml` -- Configuration of Cloud Scheduler Tasks
|
||||
* * `cloud-tasks-queue.xml` -- Configuration of Cloud Tasks Queue
|
||||
* `application.xml` -- Configuration of the application name and its services
|
||||
|
||||
Cron, web, and queue are covered in more detail in the "App Engine architecture"
|
||||
doc, and the rest are covered in the general App Engine documentation.
|
||||
|
||||
If you are not writing new code to implement custom features, is unlikely that
|
||||
you will need to make any modifications beyond simple changes to
|
||||
`application.xml` and `appengine-web.xml`. If you are writing new features, it's
|
||||
likely you'll need to add cronjobs, URL paths, and task queues, and thus edit
|
||||
those associated XML files.
|
||||
|
||||
The existing codebase is configured for running a full-scale registry with
|
||||
multiple TLDs. In order to deploy to App Engine, you will either need to
|
||||
[increase your quota](https://cloud.google.com/compute/quotas#requesting_additional_quota)
|
||||
to allow for at least 100 running instances or reduce `max-instances` in the
|
||||
backend `appengine-web.xml` files to 25 or less.
|
||||
TODO: documentation about how to set up GKE and what config points are necessary
|
||||
to modify there
|
||||
|
||||
## Global configuration
|
||||
|
||||
@@ -65,9 +40,9 @@ deployed in the app. The full list of config options and their default values
|
||||
can be found in the [`default-config.yaml`][default-config] file. If you wish to
|
||||
change any of these values, do not edit this file. Instead, edit the environment
|
||||
configuration file named
|
||||
`google/registry/config/files/nomulus-config-ENVIRONMENT.yaml`, overriding only
|
||||
the options you wish to change. Nomulus ships with blank placeholders for all
|
||||
standard environments.
|
||||
`core/src/main/java/google/registry/config/files/nomulus-config-ENVIRONMENT.yaml`,
|
||||
overriding only the options you wish to change. Nomulus ships with blank
|
||||
placeholders for all standard environments.
|
||||
|
||||
You will not need to change most of the default settings. Here is the subset of
|
||||
settings that you will need to change for all deployed environments, including
|
||||
@@ -75,52 +50,65 @@ development environments. See [`default-config.yaml`][default-config] for a full
|
||||
description of each option:
|
||||
|
||||
```yaml
|
||||
appEngine:
|
||||
projectId: # Your App Engine project ID
|
||||
toolsServiceUrl: https://tools-dot-PROJECT-ID.appspot.com # Insert your project ID
|
||||
isLocal: false # Causes saved credentials to be used.
|
||||
gcpProject:
|
||||
projectId: # Your GCP project ID
|
||||
projectIdNumber: # The corresponding ID number, found on the home page
|
||||
locationId: # e.g. us-central1
|
||||
isLocal: false # Causes saved credentials to be used
|
||||
baseDomain: # the base domain from which the registry will be served, e.g. registry.google
|
||||
|
||||
gSuite:
|
||||
domainName: # Your G Suite domain name
|
||||
adminAccountEmailAddress: # An admin login for your G Suite account
|
||||
domainName: # Your GSuit domain name, likely same as baseDomain above
|
||||
adminAccountEmailAddress: # An admin login for your GSuite account
|
||||
|
||||
auth:
|
||||
allowedServiceAccountEmails:
|
||||
- # a list of service account emails given access to Nomulus
|
||||
oauthClientId: # the client ID of the Identity-Aware Proxy
|
||||
|
||||
cloudSql:
|
||||
jdbcUrl: # path to the Postgres server
|
||||
|
||||
```
|
||||
|
||||
For fully-featured production environments that need the full range of features
|
||||
(e.g. RDE, correct contact information on the registrar console, etc.) you will
|
||||
need to specify more settings.
|
||||
need to specify *many* more settings.
|
||||
|
||||
From a code perspective, all configuration settings ultimately come through the
|
||||
[`RegistryConfig`][registry-config] class. This includes a Dagger module called
|
||||
`ConfigModule` that provides injectable configuration options. While most
|
||||
configuration options can be changed from within the yaml config file, certain
|
||||
derived options may still need to be overriden by changing the code in this
|
||||
derived options may still need to be overridden by changing the code in this
|
||||
module.
|
||||
|
||||
## OAuth 2 client id configuration
|
||||
## OAuth 2 client ID configuration
|
||||
|
||||
The open source Nomulus release uses OAuth 2 to authenticate and authorize
|
||||
users. This includes the `nomulus` tool when it connects to the system to
|
||||
execute commands. OAuth must be configured before you can use the `nomulus` tool
|
||||
to set up the system.
|
||||
Nomulus uses OAuth 2 to authenticate and authorize users. This includes the
|
||||
`nomulus` [command-line tool](admin-tool.md) when it connects to the system to
|
||||
execute commands as well as the
|
||||
[Identity-Aware Proxy](https://pantheon.corp.google.com/security/iap) used to
|
||||
authenticate standard requests. OAuth must be configured before you can use
|
||||
either system.
|
||||
|
||||
OAuth defines the concept of a *client id*, which identifies the application
|
||||
OAuth defines the concept of a *client ID*, which identifies the application
|
||||
which the user wants to authorize. This is so that, when a user clicks in an
|
||||
OAuth permission dialog and grants access to data, they are not granting access
|
||||
to every application on their computer (including potentially malicious ones),
|
||||
but only to the application which they agree needs access. Each environment of
|
||||
the Nomulus system should have its own client id. Multiple installations of the
|
||||
`nomulus` tool application can share the same client id for the same
|
||||
environment.
|
||||
the Nomulus system should have its own pair of client IDs. Multiple
|
||||
installations of the `nomulus` tool application can share the same client ID for
|
||||
the same environment.
|
||||
|
||||
There are three steps to configuration.
|
||||
For the Nomulus tool OAuth configuration, do the following steps:
|
||||
|
||||
* **Create the client id in App Engine:** Go to your project's
|
||||
* **Create the registry tool client ID in GCP:** Go to your project's
|
||||
["Credentials" page](https://console.developers.google.com/apis/credentials)
|
||||
in the Developer's Console. Click "Create credentials" and select "OAuth
|
||||
client ID" from the dropdown. In the create credentials window, select an
|
||||
application type of "Desktop app". After creating the client id, copy the
|
||||
client id and client secret which are displayed in the popup window. You may
|
||||
also obtain this information by downloading the json file for the client id.
|
||||
application type of "Desktop app". After creating the client ID, copy the
|
||||
client ID and client secret which are displayed in the popup window. You may
|
||||
also obtain this information by downloading the JSON file for the client ID
|
||||
|
||||
* **Copy the client secret information to the config file:** The *client
|
||||
secret file* contains both the client ID and the client secret. Copy the
|
||||
@@ -129,18 +117,21 @@ There are three steps to configuration.
|
||||
`registryTool` section. This will make the `nomulus` tool use this
|
||||
credential to authenticate itself to the system.
|
||||
|
||||
* **Add the new client id to the configured list of allowed client ids:** The
|
||||
configuration files include an `oAuth` section, which defines a parameter
|
||||
called `allowedOauthClientIds`, specifying a list of client ids which are
|
||||
permitted to connect. Add the client ID to the list. You will need to
|
||||
rebuild and redeploy the project so that the configuration changes take
|
||||
effect.
|
||||
For IAP configuration, do the following steps: * **Create the IAP client ID:**
|
||||
Follow similar steps from above to create an additional OAuth client ID, but
|
||||
using an application type of "Web application". Note the client ID and secret. *
|
||||
**Enable IAP for your HTTPS load balancer:** On the
|
||||
[IAP page](https://pantheon.corp.google.com/security/iap), enable IAP for all of
|
||||
the backend services that all use the same HTTPS load balancer. * **Use a custom
|
||||
OAuth configuration:** For the backend services, under the "Settings" section
|
||||
(in the three-dot menu) enable custom OAuth and insert the client ID and secret
|
||||
that we just created * **Save the client ID:** In the configuration file, save
|
||||
the client ID as `oauthClientId` in the `auth` section
|
||||
|
||||
Once these steps are taken, the `nomulus` tool will use a client id which the
|
||||
server is configured to accept, and authentication should succeed. Note that
|
||||
many Nomulus commands also require that the user have App Engine admin
|
||||
privileges, meaning that the user needs to be added as an owner or viewer of the
|
||||
App Engine project.
|
||||
Once these steps are taken, the `nomulus` tool and IAP will both use client IDs
|
||||
which the server is configured to accept, and authentication should succeed.
|
||||
Note that many Nomulus commands also require that the user have GCP admin
|
||||
privileges on the project in question.
|
||||
|
||||
## Sensitive global configuration
|
||||
|
||||
@@ -151,8 +142,8 @@ control mishap. We use a secret store to persist these values in a secure
|
||||
manner, which is backed by the GCP Secret Manager.
|
||||
|
||||
The `Keyring` interface contains methods for all sensitive configuration values,
|
||||
which are primarily credentials used to access various ICANN and ICANN-
|
||||
affiliated services (such as RDE). These values are only needed for real
|
||||
which are primarily credentials used to access various ICANN and
|
||||
ICANN-affiliated services (such as RDE). These values are only needed for real
|
||||
production registries and PDT environments. If you are just playing around with
|
||||
the platform at first, it is OK to put off defining these values until
|
||||
necessary. This allows the codebase to start and run, but of course any actions
|
||||
@@ -169,16 +160,16 @@ ${KEY_NAME}`.
|
||||
|
||||
## Per-TLD configuration
|
||||
|
||||
`Tld` entities, which are persisted to the database, are used for per-TLD
|
||||
configuration. They contain any kind of configuration that is specific to a TLD,
|
||||
such as the create/renew price of a domain name, the pricing engine
|
||||
implementation, the DNS writer implementation, whether escrow exports are
|
||||
enabled, the default currency, the reserved label lists, and more. The `nomulus
|
||||
update_tld` command is used to set all of these options. See the
|
||||
[admin tool documentation](./admin-tool.md) for more information, as well as the
|
||||
command-line help for the `update_tld` command. Unlike global configuration
|
||||
above, per-TLD configuration options are stored as data in the running system,
|
||||
and thus do not require code pushes to update.
|
||||
`Tld` entities, which are persisted to the database and stored in YAML files,
|
||||
are used for per-TLD configuration. They contain any kind of configuration that
|
||||
is specific to a TLD, such as the create/renew price of a domain name, the
|
||||
pricing engine implementation, the DNS writer implementation, whether escrow
|
||||
exports are enabled, the default currency, the reserved label lists, and more.
|
||||
|
||||
To create or update TLDs, we use
|
||||
[YAML files](operational-procedures/modifying-tlds.md) and the `nomulus
|
||||
configure_tld` command. Because the TLDs are stored as data in the running
|
||||
system, they do not require code pushes to update.
|
||||
|
||||
[app-engine-config]: https://cloud.google.com/appengine/docs/java/configuration-files
|
||||
[default-config]: https://github.com/google/nomulus/blob/master/java/google/registry/config/files/default-config.yaml
|
||||
@@ -242,7 +233,7 @@ connectionName: your-project:us-central1:nomulus
|
||||
|
||||
Use the `update_keyring_secret` command to update the `SQL_PRIMARY_CONN_NAME`
|
||||
key with the connection name. If you have created a read-replica, update the
|
||||
`SQL_REPLICA_CONN_NAME` key with the replica's connection time.
|
||||
`SQL_REPLICA_CONN_NAME` key with the replica's connection name.
|
||||
|
||||
### Installing the Schema
|
||||
|
||||
@@ -334,6 +325,17 @@ $ gcloud sql connect nomulus --user=nomulus
|
||||
From this, you should have a postgres prompt and be able to enter the "GRANT"
|
||||
command specified above.
|
||||
|
||||
### Replication and Backups
|
||||
|
||||
We highly recommend creating a read-only replica of the database and using the
|
||||
previously-mentioned `SQL_REPLICA_CONN_NAME` value in the keyring to the name of
|
||||
that replica. By doing so, you can remove some load from the primary database.
|
||||
|
||||
We also recommend enabling
|
||||
[point-in-time recovery](https://docs.cloud.google.com/sql/docs/postgres/backup-recovery/pitr)
|
||||
for the instance, just in case something bad happens and you need to restore
|
||||
from a backup.
|
||||
|
||||
### Cloud SecretManager
|
||||
|
||||
You'll need to enable the SecretManager API in your project.
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
# Developing
|
||||
|
||||
This document contains advice on how to do development on the Nomulus codebase,
|
||||
including how to set up an IDE environment and run tests.
|
||||
|
||||
## Running a local development server
|
||||
|
||||
`RegistryTestServer` is a lightweight test server for the registry that is
|
||||
suitable for running locally for development. It uses local versions of all
|
||||
Google Cloud Platform dependencies, when available. Correspondingly, its
|
||||
functionality is limited compared to a Nomulus instance running on an actual App
|
||||
Engine instance. It is most helpful for doing web UI development such as on the
|
||||
registrar console: it allows you to update JS, CSS, images, and other front-end
|
||||
resources, and see the changes instantly simply by refreshing the relevant page
|
||||
in your browser.
|
||||
|
||||
To see the registry server's command-line parameters, run:
|
||||
|
||||
```shell
|
||||
$ bazel run //javatests/google/registry/server -- --help
|
||||
```
|
||||
|
||||
To start an instance of the server, run:
|
||||
|
||||
```shell
|
||||
$ bazel run //javatests/google/registry/server {your params}
|
||||
```
|
||||
|
||||
Once it is running, you can interact with it via normal `nomulus` commands, or
|
||||
view the registrar console in a web browser by navigating to
|
||||
[http://localhost:8080/registrar](http://localhost:8080/registrar). The server
|
||||
will continue running until you terminate the process.
|
||||
|
||||
If you are adding new URL paths, or new directories of web-accessible resources,
|
||||
you will need to make the corresponding changes in `RegistryTestServer`. This
|
||||
class contains all of the routing and static file information used by the local
|
||||
development server.
|
||||
@@ -3,8 +3,8 @@
|
||||
This document covers the first steps of creating some test entities in a newly
|
||||
deployed and configured testing environment. It isn't required, but it does help
|
||||
gain familiarity with the system. If you have not already done so, you must
|
||||
first complete [installation](./install.md) and [initial
|
||||
configuration](./configuration.md).
|
||||
first complete [installation](./install.md) and
|
||||
[initial configuration](./configuration.md).
|
||||
|
||||
Note: Do not create these entities on a production environment! All commands
|
||||
below use the [`nomulus` admin tool](./admin-tool.md) to interact with the
|
||||
@@ -12,55 +12,30 @@ running registry system. We'll assume that all commands below are running in the
|
||||
`alpha` environment; if you named your environment differently, then use that
|
||||
everywhere that `alpha` appears.
|
||||
|
||||
## Temporary extra steps
|
||||
|
||||
Using the `nomulus` admin tool currently requires two additional steps to enable
|
||||
full functionality. These steps should _not_ be done for a production
|
||||
deployment - a suitable solution for production is in progress.
|
||||
|
||||
Modify the `tools` module `web.xml` file to remove admin-only restrictions.
|
||||
Look for the `<auth-constraint>admin</auth-constraint>` element. Comment out
|
||||
this element, and redeploy the tools module to your live app.
|
||||
|
||||
[app-default-creds]: https://developers.google.com/identity/protocols/application-default-credentials
|
||||
|
||||
## Create a TLD
|
||||
|
||||
Pick the name of a TLD to create. For the purposes of this example we'll use
|
||||
"example", which conveniently happens to be an ICANN reserved string, meaning
|
||||
it'll never be created for real on the Internet at large.
|
||||
it'll never be created for real on the Internet at large. Then,
|
||||
[create a TLD](operational-procedures/modifying-tlds.md) using the
|
||||
[example template](https://github.com/google/nomulus/blob/master/core/src/test/resources/google/registry/tools/tld.yaml)
|
||||
as a guide.
|
||||
|
||||
The fields you'll want to change from the template: * `driveFolderId` should be
|
||||
null * `roidSuffix` should be `EXAMPLE` -- this is the suffix that will be used
|
||||
for repository ids of domains on the TLD. This suffix must be all uppercase and
|
||||
a maximum of eight ASCII characters and can be set to the upper-case equivalent
|
||||
of our TLD name (if it is 8 characters or fewer), such as "EXAMPLE." You can
|
||||
also abbreviate the upper-case TLD name down to 8 characters. Refer to the
|
||||
[gTLD Registry Advisory: Correction of non-compliant ROIDs][roids] for further
|
||||
information. * `tldStr` should be `example` * `tldType` should be `TEST`, which
|
||||
identifies that the TLD is for testing purposes, whereas `REAL` would identify
|
||||
the TLD as a live TLD
|
||||
|
||||
```shell
|
||||
$ nomulus -e alpha create_tld example --roid_suffix EXAMPLE \
|
||||
--initial_tld_state GENERAL_AVAILABILITY --tld_type TEST \
|
||||
--dns_writers VoidDnsWriter
|
||||
[ ... snip confirmation prompt ... ]
|
||||
Perform this command? (y/N): y
|
||||
Updated 1 entities.
|
||||
$ nomulus -e alpha configure_tld --input=example.yaml
|
||||
```
|
||||
|
||||
* `-e` is the environment name (`alpha` in this example).
|
||||
* `create_tld` is the subcommand to create a TLD. The TLD name is "example"
|
||||
which happens to be an ICANN reserved string, and therefore "example" can
|
||||
never be created on the Internet at large.
|
||||
* `--initial_tld_state` defines the initial state of the TLD.
|
||||
`GENERAL_AVAILABILITY`, in the case of our example, allows you to
|
||||
immediately create domain names by bypassing the sunrise and landrush domain
|
||||
registration periods.
|
||||
* `--tld_type` is the type of TLD. `TEST` identifies that the TLD is for
|
||||
testing purposes, where `REAL` identifies the TLD is a live TLD.
|
||||
* `roid_suffix` is the suffix that will be used for repository ids of domains
|
||||
on the TLD. This suffix must be all uppercase and a maximum of eight ASCII
|
||||
characters and can be set to the upper-case equivalent of our TLD name (if
|
||||
it is 8 characters or fewer), such as "EXAMPLE." You can also abbreviate the
|
||||
upper-case TLD name down to 8 characters. Refer to the [gTLD Registry
|
||||
Advisory: Correction of non-compliant ROIDs][roids] for further information.
|
||||
* `--dns_writers` is the list of DNS writer modules that specify how changes
|
||||
to domains for the TLD are communicated to actual DNS servers. We use
|
||||
`VoidDnsWriter` in this case so as to not have to set up DNS. Typically
|
||||
one might use CloudDnsWriter (for Google Cloud DNS) or implement your own
|
||||
solution.
|
||||
|
||||
## Create a registrar
|
||||
|
||||
Now we need to create a registrar and give it access to operate on the example
|
||||
@@ -96,36 +71,6 @@ Where:
|
||||
* `--allowed_tlds` is a comma-delimited list of top level domains where this
|
||||
registrar has access.
|
||||
|
||||
## Create a contact
|
||||
|
||||
Now we want to create a contact, as a contact is required before a domain can be
|
||||
created. Contacts can be used on any number of domains across any number of
|
||||
TLDs, and contain the information on who owns or provides technical support for
|
||||
a TLD. These details will appear in WHOIS queries.
|
||||
|
||||
```shell
|
||||
$ nomulus -e alpha create_contact -c acme --id abcd1234 \
|
||||
--name 'John Smith' --street '234 Fake St' --city 'North Fakington' \
|
||||
--state MA --zip 23456 --cc US --email jsmith@e.mail
|
||||
[ ... snip EPP response ... ]
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
* `create_contact` is the subcommand to create a contact.
|
||||
* `-c` is used to define the registrar. The `-c` option is used with most
|
||||
`registry_tool` commands to specify the id of the registrar executing the
|
||||
command. Contact, domain, and host creation all work by constructing an EPP
|
||||
message that is sent to the registry, and EPP commands need to run under the
|
||||
context of a registrar. The "acme" registrar that was created above is used
|
||||
for this purpose.
|
||||
* `--id` is the contact id, and is referenced elsewhere in the system (e.g.
|
||||
when a domain is created and the admin contact is specified).
|
||||
* `--name` is the display name of the contact, which is usually the name of a
|
||||
company or of a person.
|
||||
|
||||
The address and `email` fields are required to create a contact.
|
||||
|
||||
## Create a host
|
||||
|
||||
Hosts are used to specify the IP addresses (either v4 or v6) that are associated
|
||||
@@ -156,8 +101,7 @@ To tie it all together, let's create a domain name that uses the above contact
|
||||
and host.
|
||||
|
||||
```shell
|
||||
$ nomulus -e alpha create_domain fake.example --client acme --admins abcd1234 \
|
||||
--techs abcd1234 --registrant abcd1234 --nameservers ns1.google.com
|
||||
$ nomulus -e alpha create_domain fake.example --client acme --nameservers ns1.google.com
|
||||
[ ... snip EPP response ... ]
|
||||
```
|
||||
|
||||
@@ -166,26 +110,19 @@ Where:
|
||||
* `create_domain` is the subcommand to create a domain name. It accepts a
|
||||
whitespace-separated list of domain names to be created
|
||||
* `--client` is used to define the registrar.
|
||||
* `--admins` is the administrative contact's id(s).
|
||||
* `--techs` is the technical contact's id(s).
|
||||
* `--registrant` is the registrant contact's id.
|
||||
* `--nameservers` is a comma-separated list of hosts.
|
||||
|
||||
Note how the same contact id is used for the administrative, technical, and
|
||||
registrant contact. It is common for domain names to use the same details for
|
||||
all contacts on a domain name.
|
||||
## Verify test entities using RDAP
|
||||
|
||||
## Verify test entities using WHOIS
|
||||
|
||||
To verify that everything worked, let's query the WHOIS information for
|
||||
To verify that everything worked, let's query the RDAP information for
|
||||
fake.example:
|
||||
|
||||
```shell
|
||||
$ nomulus -e alpha whois_query fake.example
|
||||
[ ... snip WHOIS response ... ]
|
||||
$ nomulus -e alpha rdap_query fake.example
|
||||
[ ... snip RDAP response ... ]
|
||||
```
|
||||
|
||||
You should see all of the information in WHOIS that you entered above for the
|
||||
contact, nameserver, and domain.
|
||||
You should see all the information in RDAP that you entered above for the
|
||||
nameserver and domain.
|
||||
|
||||
[roids]: https://www.icann.org/resources/pages/correction-non-compliant-roids-2015-08-26-en
|
||||
|
||||
@@ -22,15 +22,17 @@ To upgrade to a new Gradle version for this project, use:
|
||||
gradle wrapper --gradle-version version-number
|
||||
```
|
||||
|
||||
## Deploy to AppEngine
|
||||
## Deploy to GCP Test Projects
|
||||
|
||||
Use the Gradle task 'appengineDeploy' to build and deploy to AppEngine. For now
|
||||
you must update the appengine.deploy.project in build.gradle to your
|
||||
GCP project ID.
|
||||
If your [configuration](configuration.md) is up to date with the proper test
|
||||
projects configured, you can deploy to GCP through the Gradle command line.
|
||||
|
||||
To deploy the Gradle build, you will need the Google Cloud SDK and its
|
||||
app-engine-java component.
|
||||
Use the Gradle task `deployNomulus` to build and deploy to a GCP test project
|
||||
providing the test project as an argument, e.g.
|
||||
|
||||
```shell
|
||||
./gradlew deployNomulus -Penvironment=alpha
|
||||
```
|
||||
|
||||
### Notable Issues
|
||||
|
||||
@@ -40,13 +42,8 @@ is easier to exclude the suite classes than individual test classes. This is the
|
||||
reason why all test tasks in the :core project contain the exclude pattern
|
||||
'"**/*TestCase.*", "**/*TestSuite.*"'
|
||||
|
||||
Many Nomulus tests are not hermetic: they modify global state, but do not clean
|
||||
Some Nomulus tests are not hermetic: they modify global state, but do not clean
|
||||
up on completion. This becomes a problem with Gradle. In the beginning we forced
|
||||
Gradle to run every test class in a new process, and incurred heavy overheads.
|
||||
Since then, we have fixed some tests, and manged to divide all tests into three
|
||||
suites that do not have intra-suite conflicts. We will revisit the remaining
|
||||
tests soon.
|
||||
|
||||
Note that it is unclear if all conflicting tests have been identified. More may
|
||||
be exposed if test execution order changes, e.g., when new tests are added or
|
||||
execution parallelism level changes.
|
||||
Since then, we have fixed some tests, and manged to divide all tests into two
|
||||
suites that do not have intra-suite conflicts (`fragileTest` and `standardTest`)
|
||||
|
||||
140
docs/install.md
@@ -6,48 +6,41 @@ This document covers the steps necessary to download, build, and deploy Nomulus.
|
||||
|
||||
You will need the following programs installed on your local machine:
|
||||
|
||||
* A recent version of the [Java 11 JDK][java-jdk11].
|
||||
* [Google App Engine SDK for Java][app-engine-sdk], and configure aliases to the `gcloud` and `appcfg.sh` utilities (
|
||||
you'll use them a lot).
|
||||
* [Git](https://git-scm.com/) version control system.
|
||||
* Docker (confirm with `docker info` no permission issues, use `sudo groupadd docker` for sudoless docker).
|
||||
* Python version 3.7 or newer.
|
||||
* gnupg2 (e.g. in run `sudo apt install gnupg2` in Debian-like Linuxes)
|
||||
* A recent version of the [Java 21 JDK][java-jdk21].
|
||||
* The [Google Cloud CLI](https://docs.cloud.google.com/sdk/docs/install-sdk)
|
||||
(configure an alias to the `gcloud`utility, because you'll use it a lot)
|
||||
* [Git](https://git-scm.com/) version control system.
|
||||
* Docker (confirm with `docker info` no permission issues, use `sudo groupadd
|
||||
docker` for sudoless docker).
|
||||
* Python version 3.7 or newer.
|
||||
* gnupg2 (e.g. in run `sudo apt install gnupg2` in Debian-like Linuxes)
|
||||
|
||||
**Note:** App Engine does not yet support Java 9. Also, the instructions in this
|
||||
document have only been tested on Linux. They might work with some alterations
|
||||
on other operating systems.
|
||||
**Note:** The instructions in this document have only been tested on Linux. They
|
||||
might work with some alterations on other operating systems.
|
||||
|
||||
## Download the codebase
|
||||
|
||||
Start off by using git to download the latest version from the [Nomulus GitHub
|
||||
page](https://github.com/google/nomulus). You may checkout any of the daily
|
||||
tagged versions (e.g. `nomulus-20200629-RC00`), but in general it is also
|
||||
safe to simply checkout from HEAD:
|
||||
Start off by using git to download the latest version from the
|
||||
[Nomulus GitHub page](https://github.com/google/nomulus). You may check out any
|
||||
of the daily tagged versions (e.g. `nomulus-20260101-RC00`), but in general it
|
||||
is also safe to simply check out from HEAD:
|
||||
|
||||
```shell
|
||||
$ git clone git@github.com:google/nomulus.git
|
||||
Cloning into 'nomulus'...
|
||||
[ .. snip .. ]
|
||||
$ cd nomulus
|
||||
$ ls
|
||||
apiserving CONTRIBUTORS java LICENSE scripts
|
||||
AUTHORS docs javascript python third_party
|
||||
CONTRIBUTING.md google javatests README.md WORKSPACE
|
||||
```
|
||||
|
||||
Most of the directory tree is organized into gradle sub-projects (see
|
||||
`settings.gradle` for details). The following other top-level directories are
|
||||
Most of the directory tree is organized into gradle subprojects (see
|
||||
`settings.gradle` for details). The following other top-level directories are
|
||||
also defined:
|
||||
|
||||
* `buildSrc` -- Gradle extensions specific to our local build and release
|
||||
methodology.
|
||||
* `config` -- Tools for build and code hygiene.
|
||||
* `docs` -- The documentation (including this install guide)
|
||||
* `gradle` -- Configuration and code managed by the gradle build system.
|
||||
* `gradle` -- Configuration and code managed by the Gradle build system.
|
||||
* `integration` -- Testing scripts for SQL changes.
|
||||
* `java-format` -- The Google java formatter and wrapper scripts to use it
|
||||
incrementally.
|
||||
* `python` -- Some Python reporting scripts
|
||||
* `release` -- Configuration for our continuous integration process.
|
||||
|
||||
## Build the codebase
|
||||
@@ -56,34 +49,29 @@ The first step is to build the project, and verify that this completes
|
||||
successfully. This will also download and install dependencies.
|
||||
|
||||
```shell
|
||||
$ ./nom_build build
|
||||
$ ./gradlew build
|
||||
Starting a Gradle Daemon (subsequent builds will be faster)
|
||||
Plugins: Using default repo...
|
||||
|
||||
> Configure project :buildSrc
|
||||
Java dependencies: Using Maven central...
|
||||
[ .. snip .. ]
|
||||
```
|
||||
|
||||
The `nom_build` script is just a wrapper around `gradlew`. Its main
|
||||
additional value is that it formalizes the various properties used in the
|
||||
build as command-line flags.
|
||||
The "build" command builds all the code and runs all the tests. This will take a
|
||||
while.
|
||||
|
||||
The "build" command builds all of the code and runs all of the tests. This
|
||||
will take a while.
|
||||
## Create and configure a GCP project
|
||||
|
||||
## Create an App Engine project
|
||||
|
||||
First, [create an
|
||||
application](https://cloud.google.com/appengine/docs/java/quickstart) on Google
|
||||
Cloud Platform. Make sure to choose a good Project ID, as it will be used
|
||||
repeatedly in a large number of places. If your company is named Acme, then a
|
||||
good Project ID for your production environment would be "acme-registry". Keep
|
||||
First,
|
||||
[create an application](https://cloud.google.com/appengine/docs/java/quickstart)
|
||||
on Google Cloud Platform. Make sure to choose a good Project ID, as it will be
|
||||
used repeatedly in a large number of places. If your company is named Acme, then
|
||||
a good Project ID for your production environment would be "acme-registry". Keep
|
||||
in mind that project IDs for non-production environments should be suffixed with
|
||||
the name of the environment (see the [Architecture
|
||||
documentation](./architecture.md) for more details). For the purposes of this
|
||||
example we'll deploy to the "alpha" environment, which is used for developer
|
||||
testing. The Project ID will thus be `acme-registry-alpha`.
|
||||
the name of the environment (see the
|
||||
[Architecture documentation](./architecture.md) for more details). For the
|
||||
purposes of this example we'll deploy to the "alpha" environment, which is used
|
||||
for developer testing. The Project ID will thus be `acme-registry-alpha`.
|
||||
|
||||
Now log in using the command-line Google Cloud Platform SDK and set the default
|
||||
project to be this one that was newly created:
|
||||
@@ -96,6 +84,17 @@ You are now logged in as [user@email.tld].
|
||||
$ gcloud config set project acme-registry-alpha
|
||||
```
|
||||
|
||||
And make sure the required APIs are enabled in the project:
|
||||
|
||||
```shell
|
||||
$ gcloud services enable \
|
||||
container.googleapis.com \
|
||||
artifactregistry.googleapis.com \
|
||||
sqladmin.googleapis.com \
|
||||
secretmanager.googleapis.com \
|
||||
compute.googleapis.com
|
||||
```
|
||||
|
||||
Now modify `projects.gradle` with the name of your new project:
|
||||
|
||||
<pre>
|
||||
@@ -106,42 +105,51 @@ rootProject.ext.projects = ['production': 'your-production-project',
|
||||
'crash' : 'your-crash-project']
|
||||
</pre>
|
||||
|
||||
Next follow the steps in [configuration](./configuration.md) to configure the
|
||||
complete system or, alternately, read on for an initial deploy in which case
|
||||
you'll need to deploy again after configuration.
|
||||
#### Create GKE Clusters
|
||||
|
||||
## Deploy the code to App Engine
|
||||
We recommend Standard clusters with Workload Identity enabled to allow pods to
|
||||
securely access Cloud SQL and Secret Manager. Feel free to adjust the numbers
|
||||
and sizing as desired.
|
||||
|
||||
AppEngine deployment with gradle is straightforward:
|
||||
```shell
|
||||
$ gcloud container clusters create nomulus-cluster \
|
||||
--region=$REGION \
|
||||
--workload-pool=$PROJECT_ID.svc.id.goog \
|
||||
--num-nodes=3 \
|
||||
--enable-ip-alias
|
||||
$ gcloud container clusters create proxy-cluster \
|
||||
--region=$REGION \
|
||||
--workload-pool=$PROJECT_ID.svc.id.goog \
|
||||
--num-nodes=3 \
|
||||
--enable-ip-alias
|
||||
```
|
||||
|
||||
$ ./nom_build appengineDeploy --environment=alpha
|
||||
Then create an artifact repository: `shell $ gcloud artifacts repositories
|
||||
create nomulus-repo \ --repository-format=docker \ --location=$REGION \
|
||||
--description="Nomulus Docker images"`
|
||||
|
||||
To verify successful deployment, visit
|
||||
https://acme-registry-alpha.appspot.com/registrar in your browser (adjusting
|
||||
appropriately for the project ID that you actually used). If the project
|
||||
deployed successfully, you'll see a "You need permission" page indicating that
|
||||
you need to configure the system and grant access to your Google account. It's
|
||||
time to go to the next step, configuration.
|
||||
See the files and documentation in the `release/` folder for more information on
|
||||
the release process. You will likely need to customize the internal build
|
||||
process for your own setup, including internal repository management, builds,
|
||||
and where Nomulus is deployed.
|
||||
|
||||
Configuration is handled by editing code, rebuilding the project, and deploying
|
||||
again. See the [configuration guide](./configuration.md) for more details.
|
||||
Once you have completed basic configuration (including most critically the
|
||||
project ID, client id and secret in your copy of the `nomulus-config-*.yaml`
|
||||
files), you can rebuild and start using the `nomulus` tool to create test
|
||||
entities in your newly deployed system. See the [first steps tutorial](./first-steps-tutorial.md)
|
||||
again. See the [configuration guide](./configuration.md) for more details. Once
|
||||
you have completed basic configuration (including most critically the project
|
||||
ID, client id and secret in your copy of the `nomulus-config-*.yaml` files), you
|
||||
can rebuild and start using the `nomulus` tool to create test entities in your
|
||||
newly deployed system. See the [first steps tutorial](./first-steps-tutorial.md)
|
||||
for more information.
|
||||
|
||||
[app-engine-sdk]: https://cloud.google.com/appengine/docs/java/download
|
||||
[java-jdk11]: https://www.oracle.com/java/technologies/javase-downloads.html
|
||||
[java-jdk21]: https://www.oracle.com/java/technologies/javase-downloads.html
|
||||
|
||||
## Deploy the BEAM Pipelines
|
||||
## Deploy the Beam Pipelines
|
||||
|
||||
Nomulus is in the middle of migrating all pipelines to use flex-template. For
|
||||
pipelines already based on flex-template, deployment in the testing environments
|
||||
Deployment of the Beam pipelines to Cloud Dataflow in the testing environments
|
||||
(alpha and crash) can be done using the following command:
|
||||
|
||||
```shell
|
||||
./nom_build :core:stageBeamPipelines --environment=alpha
|
||||
./gradlew :core:stageBeamPipelines -Penvironment=alpha
|
||||
```
|
||||
|
||||
Pipeline deployment in other environments are through CloudBuild. Please refer
|
||||
|
||||
34
docs/local-testing.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Local Testing
|
||||
|
||||
## Running a local development server
|
||||
|
||||
Nomulus provides a `RegistryTestServer` that is a lightweight test server
|
||||
suitable for running local development. It uses local versions of all Google
|
||||
Cloud Platform dependencies when available. Correspondingly, it is primarily
|
||||
useful for doing web UI development (i.e. the registrar console). It allows you
|
||||
to update Typescript, HTML, and CSS and see the changes simply by refreshing the
|
||||
relevant page in your browser.
|
||||
|
||||
In order to serve content locally, there are two services that must be run: *
|
||||
the `RegistryTestServer` to serve as the backing server * the Angular service to
|
||||
provide the UI files
|
||||
|
||||
In order to do this in one step, from the `console-webapp` folder, run:
|
||||
|
||||
```shell
|
||||
$ npm install
|
||||
$ npm run start:dev
|
||||
```
|
||||
|
||||
This will start both the `RegistryTestServer` and the Angular testing service.
|
||||
Any changes to Typescript/HTML/CSS files will be recompiled and available on
|
||||
page reload.
|
||||
|
||||
One it is running, you can interact with the console by going to
|
||||
`http://localhost:4200` to view the registrar console in a web browser. The
|
||||
server will continue running until you terminate the process.
|
||||
|
||||
If you are adding new URL paths, or new directories of web-accessible resources,
|
||||
you will need to make the corresponding changes in `RegistryTestServer`. This
|
||||
class contains all the routing and static file information used by the local
|
||||
development server.
|
||||
@@ -7,7 +7,7 @@ production registry system.
|
||||
|
||||
[Stackdriver Monitoring](https://cloud.google.com/monitoring/docs/) is used to
|
||||
instrument internal state within the Nomulus internal environment. This is
|
||||
broadly called white-box monitoring. EPP, DNS, and WHOIS are instrumented. The
|
||||
broadly called white-box monitoring. EPP, DNS, and RDAP are instrumented. The
|
||||
metrics monitored are as follows:
|
||||
|
||||
* `/custom/dns/publish_domain_requests` -- A count of publish domain requests,
|
||||
@@ -21,29 +21,27 @@ metrics monitored are as follows:
|
||||
* `/custom/epp/processing_time` -- A [Distribution][distribution] representing
|
||||
the processing time for EPP requests, described by command name, client id,
|
||||
and return status code.
|
||||
* `/custom/whois/requests` -- A count of WHOIS requests, described by command
|
||||
* `/custom/rdap/requests` -- A count of RDAP requests, described by command
|
||||
name, number of returned results, and return status code.
|
||||
* `/custom/whois/processing_time` -- A [Distribution][distribution]
|
||||
representing the processing time for WHOIS requests, described by command
|
||||
* `/custom/rdap/processing_time` -- A [Distribution][distribution]
|
||||
representing the processing time for RDAP requests, described by command
|
||||
name, number of returned results, and return status code.
|
||||
|
||||
Follow the guide to [set up a Stackdriver
|
||||
account](https://cloud.google.com/monitoring/accounts/guide) and associate it
|
||||
with the GCP project containing the Nomulus App Engine app. Once the two have
|
||||
been linked, monitoring will start automatically. For now, because the
|
||||
Follow the guide to
|
||||
[set up a Stackdriver account](https://cloud.google.com/monitoring/accounts/guide)
|
||||
and associate it with the GCP project containing the Nomulus app. Once the two
|
||||
have been linked, monitoring will start automatically. For now, because the
|
||||
visualization of custom metrics in Stackdriver is embryronic, you can retrieve
|
||||
and visualize the collected metrics with a script, as described in the guide on
|
||||
[Reading Time
|
||||
Series](https://cloud.google.com/monitoring/custom-metrics/reading-metrics) and
|
||||
the [custom metric code
|
||||
sample](https://github.com/GoogleCloudPlatform/python-docs-samples/blob/master/monitoring/api/v3/custom_metric.py).
|
||||
[Reading Time Series](https://cloud.google.com/monitoring/custom-metrics/reading-metrics)
|
||||
and the
|
||||
[custom metric code sample](https://github.com/GoogleCloudPlatform/python-docs-samples/blob/master/monitoring/api/v3/custom_metric.py).
|
||||
|
||||
In addition to the included white-box monitoring, black-box monitoring should be
|
||||
set up to exercise the functionality of the registry platform as a user would
|
||||
see it. This monitoring should, for example, create a new domain name every few
|
||||
minutes via EPP and then verify that the domain exists in DNS and WHOIS. For
|
||||
now, no black-box monitoring implementation is provided with the Nomulus
|
||||
platform.
|
||||
minutes via EPP and then verify that the domain exists in DNS and RDAP. For now,
|
||||
no black-box monitoring implementation is provided with the Nomulus platform.
|
||||
|
||||
## Updating cursors
|
||||
|
||||
@@ -80,11 +78,11 @@ scripts.
|
||||
|
||||
[RDE](https://newgtlds.icann.org/en/applicants/data-escrow) is a daily deposit
|
||||
of the contents of the registry, sent to a third-party escrow provider. The
|
||||
details are contained in Specification 2 of the [registry
|
||||
agreement][registry-agreement].
|
||||
details are contained in Specification 2 of the
|
||||
[registry agreement][registry-agreement].
|
||||
|
||||
Nomulus provides [code to generate and send these
|
||||
deposits](./operational-procedures/rde-deposits.md).
|
||||
Nomulus provides
|
||||
[code to generate and send these deposits](./operational-procedures/rde-deposits.md).
|
||||
|
||||
### Monthly registry activity and transaction reporting
|
||||
|
||||
@@ -92,8 +90,8 @@ ICANN requires monthly activity and transaction reporting. The details are
|
||||
contained in Specification 3 of the [registry agreement][registry-agreement].
|
||||
|
||||
These reports are mostly generated by querying the Cloud SQL database. There is
|
||||
currently a Google proprietary class to query DNS related activities that is
|
||||
not included in the open source Nomulus release.
|
||||
currently a Google proprietary class to query DNS related activities that is not
|
||||
included in the open source Nomulus release.
|
||||
|
||||
### Zone File Access (ZFA)
|
||||
|
||||
@@ -102,27 +100,29 @@ information. The details are contained in part 2 of Specification 4 of the
|
||||
[registry agreement][registry-agreement].
|
||||
|
||||
This information will come from the DNS server, rather than Nomulus itself, so
|
||||
ZFA is not part of the Nomulus release.
|
||||
ZFA is not directly part of the Nomulus release.
|
||||
|
||||
### Bulk Registration Data Access (BRDA)
|
||||
|
||||
BRDA is a weekly archive of the contents of the registry. The details are
|
||||
contained in part 3 of Specification 4 of the [registry
|
||||
agreement][registry-agreement].
|
||||
contained in part 3 of Specification 4 of the
|
||||
[registry agreement][registry-agreement].
|
||||
|
||||
ICANN uses sFTP to retrieve BRDA data from a server provided by the registry.
|
||||
Nomulus provides [code to generate these
|
||||
deposits](./operational-procedures/brda-deposits.md), but a separate sFTP server
|
||||
must be configured, and the deposits must be moved onto the server for access by
|
||||
ICANN.
|
||||
Nomulus provides
|
||||
[code to generate these deposits](./operational-procedures/brda-deposits.md),
|
||||
but a separate sFTP server must be configured, and the deposits must be moved
|
||||
onto the server for access by ICANN.
|
||||
|
||||
### Spec 11 reporting
|
||||
|
||||
[Spec 11][spec-11] reporting must be provided to ICANN as part of their
|
||||
anti-abuse efforts. This is covered in Specification 11 of the
|
||||
[registry agreement][registry-agreement], but the details are little spotty.
|
||||
|
||||
The Nomulus release does not generate these reports.
|
||||
Nomulus provides
|
||||
[code](https://github.com/google/nomulus/blob/master/core/src/main/java/google/registry/beam/spec11/Spec11Pipeline.java)
|
||||
to generate and send these reports, run on
|
||||
[a schedule](https://github.com/google/nomulus/blob/master/core/src/main/java/google/registry/config/files/tasks/cloud-scheduler-tasks-production.xml#L257-L267)
|
||||
|
||||
[distribution]: https://cloud.google.com/monitoring/api/ref_v3/rest/v3/TypedValue#Distribution
|
||||
[registry-agreement]: https://newgtlds.icann.org/sites/default/files/agreements/agreement-approved-09jan14-en.pdf
|
||||
|
||||
@@ -28,8 +28,8 @@ The BRDA copy task reads the previous file and creates two files:
|
||||
```
|
||||
|
||||
If you see an `xml.ghostryde` file but not the others, an error has occurred
|
||||
during the process. If you see the files in the
|
||||
{PROJECT-ID}-icann-brda bucket as well, the process has completed successfully.
|
||||
during the process. If you see the files in the {PROJECT-ID}-icann-brda bucket
|
||||
as well, the process has completed successfully.
|
||||
|
||||
Once the files have been created, they must be stored on an sFTP server from
|
||||
which ICANN can pull the files. The Nomulus project does not provide this last
|
||||
@@ -40,11 +40,11 @@ The cursor can be checked using the `nomulus pending_escrow` command.
|
||||
|
||||
## Generating BRDA deposits manually
|
||||
|
||||
* Get a list of "REAL" (as opposed to TEST) TLDs. Doublecheck that the command
|
||||
output doesn't contain any TLDs for tests.
|
||||
* Get a list of "REAL" (as opposed to TEST) TLDs. Double-check that the
|
||||
command output doesn't contain any TLDs for tests.
|
||||
|
||||
```shell
|
||||
$ registry-tool -e production list_tlds --fields=tldStr,tldType | grep REAL | awk '{print $1}' > realtlds.txt`
|
||||
$ nomulus -e production list_tlds --fields=tldStr,tldType | grep REAL | awk '{print $1}' > realtlds.txt`
|
||||
```
|
||||
|
||||
* Generate .ryde and .sig files of TLDs specified for given date(s) in the
|
||||
|
||||
26
docs/operational-procedures/modifying-tlds.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Creating or Modifying TLDs
|
||||
|
||||
Nomulus stores YAML representations of TLDs, in an effort to make sure that any
|
||||
(potentially significant) modifications to TLDs go through source control and
|
||||
code review. We recommend storing these TLD YAML representations in a separate
|
||||
private repository so that changes can be verified by multiple people before
|
||||
being merged
|
||||
([here is an example TLD](https://github.com/google/nomulus/blob/master/core/src/test/resources/google/registry/tools/tld.yaml))
|
||||
|
||||
Creating and updating a TLD use the same process -- the only difference is
|
||||
whether you're creating a TLD YAML file from scratch or modifying an existing
|
||||
one.
|
||||
|
||||
Similar to [premium lists](premium-list-management.md) and
|
||||
[reserved lists](reserved-list-management.md), we recommend modifying TLDs as a
|
||||
part of an automated build process after the desired changes have been merged
|
||||
into the TLD YAML files. The automated process should run:
|
||||
|
||||
```shell
|
||||
nomulus -e {ENVIRONMENT} configure_tld --build_environment --input=path/to/my/file/tld.yaml
|
||||
```
|
||||
|
||||
The `build_environment` flag signals that this is being run as part of an
|
||||
automated build process and should ideally not be used manually. There is an
|
||||
additional `--break_glass` argument that can be used in emergencies to modify
|
||||
TLDs outside a normal build process.
|
||||
@@ -69,6 +69,18 @@ Perform this command? (y/N): y
|
||||
Successfully saved premium list exampletld
|
||||
```
|
||||
|
||||
### Note:
|
||||
|
||||
We recommend only updating premium lists manually in the case of emergencies.
|
||||
Instead, we run the `update_premium_list` command (as well as `configure_tld`
|
||||
and `update_reserved_list` commands) as part of the build process after a pull
|
||||
request has been merged into the private source code repository that contains
|
||||
the files. The `--build_environment` flag is used to signal that the command is
|
||||
being run in one of those automated environments, and thus allowed to modify
|
||||
production. Without that flag, commands against production will fail.
|
||||
|
||||
This is similar to the process for [updating TLDs](modifying-tlds.md).
|
||||
|
||||
If this premium list is already applied to a TLD, then changes will take up to
|
||||
60 minutes to take effect (depending on how you've configured the relevant
|
||||
caching interval; 60 minutes is the default).
|
||||
@@ -80,16 +92,15 @@ premium list must first be applied to a TLD before it will take effect. You will
|
||||
only need to do this when first creating a premium list; once it has been
|
||||
applied, it stays applied, and updates to the list are effective automatically.
|
||||
Note that each TLD can have no more than one premium list applied to it. To
|
||||
apply a premium list to a TLD, run the `update_tld` command with the following
|
||||
parameter:
|
||||
apply a premium list to a TLD,
|
||||
[update the TLD to set the premium list](modifying-tlds.md):
|
||||
|
||||
```shell
|
||||
$ nomulus -e {ENVIRONMENT} update_tld exampletld --premium_list exampletld
|
||||
Update Registry@exampletld
|
||||
premiumList: null -> Key<?>(EntityGroupRoot("cross-tld")/PremiumList("exampletld"))
|
||||
|
||||
Perform this command? (y/N): y
|
||||
Updated 1 entities.
|
||||
...
|
||||
pendingDeleteLength: "PT432000S"
|
||||
premiumListName: "test"
|
||||
pricingEngineClassName: "google.registry.model.pricing.StaticPremiumListPricingEngine"
|
||||
...
|
||||
```
|
||||
|
||||
## Checking which premium list is applied to a TLD
|
||||
@@ -100,7 +111,7 @@ all other information about a TLD). It is used as follows:
|
||||
```shell
|
||||
$ nomulus -e {ENVIRONMENT} get_tld exampletld
|
||||
[ ... snip output ... ]
|
||||
premiumList=Key<?>(EntityGroupRoot("cross-tld")/PremiumList("exampletld"))
|
||||
premiumListName: "test"
|
||||
[ ... snip output ... ]
|
||||
```
|
||||
|
||||
@@ -127,10 +138,10 @@ $ nomulus -e production check_domain {domain_name}
|
||||
[ ... snip output ... ]
|
||||
```
|
||||
|
||||
**Note that the list can be cached for up to 60 minutes, so the old value may
|
||||
**Note that the list can be cached for up to 60 minutes, so the old value may
|
||||
still be returned for a little while**. If it is urgent that the new pricing
|
||||
changes be applied, and it's OK to potentially interrupt client connections,
|
||||
then you can use the App Engine web console to kill instances of the `default`
|
||||
then you can use the GCP web console to kill instances of the `frontend`
|
||||
service, as the cache is per-instance. Once you've killed all the existing
|
||||
instances (don't kill them all at once!), all of the newly spun up instances
|
||||
will now be using the new values you've configured.
|
||||
instances (don't kill them all at once!), all the newly spun up instances will
|
||||
now be using the new values you've configured.
|
||||
|
||||
@@ -16,9 +16,9 @@ phases:
|
||||
3. [Report](https://github.com/google/nomulus/blob/master/java/google/registry/rde/RdeReportAction.java):
|
||||
Transmit XML *report* file to ICANN via HTTPS.
|
||||
|
||||
Each phase happens with an App Engine task queue entry that retries on failure.
|
||||
When each task succeeds, it automatically enqueues a task for the next phase in
|
||||
the process. The staging files are stored in Google Cloud Storage indefinitely,
|
||||
Each phase happens with an GCP task queue entry that retries on failure. When
|
||||
each task succeeds, it automatically enqueues a task for the next phase in the
|
||||
process. The staging files are stored in Google Cloud Storage indefinitely,
|
||||
encrypted with the GhostRyDE container format.
|
||||
|
||||
Note that in order for the automated RDE processing to work correctly, you will
|
||||
@@ -99,9 +99,10 @@ that no cooldown period is necessary.
|
||||
|
||||
## Listing deposits in Cloud Storage
|
||||
|
||||
You can list the files in Cloud Storage for a given TLD using the gcloud storage tool.
|
||||
All files are stored in the {PROJECT-ID}-rde bucket, where {PROJECT-ID} is the
|
||||
name of the App Engine project for the particular environment you are checking.
|
||||
You can list the files in Cloud Storage for a given TLD using the gcloud storage
|
||||
tool. All files are stored in the {PROJECT-ID}-rde bucket, where {PROJECT-ID} is
|
||||
the name of the App Engine project for the particular environment you are
|
||||
checking.
|
||||
|
||||
```shell
|
||||
$ gcloud storage ls gs://{PROJECT-ID}-rde/zip_2015-05-16*
|
||||
@@ -116,10 +117,12 @@ Under normal circumstances, RDE is launched by TldFanoutAction, configured in
|
||||
cron.xml. If the App Engine's cron executor isn't working, you can spawn it
|
||||
manually by visiting the following URL:
|
||||
|
||||
https://backend-dot-{PROJECT-ID}.appspot.com/_dr/task/rdeStaging
|
||||
```
|
||||
https://backend.mydomain.com/_dr/task/rdeStaging
|
||||
```
|
||||
|
||||
That will spawn a staging task for each TLD under the backend module in that App
|
||||
Engine project. You can also run the task from the cron tab of the GAE console.
|
||||
That will spawn a staging task for each TLD under the backend module in that GCP
|
||||
project. You can also run the task from the GCP Cloud Scheduler UI.
|
||||
|
||||
## Notification of upload problems
|
||||
|
||||
@@ -157,7 +160,7 @@ space the uploading at least two hours apart.
|
||||
|
||||
Note that this warning only applies when you (re)upload files directly to the
|
||||
sFTP server. There's an RDE_UPLOAD_SFTP cursor that prevents the production
|
||||
system from uploading twice in a two hour window, so when you let the production
|
||||
system from uploading twice in a two-hour window, so when you let the production
|
||||
job upload missing deposits, it will be safe. Therefore, one safe approach is to
|
||||
reset the cursor, then kick off the production job manually.
|
||||
|
||||
@@ -172,7 +175,7 @@ $ gcloud storage cat gs://{PROJECT-ID}-rde/foo.ghostryde | nomulus -e production
|
||||
|
||||
## Identifying which phase of the process failed
|
||||
|
||||
Analyze the GAE logs on the backend module.
|
||||
Analyze the GCP logs on the backend module.
|
||||
|
||||
If the rdeStaging task failed, then it's likely the files do not exist in cloud
|
||||
storage.
|
||||
@@ -308,8 +311,8 @@ sftp> put ${tld}_2015-05-16_full_S1_R0.sig
|
||||
|
||||
It would be convenient to have the following in your `~/.ssh/config` file and
|
||||
store the SSH private key that you stored in `rde-ssh-client-private` as
|
||||
`~/.ssh/id_rsa_rde` so that you can simply run `$ sftp rde` to connect to
|
||||
the sFTP server.
|
||||
`~/.ssh/id_rsa_rde` so that you can simply run `$ sftp rde` to connect to the
|
||||
sFTP server.
|
||||
|
||||
```
|
||||
Host rde
|
||||
|
||||