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

Compare commits

...

22 Commits

Author SHA1 Message Date
Weimin Yu bd0d8af7b3 Make sure unsafe names can be sent in emails (#2169)
Surround the dot in unsafe domain names with a square bracket. This
is suggested by Gmail abuse-detection and allows outgoing messages
to pass Gmail's check. This should also help with recipients' checks.
2023-10-05 11:19:31 -04:00
Lai Jiang 2da8ea0185 Replace JacksonFactory with GsonFactory (#2173)
JacksonFactory is deprecated and GsonFactory is the recommended
replacement.
2023-10-04 17:02:13 -04:00
Lai Jiang 7a84844000 Remove the GAIA ID field from User (#2170)
It is not used and it is not possible to derive the GAIA ID when
creating a new User from the email address alone.
2023-10-04 15:32:03 -04:00
Weimin Yu 1580555d30 Throttle outgoing emails (#2168)
Adds a delay between emails sent in a tight loop. This helps avoid
triggering Gmail abuse detections.

Also updated the recipient address for billing alerts.
2023-10-04 11:16:56 -04:00
Pavlo Tkach 4fb8a1b50b Add dark theme support to the console (#2167) 2023-10-03 15:54:25 -04:00
Pavlo Tkach e07f25000d Add console registrars paging, fix empty registrars mobile (#2162) 2023-10-03 15:51:48 -04:00
sarahcaseybot cc1777af0c Add custom YAML serializer for Duration (#2161)
* Add custom YAML serializer for Duration

This addresses b/301119144. This changes the YAML representation of a TLD to show Duration fields as a String reperesntation using the Java Duration object's toString() format. This eliminates the previous ambiguity over the time unit that is being used for each duration.

* change standardSeconds to standardMinutes in test

* Add custom serializer to the entire mapper
2023-10-03 13:46:19 -04:00
Lai Jiang 87e54c001f Remove unused fields to make the linter happy (#2165) 2023-10-03 13:25:07 -04:00
Pavlo Tkach 2dc87d42b4 Fix console nextUrl stacking routes (#2164) 2023-10-02 17:38:03 -04:00
Lai Jiang 1eed9c82dc Deprecate the OAuth header in Nomulus tool (#2160)
Unless an --oauth flag is used, the nomulus tool will only send the OIDC
header. The server still accepts both headers and the user should use
`create_user` command to create an admin User (with the --oauth flag on), which
will then allow one to use the nomulus tool without the --oauth flag.

The --oauth flag and the server's ability to support OAuth-based
authentication will be removed soon. Users are urged to create the User
object in time to avoid service interruption.

TESTED=verified on alpha.
2023-10-02 15:50:30 -04:00
gbrodman cf43de7755 Open resources link in new tab (#2163)
We want to do this because it takes the user to an external site, which
could potentially lead to confusion if they tried to use the back button
without a new tab.
2023-10-02 15:06:33 -04:00
Weimin Yu f54bec7553 Add docs for Cloud Build status notification (#2157)
Add documentation that describes the current Cloud Build status notification
to Google Chat, as well as how to update the configuration and the
notifier service.
2023-09-29 10:49:15 -04:00
gbrodman cf698c2586 Add page for WHOIS-editable fields in the console (#2155)
This isn't the prettiest thing, but it replicates the type of view /
edit functionality that we had in the original console.

Of note: this doesn't include input field validation, which would
probably be a good idea to add at some point.
2023-09-28 22:46:18 -04:00
Lai Jiang cb240a8f03 Use equals() method to compare equality (#2158)
It will call equalsImmutableObject(), which seems the right thing to do.
We only care if the two Tld objects have the same fields, not if they
are the same object. ErrorProne complained about comparison by identity.
2023-09-28 13:27:36 -04:00
gbrodman 0801679173 Close sidenav on click (#2156)
It shouldn't stick around after we've clicked on one of the links
2023-09-25 14:43:07 -04:00
sarahcaseybot a87c4a31a3 Add breakglass handling to configureTldCommand (#2154)
* Add a breakglass flag to configureTldCommand

* Add tests

* small fixes
2023-09-22 11:51:02 -04:00
sarahcaseybot 58c7e3a52c Change __REMOVEDOMAIN__ token to __REMOVE_BULK_PRICING__ (#2152) 2023-09-21 16:03:39 -04:00
Pavlo Tkach dded258864 Add resources widget front-end (#2151) 2023-09-21 13:59:40 -04:00
Lai Jiang 759143535f Update proxy k8s manifest (#2153)
The beta API is deprecated.

TESTED=deployed the new manifest to alpha. Without the change, deploying
resulted in an error.

<!-- Reviewable:start -->
- - -
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/google/nomulus/2153)
<!-- Reviewable:end -->
2023-09-21 10:53:39 -04:00
Weimin Yu 46fdf2c996 Defend against deserialization-based attacks (#2150)
* Defend against deserialization-based attacks

Added the `SafeObjectInputStream` class that defends attacks using
malformed serialized data, including remote code execution and
denial-of-service attacks.

Started using the new class to handle EPP resource VKeys and
PendingDeposits, which are passed across credential-boundaries: between
TaskQueue and AppEngine server, and between AppEngine server and the RDE
pipeline on GCE. Note that the wireformat of VKeys do not change,
therefore existing tasks sitting in the TaskQueue are not affected.

Also removed an unused class: JaxbFragment.
2023-09-20 16:56:56 -04:00
sarahcaseybot fc1857717d Use PrintStream in ConfirmingCommand (#2140)
* Use PrintStream in ConfirmingCommand

* Add errorPrintStream

* remove unneccesary line
2023-09-19 12:11:18 -04:00
sarahcaseybot e182692a5f Check for diffs in ConfigureTldCommand (#2146)
* Check for diffs in ConfigureTldCommand

* undo override

* Add handling for ordering sets

* Fix comments

* fix formatting

* fix test
2023-09-19 12:10:26 -04:00
123 changed files with 2240 additions and 575 deletions
+2 -2
View File
@@ -41,8 +41,8 @@
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
+10 -11
View File
@@ -24,6 +24,10 @@ import SettingsSecurityComponent from './settings/security/security.component';
import { RegistrarGuard } from './registrar/registrar.guard';
import { RegistrarComponent } from './registrar/registrarsTable.component';
import { EmptyRegistrar } from './registrar/emptyRegistrar.component';
import ContactComponent from './settings/contact/contact.component';
import WhoisComponent from './settings/whois/whois.component';
import SecurityComponent from './settings/security/security.component';
import UsersComponent from './settings/users/users.component';
const routes: Routes = [
{ path: '', redirectTo: '/home', pathMatch: 'full' },
@@ -32,7 +36,7 @@ const routes: Routes = [
{ path: 'home', component: HomeComponent, canActivate: [RegistrarGuard] },
{ path: 'tlds', component: TldsComponent, canActivate: [RegistrarGuard] },
{
path: 'settings',
path: SettingsComponent.PATH,
component: SettingsComponent,
children: [
{
@@ -41,32 +45,27 @@ const routes: Routes = [
pathMatch: 'full',
},
{
path: 'contact',
path: ContactComponent.PATH,
component: SettingsContactComponent,
canActivate: [RegistrarGuard],
},
{
path: 'whois',
path: WhoisComponent.PATH,
component: SettingsWhoisComponent,
canActivate: [RegistrarGuard],
},
{
path: 'security',
path: SecurityComponent.PATH,
component: SettingsSecurityComponent,
canActivate: [RegistrarGuard],
},
{
path: 'epp-password',
component: SettingsSecurityComponent,
canActivate: [RegistrarGuard],
},
{
path: 'users',
path: UsersComponent.PATH,
component: SettingsUsersComponent,
canActivate: [RegistrarGuard],
},
{
path: 'registrars',
path: RegistrarComponent.PATH,
component: RegistrarComponent,
},
],
+1 -1
View File
@@ -37,7 +37,7 @@
background-color: transparent;
}
.active {
background: #eae1e1;
background-color: var(--secondary);
}
}
&__content-wrapper {
+19 -2
View File
@@ -12,9 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { Component, ViewChild } from '@angular/core';
import { RegistrarService } from './registrar/registrar.service';
import { UserDataService } from './shared/services/userData.service';
import { GlobalLoaderService } from './shared/services/globalLoader.service';
import { NavigationEnd, Router } from '@angular/router';
import { MatSidenav } from '@angular/material/sidenav';
@Component({
selector: 'app-root',
@@ -23,9 +26,15 @@ import { GlobalLoaderService } from './shared/services/globalLoader.service';
})
export class AppComponent {
renderRouter: boolean = true;
@ViewChild('sidenav')
sidenav!: MatSidenav;
constructor(
protected registrarService: RegistrarService,
protected globalLoader: GlobalLoaderService
protected userDataService: UserDataService,
protected globalLoader: GlobalLoaderService,
protected router: Router
) {
registrarService.activeRegistrarIdChange.subscribe(() => {
this.renderRouter = false;
@@ -34,4 +43,12 @@ export class AppComponent {
}, 400);
});
}
ngAfterViewInit() {
this.router.events.subscribe((event) => {
if (event instanceof NavigationEnd) {
this.sidenav.close();
}
});
}
}
+4
View File
@@ -46,6 +46,8 @@ import { EppWidgetComponent } from './home/widgets/epp-widget.component';
import { BillingWidgetComponent } from './home/widgets/billing-widget.component';
import { DomainsWidgetComponent } from './home/widgets/domains-widget.component';
import { SettingsWidgetComponent } from './home/widgets/settings-widget.component';
import { UserDataService } from './shared/services/userData.service';
import WhoisComponent from './settings/whois/whois.component';
@NgModule({
declarations: [
@@ -68,6 +70,7 @@ import { SettingsWidgetComponent } from './home/widgets/settings-widget.componen
SettingsWidgetComponent,
TldsComponent,
TldsWidgetComponent,
WhoisComponent,
],
imports: [
AppRoutingModule,
@@ -81,6 +84,7 @@ import { SettingsWidgetComponent } from './home/widgets/settings-widget.componen
BackendService,
GlobalLoaderService,
RegistrarGuard,
UserDataService,
{
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
useValue: {
@@ -1,24 +1,30 @@
<p>
<p class="console-app__header">
<mat-toolbar color="primary">
<button mat-icon-button aria-label="Open menu" (click)="toggleNavPane()">
<mat-icon>menu</mat-icon>
</button>
<span>
<a
[routerLink]="'/home'"
routerLinkActive="active"
class="console-app__logo"
>
Google Registry
</a>
</span>
<a
[routerLink]="'/home'"
routerLinkActive="active"
class="console-app__logo"
>
Google Registry
</a>
<span class="spacer"></span>
<app-registrar-selector />
<button mat-icon-button aria-label="Open FAQ">
<mat-icon>question_mark</mat-icon>
</button>
<button mat-icon-button aria-label="Open user info">
<button
mat-icon-button
[matMenuTriggerFor]="menu"
#menuTrigger
aria-label="Open user info"
>
<mat-icon>person</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="logOut()">Log out</button>
</mat-menu>
</mat-toolbar>
</p>
@@ -17,6 +17,21 @@
color: inherit;
text-decoration: none;
}
&__header {
@media (max-width: 599px) {
.mat-toolbar {
padding: 0;
}
.console-app__logo {
font-size: 16px;
}
button {
padding-left: 0;
padding-right: 0;
width: 30px;
}
}
}
}
.spacer {
flex: 1;
@@ -28,4 +28,8 @@ export class HeaderComponent {
this.isNavOpen = !this.isNavOpen;
this.toggleNavOpen.emit(this.isNavOpen);
}
logOut() {
window.open('/console?gcp-iap-mode=CLEAR_LOGIN_COOKIE', '_self');
}
}
@@ -29,4 +29,9 @@
}
}
}
@media (max-width: 510px) {
.console-app__widget-wrapper__wide {
grid-column: initial;
}
}
}
@@ -13,13 +13,13 @@
Give us a Call
</button>
<p class="secondary-text">
Call Google Registry support at +1 (404) 978 8419
Call Google Registry support at <b>+1 (404) 978 8419</b>
</p>
<button mat-button color="primary" class="console-app__widget-link">
Send us an Email
</button>
<p class="secondary-text">
Email Google Registry at support@google.com
Email Google Registry at <b>support@google.com</b>
</p>
</div>
</div>
@@ -1,13 +1,17 @@
<mat-card>
<mat-card-content>
<div class="console-app__widget">
<div class="console-app__widget_left">
<a
class="console-app__widget_left"
href="{{ userDataService.userData?.technicalDocsUrl }}"
target="_blank"
>
<mat-icon class="console-app__widget-icon">menu_book</mat-icon>
<h1 class="console-app__widget-title">Resources</h1>
<h4 class="secondary-text text-center">
Use Google Drive to view onboarding FAQs, and technical documentation.
</h4>
</div>
</a>
</div>
</mat-card-content>
</mat-card>
@@ -13,11 +13,12 @@
// limitations under the License.
import { Component } from '@angular/core';
import { UserDataService } from 'src/app/shared/services/userData.service';
@Component({
selector: '[app-resources-widget]',
templateUrl: './resources-widget.component.html',
})
export class ResourcesWidgetComponent {
constructor() {}
constructor(public userDataService: UserDataService) {}
}
@@ -10,11 +10,21 @@
</h4>
</div>
<div class="console-app__widget_right">
<button mat-button color="primary" class="console-app__widget-link">
<button
mat-button
color="primary"
class="console-app__widget-link"
(click)="openContactsPage()"
>
Contact Information
</button>
<p class="secondary-text">Manage Primary, Technical, etc contacts.</p>
<button mat-button color="primary" class="console-app__widget-link">
<button
mat-button
color="primary"
class="console-app__widget-link"
(click)="openSecurityPage()"
>
Security
</button>
<p class="secondary-text">
@@ -28,7 +38,12 @@
User Management
</button>
<p class="secondary-text">Create and manage console user accounts</p>
<button mat-button color="primary" class="console-app__widget-link">
<button
mat-button
color="primary"
class="console-app__widget-link"
(click)="openRegistrarsPage()"
>
Registrar Management
</button>
<p class="secondary-text">Create and manage registrar accounts</p>
@@ -13,11 +13,32 @@
// limitations under the License.
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { RegistrarComponent } from 'src/app/registrar/registrarsTable.component';
import ContactComponent from 'src/app/settings/contact/contact.component';
import SecurityComponent from 'src/app/settings/security/security.component';
import { SettingsComponent } from 'src/app/settings/settings.component';
@Component({
selector: '[app-settings-widget]',
templateUrl: './settings-widget.component.html',
})
export class SettingsWidgetComponent {
constructor() {}
constructor(private router: Router) {}
openRegistrarsPage() {
this.navigate(RegistrarComponent.PATH);
}
openSecurityPage() {
this.navigate(SecurityComponent.PATH);
}
openContactsPage() {
this.navigate(ContactComponent.PATH);
}
private navigate(route: string) {
this.router.navigate([SettingsComponent.PATH, route]);
}
}
@@ -8,6 +8,7 @@
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
white-space: nowrap;
&-icon {
transform: scale(3);
@@ -1,5 +1,14 @@
<div class="console-app__registrar">
<div>
<button
mat-button
[routerLink]="'/settings/registrars'"
routerLinkActive="active"
*ngIf="isMobile; else desktop"
>
{{ registrarService.activeRegistrarId || "Select registrar" }}
<mat-icon>open_in_new</mat-icon>
</button>
<ng-template #desktop>
<mat-form-field class="mat-form-field-density-5" appearance="fill">
<mat-label>Registrar</mat-label>
<mat-select
@@ -14,5 +23,5 @@
</mat-option>
</mat-select>
</mat-form-field>
</div>
</ng-template>
</div>
@@ -12,14 +12,35 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { RegistrarService } from './registrar.service';
import { BreakpointObserver } from '@angular/cdk/layout';
import { distinctUntilChanged } from 'rxjs';
const MOBILE_LAYOUT_BREAKPOINT = '(max-width: 599px)';
@Component({
selector: 'app-registrar-selector',
templateUrl: './registrar-selector.component.html',
styleUrls: ['./registrar-selector.component.scss'],
})
export class RegistrarSelectorComponent {
constructor(protected registrarService: RegistrarService) {}
export class RegistrarSelectorComponent implements OnInit {
protected isMobile: boolean = false;
readonly breakpoint$ = this.breakpointObserver
.observe([MOBILE_LAYOUT_BREAKPOINT])
.pipe(distinctUntilChanged());
constructor(
protected registrarService: RegistrarService,
protected breakpointObserver: BreakpointObserver
) {}
ngOnInit(): void {
this.breakpoint$.subscribe(() => this.breakpointChanged());
}
private breakpointChanged() {
this.isMobile = this.breakpointObserver.isMatched(MOBILE_LAYOUT_BREAKPOINT);
}
}
@@ -13,7 +13,11 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import {
ActivatedRouteSnapshot,
Router,
RouterStateSnapshot,
} from '@angular/router';
import { RegistrarService } from './registrar.service';
@@ -26,13 +30,16 @@ export class RegistrarGuard {
private registrarService: RegistrarService
) {}
canActivate(): Promise<boolean> | boolean {
canActivate(
_: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Promise<boolean> | boolean {
if (this.registrarService.activeRegistrarId) {
return true;
}
// Get the full URL including any nested children (skip the initial '#/')
// NB: an empty nextUrl takes the user to the home page
const nextUrl = location.hash.split('#/')[1] || '';
return this.router.navigate([`/empty-registrar`, { nextUrl }]);
return this.router.navigate([
`/empty-registrar`,
{ nextUrl: state.url || '' },
]);
}
}
@@ -13,14 +13,16 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { BackendService } from '../shared/services/backend.service';
import { Observable, Subject, tap } from 'rxjs';
import { BackendService } from '../shared/services/backend.service';
import {
GlobalLoader,
GlobalLoaderService,
} from '../shared/services/globalLoader.service';
import { MatSnackBar } from '@angular/material/snack-bar';
interface Address {
export interface Address {
street?: string[];
city?: string;
countryCode?: string;
@@ -30,16 +32,20 @@ interface Address {
export interface Registrar {
allowedTlds?: string[];
ipAddressAllowList?: string[];
emailAddress?: string;
billingAccountMap?: object;
driveFolderId?: string;
emailAddress?: string;
faxNumber?: string;
ianaIdentifier?: number;
icannReferralEmail?: string;
ipAddressAllowList?: string[];
localizedAddress?: Address;
phoneNumber?: string;
registrarId: string;
registrarName: string;
registryLockAllowed?: boolean;
url?: string;
whoisServer?: string;
}
@Injectable({
@@ -52,7 +58,8 @@ export class RegistrarService implements GlobalLoader {
constructor(
private backend: BackendService,
private globalLoader: GlobalLoaderService
private globalLoader: GlobalLoaderService,
private _snackBar: MatSnackBar
) {
this.loadRegistrars().subscribe((r) => {
this.globalLoader.stopGlobalLoader(this);
@@ -82,6 +89,8 @@ export class RegistrarService implements GlobalLoader {
}
loadingTimeout() {
// TODO: Decide what to do when timeout happens
this._snackBar.open('Timeout loading registrars', undefined, {
duration: 1500,
});
}
}
@@ -1,22 +1,21 @@
<div class="console-app__registrars">
<table
mat-table
[dataSource]="registrarService.registrars"
<mat-table
[dataSource]="dataSource"
class="mat-elevation-z8"
class="console-app__registrars-table"
matSort
>
<ng-container
*ngFor="let column of columns"
[matColumnDef]="column.columnDef"
>
<th mat-header-cell *matHeaderCellDef>
{{ column.header }}
</th>
<td mat-cell *matCellDef="let row" [innerHTML]="column.cell(row)"></td>
<mat-header-cell *matHeaderCellDef> {{ column.header }} </mat-header-cell>
<mat-cell *matCellDef="let row" [innerHTML]="column.cell(row)"></mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
<mat-paginator
class="mat-elevation-z8"
[pageSizeOptions]="[5, 10, 20]"
@@ -1,5 +1,27 @@
.console-app {
$min-width: 756px;
&__registrars {
margin-top: 1.5rem;
width: 100%;
overflow: auto;
}
&__registrars-table {
min-width: $min-width !important;
}
.mat-mdc-paginator {
min-width: $min-width !important;
}
.mat-column {
&-driveId {
min-width: 200px;
word-break: break-all;
}
&-registryLockAllowed {
max-width: 80px;
}
}
}
@@ -12,15 +12,21 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { Component, ViewChild, ViewEncapsulation } from '@angular/core';
import { Registrar, RegistrarService } from './registrar.service';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
@Component({
selector: 'app-registrar',
templateUrl: './registrarsTable.component.html',
styleUrls: ['./registrarsTable.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export class RegistrarComponent {
public static PATH = 'registrars';
dataSource: MatTableDataSource<Registrar>;
columns = [
{
columnDef: 'registrarId',
@@ -71,5 +77,18 @@ export class RegistrarComponent {
},
];
displayedColumns = this.columns.map((c) => c.columnDef);
constructor(protected registrarService: RegistrarService) {}
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
constructor(protected registrarService: RegistrarService) {
this.dataSource = new MatTableDataSource<Registrar>(
registrarService.registrars
);
}
ngAfterViewInit() {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
}
@@ -1,7 +1,7 @@
<h3 mat-dialog-title>Contact details</h3>
<div mat-dialog-content>
<form (ngSubmit)="saveAndClose($event)">
<div>
<p>
<mat-form-field class="contact-details__input">
<mat-label>Name: </mat-label>
<input
@@ -11,9 +11,9 @@
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</div>
</p>
<div>
<p>
<mat-form-field class="contact-details__input">
<mat-label>Primary account email: </mat-label>
<input
@@ -25,9 +25,9 @@
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</div>
</p>
<div>
<p>
<mat-form-field class="contact-details__input">
<mat-label>Phone: </mat-label>
<input
@@ -36,9 +36,9 @@
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</div>
</p>
<div>
<p>
<mat-form-field class="contact-details__input">
<mat-label>Fax: </mat-label>
<input
@@ -47,7 +47,7 @@
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</div>
</p>
<div class="contact-details__group">
<label>Contact type:</label>
@@ -143,6 +143,8 @@ export class ContactDetailsDialogComponent {
styleUrls: ['./contact.component.scss'],
})
export default class ContactComponent {
public static PATH = 'contact';
loading: boolean = false;
constructor(
private dialog: MatDialog,
@@ -29,6 +29,8 @@ import { RegistrarService } from 'src/app/registrar/registrar.service';
providers: [SecurityService],
})
export default class SecurityComponent {
public static PATH = 'security';
loading: boolean = false;
inEdit: boolean = false;
dataSource: SecuritySettings = {};
@@ -15,9 +15,9 @@
.console-settings {
.mdc-tab {
&.active-link {
border-bottom: 2px solid #673ab7;
border-bottom: 2px solid var(--primary);
.mdc-tab__text-label {
color: #673ab7;
color: var(--primary);
}
}
}
@@ -20,4 +20,6 @@ import { Component, ViewEncapsulation } from '@angular/core';
styleUrls: ['./settings.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export class SettingsComponent {}
export class SettingsComponent {
public static PATH = 'settings';
}
@@ -19,4 +19,6 @@ import { Component } from '@angular/core';
templateUrl: './users.component.html',
styleUrls: ['./users.component.scss'],
})
export default class UsersComponent {}
export default class UsersComponent {
public static PATH = 'users';
}
@@ -1 +1,250 @@
<p>whois works!</p>
<div class="settings-whois">
<h2>WHOIS settings</h2>
<h3>
General registrar information for your WHOIS record. This information is
always visible in WHOIS.
</h3>
<div *ngIf="loading" class="settings-whois__loading">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-description">
<h3>Name:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
matInput
type="text"
[(ngModel)]="registrar.registrarName"
disabled
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-description">
<h3>IANA Identifier:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
matInput
type="text"
[(ngModel)]="registrar.ianaIdentifier"
disabled
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-description">
<h3>ICANN Referral Email:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
matInput
type="email"
[(ngModel)]="registrar.icannReferralEmail"
disabled
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-description">
<h3>WHOIS server:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
matInput
type="text"
[(ngModel)]="registrar.whoisServer"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-description">
<h3>Referral URL:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
matInput
type="text"
[(ngModel)]="registrar.url"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-description">
<h3>Email:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
matInput
type="email"
[(ngModel)]="registrar.emailAddress"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-description">
<h3>Phone::</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
matInput
type="text"
[(ngModel)]="registrar.phoneNumber"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-description">
<h3>Fax:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
matInput
type="text"
[(ngModel)]="registrar.faxNumber"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-address">
<div class="settings-whois__section-description">
<h3>Address Line 1:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
*ngIf="registrar.localizedAddress?.street"
matInput
type="text"
[(ngModel)]="(registrar.localizedAddress?.street)![0]"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section-address">
<div class="settings-whois__section-description">
<h3>City:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
*ngIf="registrar.localizedAddress"
matInput
type="text"
[(ngModel)]="registrar.localizedAddress.city"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-address">
<div class="settings-whois__section-description">
<h3>Address Line 2:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
*ngIf="registrar.localizedAddress?.street"
matInput
type="text"
[(ngModel)]="(registrar.localizedAddress?.street)![1]"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section-address">
<div class="settings-whois__section-description">
<h3>State/Region:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
*ngIf="registrar.localizedAddress"
matInput
type="text"
[(ngModel)]="registrar.localizedAddress.state"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-address">
<div class="settings-whois__section-description">
<h3>Address Line 3:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
*ngIf="registrar.localizedAddress?.street"
matInput
type="text"
[(ngModel)]="(registrar.localizedAddress?.street)![2]"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section-address">
<div class="settings-whois__section-description">
<h3>Country Code:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
*ngIf="registrar.localizedAddress"
matInput
type="text"
[(ngModel)]="registrar.localizedAddress.countryCode"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
</div>
<div class="settings-whois__actions">
<ng-template [ngIf]="inEdit" [ngIfElse]="inView">
<button
class="actions-save"
mat-raised-button
color="primary"
(click)="save()"
>
Save
</button>
<button class="actions-cancel" mat-stroked-button (click)="cancel()">
Cancel
</button>
</ng-template>
<ng-template #inView>
<button #elseBlock mat-raised-button (click)="enableEdit()">Edit</button>
</ng-template>
</div>
</div>
@@ -11,3 +11,49 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
.settings-whois {
margin-top: 1.5rem;
&__section {
display: flex;
flex-wrap: wrap;
margin-bottom: 10px;
min-width: 400px;
}
&__section-address {
display: flex;
flex-wrap: wrap;
margin-bottom: 5px;
min-width: 450px;
width: 50%;
max-width: 50%;
}
&__section-description {
display: inline-block;
margin-block-start: 1em;
min-width: 150px;
}
&__section-form {
display: inline-block;
width: 70%;
mat-form-field {
width: 90%;
min-width: 300px;
}
input:disabled {
border: 0;
}
}
&__loading {
margin: 2rem 0;
}
&__actions {
margin-top: 50px;
display: flex;
justify-content: flex-end;
margin-right: 50px;
button {
margin-left: 20px;
}
}
}
@@ -12,11 +12,66 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { HttpErrorResponse } from '@angular/common/http';
import { Component } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import {
Registrar,
RegistrarService,
} from 'src/app/registrar/registrar.service';
import { WhoisService } from './whois.service';
@Component({
selector: 'app-whois',
templateUrl: './whois.component.html',
styleUrls: ['./whois.component.scss'],
providers: [WhoisService],
})
export default class WhoisComponent {}
export default class WhoisComponent {
public static PATH = 'whois';
loading = false;
inEdit = false;
registrar: Registrar;
constructor(
public whoisService: WhoisService,
public registrarService: RegistrarService,
private _snackBar: MatSnackBar
) {
this.registrar = JSON.parse(
JSON.stringify(this.registrarService.registrar)
);
}
enableEdit() {
this.inEdit = true;
}
cancel() {
this.inEdit = false;
this.resetDataSource();
}
save() {
this.loading = true;
this.whoisService.saveChanges(this.registrar).subscribe({
complete: () => {
this.loading = false;
this.resetDataSource();
},
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error, undefined, {
duration: 1500,
});
},
});
this.cancel();
}
resetDataSource() {
this.registrar = JSON.parse(
JSON.stringify(this.registrarService.registrar)
);
}
}
@@ -0,0 +1,45 @@
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { switchMap } from 'rxjs';
import { Address, RegistrarService } from 'src/app/registrar/registrar.service';
import { BackendService } from 'src/app/shared/services/backend.service';
export interface WhoisRegistrarFields {
ianaIdentifier?: number;
icannReferralEmail?: string;
localizedAddress?: Address;
registrarId?: string;
url?: string;
whoisServer?: string;
}
@Injectable()
export class WhoisService {
whoisRegistrarFields: WhoisRegistrarFields = {};
constructor(
private backend: BackendService,
private registrarService: RegistrarService
) {}
saveChanges(newWhoisRegistrarFields: WhoisRegistrarFields) {
return this.backend.postWhoisRegistrarFields(newWhoisRegistrarFields).pipe(
switchMap(() => {
return this.registrarService.loadRegistrars();
})
);
}
}
@@ -19,6 +19,8 @@ import { SecuritySettingsBackendModel } from 'src/app/settings/security/security
import { Contact } from '../../settings/contact/contact.service';
import { Registrar } from '../../registrar/registrar.service';
import { UserData } from './userData.service';
import { WhoisRegistrarFields } from 'src/app/settings/whois/whois.service';
@Injectable()
export class BackendService {
@@ -90,4 +92,19 @@ export class BackendService {
securitySettings
);
}
getUserData(): Observable<UserData> {
return this.http
.get<UserData>('/console-api/userdata')
.pipe(catchError((err) => this.errorCatcher<UserData>(err)));
}
postWhoisRegistrarFields(
whoisRegistrarFields: WhoisRegistrarFields
): Observable<WhoisRegistrarFields> {
return this.http.post<WhoisRegistrarFields>(
'/console-api/settings/whois-fields',
whoisRegistrarFields
);
}
}
@@ -0,0 +1,56 @@
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { Observable, tap } from 'rxjs';
import { BackendService } from './backend.service';
import { MatSnackBar } from '@angular/material/snack-bar';
import { GlobalLoader, GlobalLoaderService } from './globalLoader.service';
export interface UserData {
isAdmin: boolean;
globalRole: string;
technicalDocsUrl: string;
}
@Injectable({
providedIn: 'root',
})
export class UserDataService implements GlobalLoader {
public userData?: UserData;
constructor(
private backend: BackendService,
protected globalLoader: GlobalLoaderService,
private _snackBar: MatSnackBar
) {
this.getUserData().subscribe(() => {
this.globalLoader.stopGlobalLoader(this);
});
this.globalLoader.startGlobalLoader(this);
}
getUserData(): Observable<UserData> {
return this.backend.getUserData().pipe(
tap((userData: UserData) => {
this.userData = userData;
})
);
}
loadingTimeout() {
this._snackBar.open('Timeout loading user data', undefined, {
duration: 1500,
});
}
}
+8 -5
View File
@@ -44,16 +44,19 @@ body {
&-link {
padding: 0 !important;
text-align: left;
height: 20px !important;
min-width: auto !important;
height: min-content !important;
}
&-title {
color: var(--primary) !important;
text-align: center;
}
&-icon {
font-size: 4rem;
line-height: 4rem;
height: 4rem !important;
width: 4rem !important;
color: var(--text);
font-size: 5rem;
line-height: 5rem;
height: 5rem !important;
width: 5rem !important;
}
&_left {
flex: 1;
+61 -27
View File
@@ -17,20 +17,9 @@ $theme-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400);
// The warn palette is optional (defaults to red).
$theme-warn: mat.define-palette(mat.$red-palette);
// Create the theme object. A theme consists of configurations for individual
// theming systems such as "color" or "typography".
$theme: mat.define-light-theme(
(
color: (
primary: $theme-primary,
accent: $theme-accent,
warn: $theme-warn,
),
density: 0,
)
);
/** Application specific section **/
/**
** Application specific section - Global styles and mixins
**/
@mixin form-field-density($density) {
$field-typography: mat.define-typography-config(
@@ -46,19 +35,6 @@ $theme: mat.define-light-theme(
@include form-field-density(-5);
}
$foreground: map.merge($theme, mat.$light-theme-foreground-palette);
// Access and define a class with secondary color exposed
.secondary-text {
color: map.get($foreground, "secondary-text");
}
:root {
--primary: #{mat.get-color-from-palette($theme-primary, 500)};
--secondary: #{map.get($foreground, "secondary-text")};
}
@include mat.all-component-themes($theme);
@import "@angular/material/theming";
// Define application specific typography settings, font-family, etc
@@ -67,3 +43,61 @@ $typography-configuration: mat-typography-config(
);
@include angular-material-typography($typography-configuration);
/**
** Light theme
**/
$light-theme: mat.define-light-theme(
(
color: (
primary: $theme-primary,
accent: $theme-accent,
warn: $theme-warn,
),
density: 0,
)
);
// Access and define a class with secondary color exposed
.secondary-text {
color: map.get(mat.$light-theme-foreground-palette, "secondary-text");
}
:root {
--text: #{map.get(mat.$light-theme-foreground-palette, "base")};
--primary: #{mat.get-color-from-palette($theme-primary, 500)};
--secondary: #{map.get(mat.$light-theme-foreground-palette, "secondary-text")};
}
@include mat.all-component-themes($light-theme);
/**
** Dark theme
**/
$dark-theme: mat.define-dark-theme(
(
color: (
primary: mat.define-palette(mat.$pink-palette),
accent: mat.define-palette(mat.$blue-grey-palette),
),
density: 0,
)
);
@mixin _apply-dark-mode-colors() {
@include mat.all-component-colors($dark-theme);
.secondary-text {
color: map.get(mat.$dark-theme-foreground-palette, "secondary-text");
}
:root {
--text: #{map.get(mat.$dark-theme-foreground-palette, "base")};
--primary: #{mat.get-color-from-palette(mat.$pink-palette, 500)};
--secondary: #{map.get(mat.$dark-theme-background-palette, "secondary-text")};
}
}
@media (prefers-color-scheme: dark) {
@include _apply-dark-mode-colors();
}
@@ -27,6 +27,10 @@ import static google.registry.beam.rde.RdePipeline.TupleTags.REVISION_ID;
import static google.registry.beam.rde.RdePipeline.TupleTags.SUPERORDINATE_DOMAINS;
import static google.registry.model.reporting.HistoryEntryDao.RESOURCE_TYPES_TO_HISTORY_TYPES;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.SafeSerializationUtils.safeDeserializeCollection;
import static google.registry.util.SafeSerializationUtils.serializeCollection;
import static google.registry.util.SerializeUtils.decodeBase64;
import static google.registry.util.SerializeUtils.encodeBase64;
import static org.apache.beam.sdk.values.TypeDescriptors.kvs;
import com.google.common.collect.ImmutableList;
@@ -65,11 +69,7 @@ import google.registry.rde.PendingDeposit.PendingDepositCoder;
import google.registry.rde.RdeMarshaller;
import google.registry.util.UtilsModule;
import google.registry.xml.ValidationMode;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.HashSet;
import javax.inject.Inject;
@@ -658,14 +658,8 @@ public class RdePipeline implements Serializable {
*/
@SuppressWarnings("unchecked")
static ImmutableSet<PendingDeposit> decodePendingDeposits(String encodedPendingDeposits) {
try (ObjectInputStream ois =
new ObjectInputStream(
new ByteArrayInputStream(
BaseEncoding.base64Url().omitPadding().decode(encodedPendingDeposits)))) {
return (ImmutableSet<PendingDeposit>) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
throw new IllegalArgumentException("Unable to parse encoded pending deposit map.", e);
}
return ImmutableSet.copyOf(
safeDeserializeCollection(PendingDeposit.class, decodeBase64(encodedPendingDeposits)));
}
/**
@@ -674,12 +668,7 @@ public class RdePipeline implements Serializable {
*/
public static String encodePendingDeposits(ImmutableSet<PendingDeposit> pendingDeposits)
throws IOException {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(pendingDeposits);
oos.flush();
return BaseEncoding.base64Url().omitPadding().encode(baos.toByteArray());
}
return encodeBase64(serializeCollection(pendingDeposits));
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
@@ -879,6 +879,17 @@ public final class RegistryConfig {
return Optional.ofNullable(config.misc.sheetExportId);
}
/**
* Returns the desired delay between outgoing emails when sending in bulk.
*
* <p>Gmail apparently has unpublished limits on peak throughput over short period.
*/
@Provides
@Config("emailThrottleDuration")
public static Duration provideEmailThrottleSeconds(RegistryConfigSettings config) {
return Duration.standardSeconds(config.misc.emailThrottleSeconds);
}
/**
* Returns the email address we send various alert e-mails to.
*
@@ -208,6 +208,7 @@ public class RegistryConfigSettings {
public static class Misc {
public String sheetExportId;
public boolean isEmailSendingEnabled;
public int emailThrottleSeconds;
public String alertRecipientEmailAddress;
// TODO(b/279671974): remove below field after migration
public String newAlertRecipientEmailAddress;
@@ -443,6 +443,9 @@ misc:
# Whether emails may be sent. For Prod and Sandbox this should be true.
isEmailSendingEnabled: false
# Delay between bulk messages to avoid triggering Gmail fraud checks
emailThrottleSeconds: 30
# Address we send alert summary emails to.
alertRecipientEmailAddress: email@example.com
@@ -53,8 +53,8 @@ import google.registry.flows.custom.DomainRenewFlowCustomLogic.BeforeResponseRet
import google.registry.flows.custom.DomainRenewFlowCustomLogic.BeforeSaveParameters;
import google.registry.flows.custom.EntityChanges;
import google.registry.flows.domain.token.AllocationTokenFlowUtils;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.MissingRemoveDomainTokenOnBulkPricingDomainException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.RemoveDomainTokenOnNonBulkPricingDomainException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.MissingRemoveBulkPricingTokenOnBulkPricingDomainException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.RemoveBulkPricingTokenOnNonBulkPricingDomainException;
import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingEvent;
@@ -121,8 +121,8 @@ import org.joda.time.Duration;
* @error {@link DomainFlowUtils.RegistrarMustBeActiveForThisOperationException}
* @error {@link DomainFlowUtils.UnsupportedFeeAttributeException}
* @error {@link DomainRenewFlow.IncorrectCurrentExpirationDateException}
* @error {@link MissingRemoveDomainTokenOnBulkPricingDomainException}
* @error {@link RemoveDomainTokenOnNonBulkPricingDomainException}
* @error {@link MissingRemoveBulkPricingTokenOnBulkPricingDomainException}
* @error {@link RemoveBulkPricingTokenOnNonBulkPricingDomainException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException}
* @error {@link
@@ -328,7 +328,7 @@ public final class DomainRenewFlow implements MutatingFlow {
checkHasBillingAccount(registrarId, existingDomain.getTld());
}
verifyUnitIsYears(command.getPeriod());
// We only allow __REMOVEDOMAIN__ token on bulk pricing domains for now
// We only allow __REMOVE_BULK_PRICING__ token on bulk pricing domains for now
verifyTokenAllowedOnDomain(existingDomain, allocationToken);
// If the date they specify doesn't match the expiration, fail. (This is an idempotence check).
if (!command.getCurrentExpirationDate().equals(
@@ -243,21 +243,21 @@ public class AllocationTokenFlowUtils {
Domain domain, Optional<AllocationToken> allocationToken) throws EppException {
boolean domainHasBulkToken = domain.getCurrentBulkToken().isPresent();
boolean hasRemoveDomainToken =
boolean hasRemoveBulkPricingToken =
allocationToken.isPresent()
&& TokenBehavior.REMOVE_DOMAIN.equals(allocationToken.get().getTokenBehavior());
&& TokenBehavior.REMOVE_BULK_PRICING.equals(allocationToken.get().getTokenBehavior());
if (hasRemoveDomainToken && !domainHasBulkToken) {
throw new RemoveDomainTokenOnNonBulkPricingDomainException();
} else if (!hasRemoveDomainToken && domainHasBulkToken) {
throw new MissingRemoveDomainTokenOnBulkPricingDomainException();
if (hasRemoveBulkPricingToken && !domainHasBulkToken) {
throw new RemoveBulkPricingTokenOnNonBulkPricingDomainException();
} else if (!hasRemoveBulkPricingToken && domainHasBulkToken) {
throw new MissingRemoveBulkPricingTokenOnBulkPricingDomainException();
}
}
public static Domain maybeApplyBulkPricingRemovalToken(
Domain domain, Optional<AllocationToken> allocationToken) {
if (!allocationToken.isPresent()
|| !TokenBehavior.REMOVE_DOMAIN.equals(allocationToken.get().getTokenBehavior())) {
|| !TokenBehavior.REMOVE_BULK_PRICING.equals(allocationToken.get().getTokenBehavior())) {
return domain;
}
@@ -338,19 +338,19 @@ public class AllocationTokenFlowUtils {
}
}
/** The __REMOVEDOMAIN__ token is missing on a bulk pricing domain command */
public static class MissingRemoveDomainTokenOnBulkPricingDomainException
/** The __REMOVE_BULK_PRICING__ token is missing on a bulk pricing domain command */
public static class MissingRemoveBulkPricingTokenOnBulkPricingDomainException
extends AssociationProhibitsOperationException {
MissingRemoveDomainTokenOnBulkPricingDomainException() {
MissingRemoveBulkPricingTokenOnBulkPricingDomainException() {
super("Domains that are inside bulk pricing cannot be explicitly renewed or transferred");
}
}
/** The __REMOVEDOMAIN__ token is not allowed on non bulk pricing domains */
public static class RemoveDomainTokenOnNonBulkPricingDomainException
/** The __REMOVE_BULK_PRICING__ token is not allowed on non bulk pricing domains */
public static class RemoveBulkPricingTokenOnNonBulkPricingDomainException
extends AssociationProhibitsOperationException {
RemoveDomainTokenOnNonBulkPricingDomainException() {
super("__REMOVEDOMAIN__ token is not allowed on non bulk pricing domains");
RemoveBulkPricingTokenOnNonBulkPricingDomainException() {
super("__REMOVE_BULK_PRICING__ token is not allowed on non bulk pricing domains");
}
}
}
@@ -14,6 +14,7 @@
package google.registry.model;
import static com.google.common.collect.ImmutableSortedMap.toImmutableSortedMap;
import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
import static com.google.common.collect.Ordering.natural;
import com.fasterxml.jackson.core.JsonGenerator;
@@ -29,6 +30,7 @@ import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature;
import com.google.common.collect.ImmutableSortedSet;
import google.registry.model.common.TimedTransitionProperty;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.tld.Tld.TldState;
@@ -39,6 +41,7 @@ import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.SortedMap;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
@@ -55,16 +58,67 @@ public class EntityYamlUtils {
SimpleModule module = new SimpleModule();
module.addSerializer(Money.class, new MoneySerializer());
module.addDeserializer(Money.class, new MoneyDeserializer());
module.addSerializer(Duration.class, new DurationSerializer());
ObjectMapper mapper =
JsonMapper.builder(new YAMLFactory().disable(Feature.WRITE_DOC_START_MARKER))
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY)
.build()
.registerModule(module);
mapper.findAndRegisterModules();
.build();
mapper.findAndRegisterModules().registerModule(module);
return mapper;
}
/**
* A custom serializer for String Set to sort the order and make YAML generation deterministic.
*/
public static class SortedSetSerializer extends StdSerializer<Set<String>> {
public SortedSetSerializer() {
this(null);
}
public SortedSetSerializer(Class<Set<String>> t) {
super(t);
}
@Override
public void serialize(Set<String> value, JsonGenerator g, SerializerProvider provider)
throws IOException {
ImmutableSortedSet<String> sorted =
value.stream()
.collect(toImmutableSortedSet(String::compareTo)); // sort the entries into a new set
g.writeStartArray();
for (String entry : sorted) {
g.writeString(entry);
}
g.writeEndArray();
}
}
/** A custom serializer for Enum Set to sort the order and make YAML generation deterministic. */
public static class SortedEnumSetSerializer extends StdSerializer<Set<Enum>> {
public SortedEnumSetSerializer() {
this(null);
}
public SortedEnumSetSerializer(Class<Set<Enum>> t) {
super(t);
}
@Override
public void serialize(Set<Enum> value, JsonGenerator g, SerializerProvider provider)
throws IOException {
ImmutableSortedSet<String> sorted =
value.stream()
.map(Enum::name)
.collect(toImmutableSortedSet(String::compareTo)); // sort the entries into a new set
g.writeStartArray();
for (String entry : sorted) {
g.writeString(entry);
}
g.writeEndArray();
}
}
/** A custom JSON serializer for {@link Money}. */
public static class MoneySerializer extends StdSerializer<Money> {
@@ -147,6 +201,24 @@ public class EntityYamlUtils {
}
}
/** A custom JSON serializer for a {@link Duration} object. */
public static class DurationSerializer extends StdSerializer<Duration> {
public DurationSerializer() {
this(null);
}
public DurationSerializer(Class<Duration> t) {
super(t);
}
@Override
public void serialize(Duration value, JsonGenerator gen, SerializerProvider provider)
throws IOException {
gen.writeString(value.toString());
}
}
/** A custom JSON serializer for an Optional of a {@link Duration} object. */
public static class OptionalDurationSerializer extends StdSerializer<Optional<Duration>> {
@@ -162,7 +234,7 @@ public class EntityYamlUtils {
public void serialize(Optional<Duration> value, JsonGenerator gen, SerializerProvider provider)
throws IOException {
if (value.isPresent()) {
gen.writeNumber(value.get().getMillis());
gen.writeString(value.get().toString());
} else {
gen.writeNull();
}
@@ -35,21 +35,16 @@ import javax.persistence.Table;
/** A console user, either a registry employee or a registrar partner. */
@Entity
@Table(
indexes = {
@Index(columnList = "gaiaId", name = "user_gaia_id_idx"),
@Index(columnList = "emailAddress", name = "user_email_address_idx")
})
@Table(indexes = {@Index(columnList = "emailAddress", name = "user_email_address_idx")})
public class User extends UpdateAutoTimestampEntity implements Buildable {
private static final long serialVersionUID = 6936728603828566721L;
/** Autogenerated unique ID of this user. */
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** GAIA ID associated with the user in question. */
private String gaiaId;
/** Email address of the user in question. */
@Column(nullable = false)
private String emailAddress;
@@ -71,10 +66,6 @@ public class User extends UpdateAutoTimestampEntity implements Buildable {
return id;
}
public String getGaiaId() {
return gaiaId;
}
public String getEmailAddress() {
return emailAddress;
}
@@ -139,12 +130,6 @@ public class User extends UpdateAutoTimestampEntity implements Buildable {
return super.build();
}
public Builder setGaiaId(String gaiaId) {
checkArgument(!isNullOrEmpty(gaiaId), "Gaia ID cannot be null or empty");
getInstance().gaiaId = gaiaId;
return this;
}
public Builder setEmailAddress(String emailAddress) {
getInstance().emailAddress = checkValidEmail(emailAddress);
return this;
@@ -77,10 +77,10 @@ import org.joda.time.DateTime;
public class AllocationToken extends UpdateAutoTimestampEntity implements Buildable {
private static final long serialVersionUID = -3954475393220876903L;
private static final String REMOVE_DOMAIN = "__REMOVEDOMAIN__";
private static final String REMOVE_BULK_PRICING = "__REMOVE_BULK_PRICING__";
private static final ImmutableMap<String, TokenBehavior> STATIC_TOKEN_BEHAVIORS =
ImmutableMap.of(REMOVE_DOMAIN, TokenBehavior.REMOVE_DOMAIN);
ImmutableMap.of(REMOVE_BULK_PRICING, TokenBehavior.REMOVE_BULK_PRICING);
// Promotions should only move forward, and ENDED / CANCELLED are terminal states.
private static final ImmutableMultimap<TokenStatus, TokenStatus> VALID_TOKEN_STATUS_TRANSITIONS =
@@ -91,10 +91,10 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda
private static final ImmutableMap<String, AllocationToken> BEHAVIORAL_TOKENS =
ImmutableMap.of(
REMOVE_DOMAIN,
REMOVE_BULK_PRICING,
new AllocationToken.Builder()
.setTokenType(TokenType.UNLIMITED_USE)
.setToken(REMOVE_DOMAIN)
.setToken(REMOVE_BULK_PRICING)
.build());
public static Optional<AllocationToken> maybeGetStaticTokenInstance(String name) {
@@ -142,10 +142,10 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda
/** No special behavior */
DEFAULT,
/**
* REMOVE_DOMAIN triggers domain removal from a bulk pricing package, bypasses DEFAULT token
* validations.
* REMOVE_BULK_PRICING triggers domain removal from a bulk pricing package, bypasses DEFAULT
* token validations.
*/
REMOVE_DOMAIN
REMOVE_BULK_PRICING
}
/** The status of this token with regard to any potential promotion. */
@@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Maps.toMap;
import static google.registry.config.RegistryConfig.getSingletonCacheRefreshDuration;
import static google.registry.model.EntityYamlUtils.createObjectMapper;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
@@ -28,6 +29,8 @@ import static org.joda.money.CurrencyUnit.USD;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.github.benmanes.caffeine.cache.CacheLoader;
@@ -50,6 +53,8 @@ import google.registry.model.EntityYamlUtils.CurrencyDeserializer;
import google.registry.model.EntityYamlUtils.CurrencySerializer;
import google.registry.model.EntityYamlUtils.OptionalDurationSerializer;
import google.registry.model.EntityYamlUtils.OptionalStringSerializer;
import google.registry.model.EntityYamlUtils.SortedEnumSetSerializer;
import google.registry.model.EntityYamlUtils.SortedSetSerializer;
import google.registry.model.EntityYamlUtils.TimedTransitionPropertyMoneyDeserializer;
import google.registry.model.EntityYamlUtils.TimedTransitionPropertyTldStateDeserializer;
import google.registry.model.EntityYamlUtils.TokenVKeyListDeserializer;
@@ -124,6 +129,20 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
public static final Money DEFAULT_SERVER_STATUS_CHANGE_BILLING_COST = Money.of(USD, 20);
public static final Money DEFAULT_REGISTRY_LOCK_OR_UNLOCK_BILLING_COST = Money.of(USD, 0);
public boolean equalYaml(Tld tldToCompare) {
if (this.equals(tldToCompare)) {
return true;
}
ObjectMapper mapper = createObjectMapper();
try {
String thisYaml = mapper.writeValueAsString(this);
String otherYaml = mapper.writeValueAsString(tldToCompare);
return thisYaml.equals(otherYaml);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
/** The type of TLD, which determines things like backups and escrow policy. */
public enum TldType {
/**
@@ -255,6 +274,7 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
* <p>All entries of this list must be valid keys for the map of {@code DnsWriter}s injected by
* {@code @Inject Map<String, DnsWriter>}
*/
@JsonSerialize(using = SortedSetSerializer.class)
@Column(nullable = false)
Set<String> dnsWriters;
@@ -354,6 +374,7 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
CreateAutoTimestamp creationTime = CreateAutoTimestamp.create(null);
/** The set of reserved list names that are applicable to this tld. */
@JsonSerialize(using = SortedSetSerializer.class)
@Column(name = "reserved_list_names")
Set<String> reservedListNames;
@@ -493,10 +514,14 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
DateTime claimsPeriodEnd = END_OF_TIME;
/** An allowlist of clients allowed to be used on domains on this TLD (ignored if empty). */
@Nullable Set<String> allowedRegistrantContactIds;
@Nullable
@JsonSerialize(using = SortedSetSerializer.class)
Set<String> allowedRegistrantContactIds;
/** An allowlist of hosts allowed to be used on domains on this TLD (ignored if empty). */
@Nullable Set<String> allowedFullyQualifiedHostNames;
@Nullable
@JsonSerialize(using = SortedSetSerializer.class)
Set<String> allowedFullyQualifiedHostNames;
/**
* Indicates when the TLD is being modified using locally modified files to override the source
@@ -521,6 +546,7 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
List<VKey<AllocationToken>> defaultPromoTokens;
/** A set of allowed {@link IdnTableEnum}s for this TLD, or empty if we should use the default. */
@JsonSerialize(using = SortedEnumSetSerializer.class)
Set<IdnTableEnum> idnTables;
public String getTldStr() {
@@ -16,6 +16,8 @@ package google.registry.persistence;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import static google.registry.util.SafeSerializationUtils.safeDeserialize;
import static google.registry.util.SerializeUtils.decodeBase64;
import static java.util.function.Function.identity;
import com.google.common.base.Joiner;
@@ -97,7 +99,7 @@ public class VKey<T> extends ImmutableObject implements Serializable {
throw new IllegalArgumentException(
String.format("\"%s\" missing from the string: %s", LOOKUP_KEY, keyString));
}
return VKey.create(classType, SerializeUtils.parse(Serializable.class, kvs.get(LOOKUP_KEY)));
return VKey.create(classType, safeDeserialize(decodeBase64(kvs.get(LOOKUP_KEY))));
}
/** Returns the type of the entity. */
@@ -14,18 +14,23 @@
package google.registry.rde;
import static com.google.common.base.Preconditions.checkState;
import com.google.auto.value.AutoValue;
import google.registry.model.common.Cursor.CursorType;
import google.registry.model.rde.RdeMode;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamException;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.Optional;
import javax.annotation.Nullable;
import org.apache.beam.sdk.coders.AtomicCoder;
import org.apache.beam.sdk.coders.BooleanCoder;
import org.apache.beam.sdk.coders.NullableCoder;
import org.apache.beam.sdk.coders.SerializableCoder;
import org.apache.beam.sdk.coders.StringUtf8Coder;
import org.apache.beam.sdk.coders.VarIntCoder;
import org.joda.time.DateTime;
@@ -35,6 +40,12 @@ import org.joda.time.Duration;
* Container representing a single RDE or BRDA XML escrow deposit that needs to be created.
*
* <p>There are some {@code @Nullable} fields here because Optionals aren't Serializable.
*
* <p>Note that this class is serialized in two ways: by Beam pipelines using custom serialization
* mechanism and the {@code Coder} API, and by Java serialization when passed as command-line
* arguments (see {@code RdePipeline#decodePendingDeposits}). The latter requires safe
* deserialization because the data crosses credential boundaries (See {@code
* SafeObjectInputStream}).
*/
@AutoValue
public abstract class PendingDeposit implements Serializable {
@@ -95,11 +106,61 @@ public abstract class PendingDeposit implements Serializable {
PendingDeposit() {}
/**
* Specifies that {@link SerializedForm} be used for {@code SafeObjectInputStream}-compatible
* custom-serialization of {@link AutoValue_PendingDeposit the AutoValue implementation class}.
*
* <p>This method is package-protected so that the AutoValue implementation class inherits this
* behavior.
*
* <p>This method leverages {@link PendingDepositCoder} to serializes an instance. However, it is
* not invoked in Beam pipelines.
*/
Object writeReplace() throws ObjectStreamException {
return new SerializedForm(this);
}
/**
* Proxy for custom-serialization of {@link PendingDeposit}. This is necessary because the actual
* class to be (de)serialized is the generated AutoValue implementation. See also {@link
* #writeReplace}.
*
* <p>This class leverages {@link PendingDepositCoder} to safely deserializes an instance.
* However, it is not used in Beam pipelines.
*/
private static class SerializedForm implements Serializable {
private static final long serialVersionUID = 3141095605225904433L;
private PendingDeposit value;
private SerializedForm(PendingDeposit value) {
this.value = value;
}
private void writeObject(ObjectOutputStream os) throws IOException {
checkState(value != null, "Non-null value expected for serialization.");
PendingDepositCoder.INSTANCE.encode(value, os);
}
private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException {
checkState(value == null, "Non-null value unexpected for deserialization.");
this.value = PendingDepositCoder.INSTANCE.decode(is);
}
@SuppressWarnings("unused")
private Object readResolve() throws ObjectStreamException {
return this.value;
}
}
/**
* A deterministic coder for {@link PendingDeposit} used during a GroupBy transform.
*
* <p>We cannot use a {@link SerializableCoder} directly because it does not guarantee
* determinism, which is required by GroupBy.
* <p>We cannot use a {@code SerializableCoder} directly for two reasons: the default
* serialization does not guarantee determinism, which is required by GroupBy in Beam; and the
* default deserialization is not robust against deserialization-based attacks (See {@code
* SafeObjectInputStream} for more information).
*/
public static class PendingDepositCoder extends AtomicCoder<PendingDeposit> {
@@ -117,10 +178,15 @@ public abstract class PendingDeposit implements Serializable {
public void encode(PendingDeposit value, OutputStream outStream) throws IOException {
BooleanCoder.of().encode(value.manual(), outStream);
StringUtf8Coder.of().encode(value.tld(), outStream);
SerializableCoder.of(DateTime.class).encode(value.watermark(), outStream);
SerializableCoder.of(RdeMode.class).encode(value.mode(), outStream);
NullableCoder.of(SerializableCoder.of(CursorType.class)).encode(value.cursor(), outStream);
NullableCoder.of(SerializableCoder.of(Duration.class)).encode(value.interval(), outStream);
StringUtf8Coder.of().encode(value.watermark().toString(), outStream);
StringUtf8Coder.of().encode(value.mode().name(), outStream);
NullableCoder.of(StringUtf8Coder.of())
.encode(
Optional.ofNullable(value.cursor()).map(CursorType::name).orElse(null), outStream);
NullableCoder.of(StringUtf8Coder.of())
.encode(
Optional.ofNullable(value.interval()).map(Duration::toString).orElse(null),
outStream);
NullableCoder.of(StringUtf8Coder.of()).encode(value.directoryWithTrailingSlash(), outStream);
NullableCoder.of(VarIntCoder.of()).encode(value.revision(), outStream);
}
@@ -130,10 +196,14 @@ public abstract class PendingDeposit implements Serializable {
return new AutoValue_PendingDeposit(
BooleanCoder.of().decode(inStream),
StringUtf8Coder.of().decode(inStream),
SerializableCoder.of(DateTime.class).decode(inStream),
SerializableCoder.of(RdeMode.class).decode(inStream),
NullableCoder.of(SerializableCoder.of(CursorType.class)).decode(inStream),
NullableCoder.of(SerializableCoder.of(Duration.class)).decode(inStream),
DateTime.parse(StringUtf8Coder.of().decode(inStream)),
RdeMode.valueOf(StringUtf8Coder.of().decode(inStream)),
Optional.ofNullable(NullableCoder.of(StringUtf8Coder.of()).decode(inStream))
.map(CursorType::valueOf)
.orElse(null),
Optional.ofNullable(NullableCoder.of(StringUtf8Coder.of()).decode(inStream))
.map(Duration::parse)
.orElse(null),
NullableCoder.of(StringUtf8Coder.of()).decode(inStream),
NullableCoder.of(VarIntCoder.of()).decode(inStream));
}
@@ -53,7 +53,7 @@ public class BillingEmailUtils {
GmailClient gmailClient,
YearMonth yearMonth,
@Config("gSuiteOutgoingEmailAddress") InternetAddress outgoingEmailAddress,
@Config("alertRecipientEmailAddress") InternetAddress alertRecipientAddress,
@Config("newAlertRecipientEmailAddress") InternetAddress alertRecipientAddress,
@Config("invoiceEmailRecipients") ImmutableList<InternetAddress> invoiceEmailRecipients,
@Config("invoiceReplyToEmailAddress") Optional<InternetAddress> replyToEmailAddress,
@Config("billingBucket") String billingBucket,
@@ -37,11 +37,13 @@ import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.reporting.spec11.soy.Spec11EmailSoyInfo;
import google.registry.util.EmailMessage;
import google.registry.util.Sleeper;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
import org.joda.time.Duration;
import org.joda.time.LocalDate;
/** Provides e-mail functionality for Spec11 tasks, such as sending Spec11 reports to registrars. */
@@ -57,6 +59,8 @@ public class Spec11EmailUtils {
.build()
.compileToTofu();
private final GmailClient gmailClient;
private final Sleeper sleeper;
private final Duration emailThrottleDuration;
private final InternetAddress outgoingEmailAddress;
private final ImmutableList<InternetAddress> spec11BccEmailAddresses;
private final InternetAddress alertRecipientAddress;
@@ -66,12 +70,16 @@ public class Spec11EmailUtils {
@Inject
Spec11EmailUtils(
GmailClient gmailClient,
Sleeper sleeper,
@Config("emailThrottleDuration") Duration emailThrottleDuration,
@Config("newAlertRecipientEmailAddress") InternetAddress alertRecipientAddress,
@Config("spec11OutgoingEmailAddress") InternetAddress spec11OutgoingEmailAddress,
@Config("spec11BccEmailAddresses") ImmutableList<InternetAddress> spec11BccEmailAddresses,
@Config("spec11WebResources") ImmutableList<String> spec11WebResources,
@Config("registryName") String registryName) {
this.gmailClient = gmailClient;
this.sleeper = sleeper;
this.emailThrottleDuration = emailThrottleDuration;
this.outgoingEmailAddress = spec11OutgoingEmailAddress;
this.spec11BccEmailAddresses = spec11BccEmailAddresses;
this.alertRecipientAddress = alertRecipientAddress;
@@ -94,6 +102,13 @@ public class Spec11EmailUtils {
for (RegistrarThreatMatches registrarThreatMatches : registrarThreatMatchesSet) {
RegistrarThreatMatches filteredMatches = filterOutNonPublishedMatches(registrarThreatMatches);
if (!filteredMatches.threatMatches().isEmpty()) {
if (numRegistrarsEmailed > 0) {
try {
sleeper.sleep(emailThrottleDuration);
} catch (InterruptedException ie) {
throw new RuntimeException(ie);
}
}
try {
// Handle exceptions individually per registrar so that one failed email doesn't prevent
// the rest from being sent.
@@ -156,7 +171,7 @@ public class Spec11EmailUtils {
gmailClient.sendEmail(
EmailMessage.newBuilder()
.setSubject(subject)
.setBody(getContent(date, soyTemplateInfo, registrarThreatMatches))
.setBody(getEmailBody(date, soyTemplateInfo, registrarThreatMatches))
.setContentType(MediaType.HTML_UTF_8)
.setFrom(outgoingEmailAddress)
.addRecipient(getEmailAddressForRegistrar(registrarThreatMatches.clientId()))
@@ -164,7 +179,7 @@ public class Spec11EmailUtils {
.build());
}
private String getContent(
private String getEmailBody(
LocalDate date,
SoyTemplateInfo soyTemplateInfo,
RegistrarThreatMatches registrarThreatMatches) {
@@ -175,7 +190,7 @@ public class Spec11EmailUtils {
.map(
threatMatch ->
ImmutableMap.of(
"domainName", threatMatch.domainName(),
"domainName", toEmailSafeString(threatMatch.domainName()),
"threatType", threatMatch.threatType()))
.collect(toImmutableList());
@@ -190,6 +205,12 @@ public class Spec11EmailUtils {
return renderer.render();
}
// Mutates a known bad domain to pass spam checks by Email sender and clients, as suggested by
// the Gmail abuse-detection team.
private String toEmailSafeString(String knownUnsafeDomain) {
return knownUnsafeDomain.replace(".", "[.]");
}
/** Sends an e-mail indicating the state of the spec11 pipeline, with a given subject and body. */
void sendAlertEmail(String subject, String body) {
try {
@@ -27,6 +27,7 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import com.google.common.flogger.FluentLogger;
import google.registry.model.tld.Tld;
import google.registry.model.tld.label.PremiumList;
import google.registry.model.tld.label.PremiumListDao;
@@ -53,6 +54,8 @@ import org.yaml.snakeyaml.Yaml;
@Parameters(separators = " =", commandDescription = "Create or update TLD using YAML")
public class ConfigureTldCommand extends MutatingCommand {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@Parameter(
names = {"-i", "--input"},
description = "Filename of TLD YAML file.",
@@ -60,17 +63,28 @@ public class ConfigureTldCommand extends MutatingCommand {
required = true)
Path inputFile;
@Parameter(
names = {"-b", "--breakglass"},
description =
"Sets the breakglass field on the TLD to true, preventing Cloud Build from overwriting"
+ " these new changes until the TLD configuration file stored internally matches the"
+ " configuration in the database.")
boolean breakglass;
@Inject ObjectMapper mapper;
@Inject
@Named("dnsWriterNames")
Set<String> validDnsWriterNames;
// TODO(sarahbot@): Add a breakglass setting to this tool to indicate when a TLD has been modified
// outside of source control
/** Indicates if the passed in file contains new changes to the TLD */
boolean newDiff = true;
// TODO(sarahbot@): Add a check for diffs between passed in file and current TLD and exit if there
// is no diff. Treat nulls and empty sets as the same value.
/**
* Indicates if the existing TLD is currently in breakglass mode and should not be modified unless
* the breakglass flag is used
*/
boolean oldTldInBreakglass = false;
@Override
protected void init() throws Exception {
@@ -80,12 +94,52 @@ public class ConfigureTldCommand extends MutatingCommand {
checkForMissingFields(tldData);
Tld oldTld = getTlds().contains(name) ? Tld.get(name) : null;
Tld newTld = mapper.readValue(inputFile.toFile(), Tld.class);
if (oldTld != null) {
oldTldInBreakglass = oldTld.getBreakglassMode();
newDiff = !oldTld.equalYaml(newTld);
}
if (!newDiff && !oldTldInBreakglass) {
// Don't construct a new object if there is no new diff
return;
}
if (oldTldInBreakglass && !breakglass) {
checkArgument(
!newDiff,
"Changes can not be applied since TLD is in breakglass mode but the breakglass flag was"
+ " not used");
// if there are no new diffs, then the YAML file has caught up to the database and the
// breakglass mode should be removed
logger.atInfo().log("Breakglass mode removed from TLD: %s", name);
}
checkPremiumList(newTld);
checkDnsWriters(newTld);
checkCurrency(newTld);
// Set the new TLD to breakglass mode if breakglass flag was used
if (breakglass) {
newTld = newTld.asBuilder().setBreakglassMode(true).build();
}
stageEntityChange(oldTld, newTld);
}
@Override
protected boolean dontRunCommand() {
if (!newDiff) {
if (oldTldInBreakglass && !breakglass) {
// Run command to remove breakglass mode
return false;
}
logger.atInfo().log("TLD YAML file contains no new changes");
checkArgument(
!breakglass || oldTldInBreakglass,
"Breakglass mode can only be set when making new changes to a TLD configuration");
return true;
}
return false;
}
private void checkName(String name, Map<String, Object> tldData) {
checkArgument(CharMatcher.ascii().matchesAllOf(name), "A TLD name must be in plain ASCII");
checkArgument(!Character.isDigit(name.charAt(0)), "TLDs cannot begin with a number");
@@ -122,7 +176,9 @@ public class ConfigureTldCommand extends MutatingCommand {
private void checkPremiumList(Tld newTld) {
Optional<String> premiumListName = newTld.getPremiumListName();
if (!premiumListName.isPresent()) return;
if (!premiumListName.isPresent()) {
return;
}
Optional<PremiumList> premiumList = PremiumListDao.getLatestRevision(premiumListName.get());
checkArgument(
premiumList.isPresent(),
@@ -15,9 +15,12 @@
package google.registry.tools;
import static google.registry.tools.CommandUtilities.promptForYes;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.beust.jcommander.Parameter;
import com.google.common.base.Strings;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
/** A {@link Command} that implements a confirmation step before executing. */
public abstract class ConfirmingCommand implements Command {
@@ -27,23 +30,37 @@ public abstract class ConfirmingCommand implements Command {
description = "Do not prompt before executing")
boolean force;
public PrintStream printStream;
public PrintStream errorPrintStream;
protected ConfirmingCommand() {
try {
printStream = new PrintStream(System.out, false, UTF_8.name());
errorPrintStream = new PrintStream(System.err, false, UTF_8.name());
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
@Override
public final void run() throws Exception {
if (checkExecutionState()) {
init();
printLineIfNotEmpty(prompt());
printLineIfNotEmpty(prompt(), printStream);
if (dontRunCommand()) {
// This typically happens when all of the work is accomplished inside of prompt(), so do
// nothing further.
return;
} else if (force || promptForYes("Perform this command?")) {
System.out.println("Running ... ");
System.out.println(execute());
printLineIfNotEmpty(postExecute());
printStream.println("Running ... ");
printStream.println(execute());
printLineIfNotEmpty(postExecute(), printStream);
} else {
System.out.println("Command aborted.");
printStream.println("Command aborted.");
}
}
printStream.close();
errorPrintStream.close();
}
/** Run any pre-execute command checks and return true if they all pass. */
@@ -76,9 +93,9 @@ public abstract class ConfirmingCommand implements Command {
}
/** Prints the provided text with a trailing newline, if text is not null or empty. */
private static void printLineIfNotEmpty(String text) {
private static void printLineIfNotEmpty(String text, PrintStream printStream) {
if (!Strings.isNullOrEmpty(text)) {
System.out.println(text);
printStream.println(text);
}
}
}
@@ -78,7 +78,7 @@ final class CreateDomainCommand extends CreateOrUpdateDomainCommand {
Money createCost = prices.getCreateCost();
currency = createCost.getCurrencyUnit().getCode();
cost = createCost.multipliedBy(period).getAmount().toString();
System.out.printf(
printStream.printf(
"NOTE: %s is premium at %s per year; sending total cost for %d year(s) of %s %s.\n",
domain, createCost, period, currency, cost);
}
@@ -39,6 +39,7 @@ import google.registry.tools.params.OptionalStringParameter;
import google.registry.tools.params.StringListParameter;
import google.registry.tools.params.TransitionListParameter.BillingCostTransitions;
import google.registry.tools.params.TransitionListParameter.TldStateTransitions;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@@ -233,12 +234,11 @@ abstract class CreateOrUpdateTldCommand extends MutatingCommand {
@Nullable
@Parameter(
names = {"--num_dns_publish_locks"},
description =
"The number of publish locks we allow in parallel for DNS updates under this tld "
+ "(1 for TLD-wide locks)",
arity = 1
)
names = {"--num_dns_publish_locks"},
description =
"The number of publish locks we allow in parallel for DNS updates under this tld "
+ "(1 for TLD-wide locks)",
arity = 1)
Integer numDnsPublishShards;
@Nullable
@@ -301,7 +301,7 @@ abstract class CreateOrUpdateTldCommand extends MutatingCommand {
protected abstract void initTldCommand();
@Override
protected final void init() {
protected final void init() throws UnsupportedEncodingException {
assertAllowedEnvironment();
initTldCommand();
String duplicates = Joiner.on(", ").join(findDuplicates(mainParameters));
@@ -360,7 +360,7 @@ abstract class CreateOrUpdateTldCommand extends MutatingCommand {
if (!renewBillingCostTransitions.isEmpty()) {
// TODO(b/20764952): need invoicing support for multiple renew billing costs.
if (renewBillingCostTransitions.size() > 1) {
System.err.println(
errorPrintStream.println(
"----------------------\n"
+ "WARNING: Do not set multiple renew cost transitions "
+ "until b/20764952 is fixed.\n"
@@ -463,7 +463,8 @@ abstract class CreateOrUpdateTldCommand extends MutatingCommand {
}
}
private void checkReservedListValidityForTld(String tld, Set<String> reservedListNames) {
private void checkReservedListValidityForTld(String tld, Set<String> reservedListNames)
throws UnsupportedEncodingException {
ImmutableList.Builder<String> builder = new ImmutableList.Builder<>();
for (String reservedListName : reservedListNames) {
if (!reservedListName.startsWith("common_") && !reservedListName.startsWith(tld + "_")) {
@@ -476,7 +477,7 @@ abstract class CreateOrUpdateTldCommand extends MutatingCommand {
Joiner.on(", ").join(invalidNames),
tld);
if (overrideReservedListRules) {
System.err.println("Error overridden: " + errMsg);
errorPrintStream.println("Error overridden: " + errMsg);
} else {
throw new IllegalArgumentException(errMsg);
}
@@ -85,7 +85,7 @@ final class DeleteAllocationTokensCommand extends UpdateOrDeleteAllocationTokens
if (!dryRun) {
tm().delete(tokensToDelete);
}
System.out.printf(
printStream.printf(
"%s tokens: %s\n",
dryRun ? "Would delete" : "Deleted",
JOINER.join(tokensToDelete.stream().map(VKey::getKey).sorted().collect(toImmutableList())));
@@ -14,6 +14,7 @@
package google.registry.tools;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.collect.ImmutableMap;
@@ -86,17 +87,17 @@ class LoadTestCommand extends ConfirmingCommand implements CommandWithConnection
@Override
protected boolean checkExecutionState() {
if (RegistryToolEnvironment.get() == RegistryToolEnvironment.PRODUCTION) {
System.err.println("You may not run a load test against production.");
errorPrintStream.println("You may not run a load test against production.");
return false;
}
// Check validity of TLD and Client Id.
if (!Tlds.getTlds().contains(tld)) {
System.err.printf("No such TLD: %s\n", tld);
errorPrintStream.printf("No such TLD: %s\n", tld);
return false;
}
if (!Registrar.loadByRegistrarId(clientId).isPresent()) {
System.err.printf("No such client: %s\n", clientId);
errorPrintStream.printf("No such client: %s\n", clientId);
return false;
}
@@ -112,7 +113,7 @@ class LoadTestCommand extends ConfirmingCommand implements CommandWithConnection
@Override
protected String execute() throws Exception {
System.err.println("Initiating load test...");
errorPrintStream.println("Initiating load test...");
ImmutableMap<String, Object> params = new ImmutableMap.Builder<String, Object>()
.put("tld", tld)
@@ -71,7 +71,7 @@ public abstract class LockOrUnlockDomainCommand extends ConfirmingCommand {
}
String duplicates = Joiner.on(", ").join(findDuplicates(mainParameters));
checkArgument(duplicates.isEmpty(), "Duplicate domain arguments found: '%s'", duplicates);
System.out.println(
printStream.println(
"== ENSURE THAT YOU HAVE AUTHENTICATED THE REGISTRAR BEFORE RUNNING THIS COMMAND ==");
}
@@ -43,34 +43,41 @@ final class RegistryCli implements CommandRunner {
// The environment parameter is parsed twice: once here, and once with {@link
// RegistryToolEnvironment#parseFromArgs} in the {@link RegistryTool#main} function.
//
// The flag names must be in sync between the two, and also - this is ugly and we should feel bad.
// The flag names must be in sync between the two, and also - this is ugly, and we should feel
// bad.
@Parameter(
names = {"-e", "--environment"},
description = "Sets the default environment to run the command.")
private RegistryToolEnvironment environment = RegistryToolEnvironment.PRODUCTION;
@Parameter(
names = "--oauth",
description =
"Turn on OAuth-based authentication, the usage of which is to be deprecated. Use"
+ " `create_user` to create an Admin user that allows for OIDC-based authentication.")
private boolean oAuth = false;
@Parameter(
names = {"-c", "--commands"},
description = "Returns all command names.")
private boolean showAllCommands;
@Parameter(
names = {"--credential"},
names = "--credential",
description =
"Name of a JSON file containing credential information used by the tool. "
+ "If not set, credentials saved by running `nomulus login' will be used.")
private String credentialJson = null;
@Parameter(
names = {"--sql_access_info"},
names = "--sql_access_info",
description =
"Name of a file containing space-separated SQL access info used when deploying "
+ "Beam pipelines")
private String sqlAccessInfoFile = null;
// Do not make this final - compile-time constant inlining may interfere with JCommander.
@ParametersDelegate
private LoggingParameters loggingParams = new LoggingParameters();
@ParametersDelegate private LoggingParameters loggingParams = new LoggingParameters();
RegistryToolComponent component;
@@ -105,8 +112,8 @@ final class RegistryCli implements CommandRunner {
jcommander.setProgramName(programName);
// Create all command instances. It would be preferrable to do this in the constructor, but
// JCommander mutates the command instances and doesn't reset them so we have to do it for every
// run.
// JCommander mutates the command instances and doesn't reset them, so we have to do it for
// every run.
try {
for (Map.Entry<String, ? extends Class<? extends Command>> entry : commands.entrySet()) {
Command command = entry.getValue().getDeclaredConstructor().newInstance();
@@ -161,6 +168,7 @@ final class RegistryCli implements CommandRunner {
DaggerRegistryToolComponent.builder()
.credentialFilePath(credentialJson)
.sqlAccessInfoFile(sqlAccessInfoFile)
.addOAuthHeader(oAuth)
.build();
// JCommander stores sub-commands as nested JCommander objects containing a list of user objects
@@ -169,7 +177,7 @@ final class RegistryCli implements CommandRunner {
Command command =
(Command)
Iterables.getOnlyElement(jcommander.getCommands().get(parsedCommand).getObjects());
loggingParams.configureLogging(); // Must be called after parameters are parsed.
loggingParams.configureLogging(); // Must be called after parameters are parsed.
try {
runCommand(command);
@@ -123,6 +123,7 @@ interface RegistryToolComponent {
void inject(GetDomainCommand command);
void inject(GetHostCommand command);
void inject(GetKeyringSecretCommand command);
void inject(GetSqlCredentialCommand command);
@@ -190,6 +191,9 @@ interface RegistryToolComponent {
@BindsInstance
Builder sqlAccessInfoFile(@Nullable @Config("sqlAccessInfoFile") String sqlAccessInfoFile);
@BindsInstance
Builder addOAuthHeader(@Config("addOauthHeader") boolean addOauthHeader);
RegistryToolComponent build();
}
}
@@ -42,7 +42,8 @@ final class RequestFactoryModule {
@Provides
static HttpRequestFactory provideHttpRequestFactory(
@ApplicationDefaultCredential GoogleCredentialsBundle credentialsBundle,
@Config("oauthClientId") String oauthClientId) {
@Config("oauthClientId") String oauthClientId,
@Config("addOauthHeader") boolean addOauthHeader) {
if (RegistryConfig.areServersLocal()) {
return new NetHttpTransport()
.createRequestFactory(
@@ -54,8 +55,10 @@ final class RequestFactoryModule {
return new NetHttpTransport()
.createRequestFactory(
request -> {
// Use the standard credential initializer to set the Authorization header
credentialsBundle.getHttpRequestInitializer().initialize(request);
if (addOauthHeader) {
// Use the standard credential initializer to set the Authorization header
credentialsBundle.getHttpRequestInitializer().initialize(request);
}
// Set OIDC token as the alternative bearer token.
request
.getHeaders()
@@ -42,6 +42,7 @@ import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry.Type;
import google.registry.util.Clock;
import google.registry.util.NonFinalForTesting;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.Optional;
import javax.inject.Inject;
@@ -76,7 +77,7 @@ class UnrenewDomainCommand extends ConfirmingCommand {
StatusValue.SERVER_UPDATE_PROHIBITED);
@Override
protected void init() {
protected void init() throws UnsupportedEncodingException {
checkArgument(period >= 1 && period <= 9, "Period must be in the range 1-9");
DateTime now = clock.nowUtc();
ImmutableSet.Builder<String> domainsNonexistentBuilder = new ImmutableSet.Builder<>();
@@ -116,20 +117,24 @@ class UnrenewDomainCommand extends ConfirmingCommand {
&& domainsDeleting.isEmpty()
&& domainsWithDisallowedStatuses.isEmpty()
&& domainsExpiringTooSoon.isEmpty());
if (foundInvalidDomains) {
System.err.print("Found domains that cannot be unrenewed for the following reasons:\n\n");
errorPrintStream.print(
"Found domains that cannot be unrenewed for the following reasons:\n\n");
}
if (!domainsNonexistent.isEmpty()) {
System.err.printf("Domains that don't exist: %s\n\n", domainsNonexistent);
errorPrintStream.printf("Domains that don't exist: %s\n\n", domainsNonexistent);
}
if (!domainsDeleting.isEmpty()) {
System.err.printf("Domains that are deleted or pending delete: %s\n\n", domainsDeleting);
errorPrintStream.printf(
"Domains that are deleted or pending delete: %s\n\n", domainsDeleting);
}
if (!domainsWithDisallowedStatuses.isEmpty()) {
System.err.printf("Domains with disallowed statuses: %s\n\n", domainsWithDisallowedStatuses);
errorPrintStream.printf(
"Domains with disallowed statuses: %s\n\n", domainsWithDisallowedStatuses);
}
if (!domainsExpiringTooSoon.isEmpty()) {
System.err.printf("Domains expiring too soon: %s\n\n", domainsExpiringTooSoon);
errorPrintStream.printf("Domains expiring too soon: %s\n\n", domainsExpiringTooSoon);
}
checkArgument(!foundInvalidDomains, "Aborting because some domains cannot be unrenewed");
}
@@ -154,7 +159,7 @@ class UnrenewDomainCommand extends ConfirmingCommand {
protected String execute() {
for (String domainName : mainParameters) {
tm().transact(() -> unrenewDomain(domainName));
System.out.printf("Unrenewed %s\n", domainName);
printStream.printf("Unrenewed %s\n", domainName);
}
return "Successfully unrenewed all domains.";
}
@@ -208,7 +208,7 @@ final class UpdateAllocationTokensCommand extends UpdateOrDeleteAllocationTokens
if (!dryRun) {
tm().putAll(batch);
}
System.out.printf(
printStream.printf(
"%s tokens: %s\n",
dryRun ? "Would update" : "Updated",
JOINER.join(
@@ -70,14 +70,14 @@ public class CreateCancellationsForBillingEventsCommand extends ConfirmingComman
}
});
billingEventsToCancel = billingEventsBuilder.build();
System.out.printf("Found %d BillingEvent(s) to cancel\n", billingEventsToCancel.size());
printStream.printf("Found %d BillingEvent(s) to cancel\n", billingEventsToCancel.size());
ImmutableSet<Long> missingIds = missingIdsBuilder.build();
if (!missingIds.isEmpty()) {
System.out.printf("Missing BillingEvent(s) for IDs %s\n", missingIds);
printStream.printf("Missing BillingEvent(s) for IDs %s\n", missingIds);
}
ImmutableSet<Long> alreadyCancelledIds = alreadyCancelledIdsBuilder.build();
if (!alreadyCancelledIds.isEmpty()) {
System.out.printf(
printStream.printf(
"The following BillingEvent IDs were already cancelled: %s\n", alreadyCancelledIds);
}
}
@@ -96,7 +96,7 @@ public class CreateCancellationsForBillingEventsCommand extends ConfirmingComman
tm().transact(
() -> {
if (alreadyCancelled(billingEvent)) {
System.out.printf(
printStream.printf(
"BillingEvent %d already cancelled, this is unexpected.\n",
billingEvent.getId());
return 0;
@@ -111,7 +111,7 @@ public class CreateCancellationsForBillingEventsCommand extends ConfirmingComman
.setReason(BillingBase.Reason.ERROR)
.setTargetId(billingEvent.getTargetId())
.build());
System.out.printf(
printStream.printf(
"Added BillingCancellation for BillingEvent with ID %d\n",
billingEvent.getId());
return 1;
@@ -1,105 +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.xjc;
import static java.nio.charset.StandardCharsets.UTF_8;
import google.registry.xml.XmlException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
/**
* JAXB element wrapper for java object serialization.
*
* Instances of {@link JaxbFragment} wrap a non-serializable JAXB element instance, and provide
* hooks into the java object serialization process that allow the elements to be safely
* marshalled and unmarshalled using {@link ObjectOutputStream} and {@link ObjectInputStream},
* respectively.
*/
public class JaxbFragment<T> implements Serializable {
private static final long serialVersionUID = 5651243983008818813L;
private T instance;
/** Stores a JAXB element in a {@link JaxbFragment} */
public static <T> JaxbFragment<T> create(T object) {
JaxbFragment<T> fragment = new JaxbFragment<>();
fragment.instance = object;
return fragment;
}
/** Serializes a JAXB element into xml bytes. */
private static <T> byte[] freezeInstance(T instance) throws IOException {
try {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
XjcXmlTransformer.marshalLenient(instance, bout, UTF_8);
return bout.toByteArray();
} catch (XmlException e) {
throw new IOException(e);
}
}
/** Deserializes a JAXB element from xml bytes. */
private static <T> T unfreezeInstance(byte[] instanceData, Class<T> instanceType)
throws IOException {
try {
ByteArrayInputStream bin = new ByteArrayInputStream(instanceData);
return XjcXmlTransformer.unmarshal(instanceType, bin);
} catch (XmlException e) {
throw new IOException(e);
}
}
/**
* Retrieves the JAXB element that is wrapped by this fragment.
*/
public T getInstance() {
return instance;
}
@Override
public String toString() {
try {
return new String(freezeInstance(instance), UTF_8);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void writeObject(ObjectOutputStream out) throws IOException {
// write instanceType, then instanceData
out.writeObject(instance.getClass());
out.writeObject(freezeInstance(instance));
}
@SuppressWarnings("unchecked")
private void readObject(ObjectInputStream in) throws IOException {
// read instanceType, then instanceData
Class<T> instanceType;
byte[] instanceData;
try {
instanceType = (Class<T>) in.readObject();
instanceData = (byte[]) in.readObject();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
instance = unfreezeInstance(instanceData, instanceType);
}
}
@@ -77,8 +77,8 @@ import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTok
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.MissingRemoveDomainTokenOnBulkPricingDomainException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.RemoveDomainTokenOnNonBulkPricingDomainException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.MissingRemoveBulkPricingTokenOnBulkPricingDomainException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.RemoveBulkPricingTokenOnNonBulkPricingDomainException;
import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException;
import google.registry.model.billing.BillingBase.Flag;
import google.registry.model.billing.BillingBase.Reason;
@@ -1276,12 +1276,13 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2", "TOKEN", "token"));
EppException thrown =
assertThrows(MissingRemoveDomainTokenOnBulkPricingDomainException.class, this::runFlow);
assertThrows(
MissingRemoveBulkPricingTokenOnBulkPricingDomainException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailsToRenewBulkPricingDomainNoRemoveDomainToken() throws Exception {
void testFailsToRenewBulkPricingDomainNoRemoveBulkPricingToken() throws Exception {
AllocationToken token =
persistResource(
new AllocationToken.Builder()
@@ -1299,25 +1300,26 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
setEppInput("domain_renew.xml", ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "5"));
EppException thrown =
assertThrows(MissingRemoveDomainTokenOnBulkPricingDomainException.class, this::runFlow);
assertThrows(
MissingRemoveBulkPricingTokenOnBulkPricingDomainException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailsToRenewNonBulkPricingDomainWithRemoveDomainToken() throws Exception {
void testFailsToRenewNonBulkPricingDomainWithRemoveBulkPricingToken() throws Exception {
persistDomain();
setEppInput(
"domain_renew_allocationtoken.xml",
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2", "TOKEN", "__REMOVEDOMAIN__"));
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2", "TOKEN", "__REMOVE_BULK_PRICING__"));
EppException thrown =
assertThrows(RemoveDomainTokenOnNonBulkPricingDomainException.class, this::runFlow);
assertThrows(RemoveBulkPricingTokenOnNonBulkPricingDomainException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccesfullyAppliesRemoveDomainToken() throws Exception {
void testSuccesfullyAppliesRemoveBulkPricingToken() throws Exception {
AllocationToken token =
persistResource(
new AllocationToken.Builder()
@@ -1333,7 +1335,7 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
reloadResourceByForeignKey().asBuilder().setCurrentBulkToken(token.createVKey()).build());
setEppInput(
"domain_renew_allocationtoken.xml",
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2", "TOKEN", "__REMOVEDOMAIN__"));
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2", "TOKEN", "__REMOVE_BULK_PRICING__"));
doSuccessfulTest(
"domain_renew_response.xml",
@@ -1347,7 +1349,7 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
}
@Test
void testDryRunRemoveDomainToken() throws Exception {
void testDryRunRemoveBulkPricingToken() throws Exception {
AllocationToken token =
persistResource(
new AllocationToken.Builder()
@@ -1364,7 +1366,7 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
setEppInput(
"domain_renew_allocationtoken.xml",
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2", "TOKEN", "__REMOVEDOMAIN__"));
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2", "TOKEN", "__REMOVE_BULK_PRICING__"));
dryRunFlowAssertResponse(
loadFile(
@@ -34,7 +34,7 @@ import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.LowLevelHttpRequest;
import com.google.api.client.http.LowLevelHttpResponse;
import com.google.api.client.json.Json;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.client.testing.http.HttpTesting;
import com.google.api.client.testing.http.MockHttpTransport;
import com.google.api.client.testing.http.MockLowLevelHttpRequest;
@@ -300,6 +300,6 @@ class DirectoryGroupsConnectionTest {
HttpRequest request = transport.createRequestFactory()
.buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL)
.setThrowExceptionOnExecuteError(false);
return GoogleJsonResponseException.from(new JacksonFactory(), request.execute());
return GoogleJsonResponseException.from(new GsonFactory(), request.execute());
}
}
@@ -30,13 +30,11 @@ public class UserDaoTest extends EntityTestCase {
User user1 =
new User.Builder()
.setEmailAddress("email@email.com")
.setGaiaId("gaiaId")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.SUPPORT_AGENT).build())
.build();
User user2 =
new User.Builder()
.setEmailAddress("foo@bar.com")
.setGaiaId("otherId")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.SUPPORT_AGENT).build())
.build();
UserDao.saveUser(user1);
@@ -54,7 +52,6 @@ public class UserDaoTest extends EntityTestCase {
User user =
new User.Builder()
.setEmailAddress("email@email.com")
.setGaiaId("gaiaId")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.SUPPORT_AGENT).build())
.build();
UserDao.saveUser(user);
@@ -71,13 +68,11 @@ public class UserDaoTest extends EntityTestCase {
User user1 =
new User.Builder()
.setEmailAddress("email@email.com")
.setGaiaId("gaiaId")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.SUPPORT_AGENT).build())
.build();
User user2 =
new User.Builder()
.setEmailAddress("email@email.com")
.setGaiaId("otherId")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.SUPPORT_AGENT).build())
.build();
UserDao.saveUser(user1);
@@ -30,10 +30,9 @@ public class UserTest extends EntityTestCase {
}
@Test
void testPersistence_lookupByGaiaId() {
void testPersistence_lookupByEmail() {
User user =
new User.Builder()
.setGaiaId("gaiaId")
.setEmailAddress("email@email.com")
.setUserRoles(
new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).setIsAdmin(true).build())
@@ -43,10 +42,11 @@ public class UserTest extends EntityTestCase {
() -> {
assertAboutImmutableObjects()
.that(
tm().query("FROM User WHERE gaiaId = 'gaiaId'", User.class).getSingleResult())
tm().query("FROM User WHERE emailAddress = 'email@email.com'", User.class)
.getSingleResult())
.isEqualExceptFields(user, "id", "updateTimestamp");
assertThat(
tm().query("FROM User WHERE gaiaId = 'badGaiaId'", User.class)
tm().query("FROM User WHERE emailAddress = 'nobody@email.com'", User.class)
.getResultList())
.isEmpty();
});
@@ -55,9 +55,6 @@ public class UserTest extends EntityTestCase {
@Test
void testFailure_badInputs() {
User.Builder builder = new User.Builder();
assertThat(assertThrows(IllegalArgumentException.class, () -> builder.setGaiaId(null)))
.hasMessageThat()
.isEqualTo("Gaia ID cannot be null or empty");
assertThat(assertThrows(IllegalArgumentException.class, () -> builder.setEmailAddress("")))
.hasMessageThat()
.isEqualTo("Provided email is not a valid email address");
@@ -72,7 +69,7 @@ public class UserTest extends EntityTestCase {
assertThat(assertThrows(IllegalArgumentException.class, () -> builder.setUserRoles(null)))
.hasMessageThat()
.isEqualTo("User roles cannot be null");
assertThat(assertThrows(IllegalArgumentException.class, builder::build))
.hasMessageThat()
.isEqualTo("Email address cannot be null");
@@ -99,7 +96,6 @@ public class UserTest extends EntityTestCase {
User user =
new User.Builder()
.setGaiaId("gaiaId")
.setEmailAddress("email@email.com")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build())
.build();
@@ -0,0 +1,59 @@
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.rde;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.common.Cursor.CursorType.RDE_STAGING;
import static google.registry.model.rde.RdeMode.FULL;
import static google.registry.util.SafeSerializationUtils.safeDeserialize;
import static google.registry.util.SerializeUtils.deserialize;
import static google.registry.util.SerializeUtils.serialize;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link PendingDeposit}. */
public class PendingDepositTest {
private final DateTime now = DateTime.parse("2000-01-01TZ");
PendingDeposit pendingDeposit =
PendingDeposit.create("soy", now, FULL, RDE_STAGING, Duration.standardDays(1));
PendingDeposit manualPendingDeposit =
PendingDeposit.createInManualOperation("soy", now, FULL, "/", null);
@Test
void deserialize_normalDeposit_success() {
assertThat(deserialize(PendingDeposit.class, serialize(pendingDeposit)))
.isEqualTo(pendingDeposit);
}
@Test
void deserialize_manualDeposit_success() {
assertThat(deserialize(PendingDeposit.class, serialize(manualPendingDeposit)))
.isEqualTo(manualPendingDeposit);
}
@Test
void safeDeserialize_normalDeposit_success() {
assertThat(safeDeserialize(serialize(pendingDeposit))).isEqualTo(pendingDeposit);
}
@Test
void safeDeserialize_manualDeposit_success() {
assertThat(safeDeserialize(serialize(manualPendingDeposit))).isEqualTo(manualPendingDeposit);
}
}
@@ -26,6 +26,8 @@ import static google.registry.testing.DatabaseHelper.loadByEntity;
import static google.registry.testing.DatabaseHelper.persistActiveHost;
import static google.registry.testing.DatabaseHelper.persistResource;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -41,11 +43,13 @@ import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationT
import google.registry.reporting.spec11.soy.Spec11EmailSoyInfo;
import google.registry.testing.DatabaseHelper;
import google.registry.util.EmailMessage;
import google.registry.util.Sleeper;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
import org.joda.time.Duration;
import org.joda.time.LocalDate;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -101,6 +105,8 @@ class Spec11EmailUtilsTest {
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
@Mock private GmailClient gmailClient;
@Mock private Sleeper sleeper;
private Duration emailThrottleDuration = Duration.millis(1);
private Spec11EmailUtils emailUtils;
private ArgumentCaptor<EmailMessage> contentCaptor;
private final LocalDate date = new LocalDate(2018, 7, 15);
@@ -114,6 +120,8 @@ class Spec11EmailUtilsTest {
emailUtils =
new Spec11EmailUtils(
gmailClient,
sleeper,
emailThrottleDuration,
new InternetAddress("my-receiver@test.com"),
new InternetAddress("abuse@test.com"),
ImmutableList.of(
@@ -128,6 +136,19 @@ class Spec11EmailUtilsTest {
persistDomainWithHost("c.com", host);
}
@Test
void testSuccess_sleepsBetweenSending() throws Exception {
emailUtils.emailSpec11Reports(
date,
Spec11EmailSoyInfo.MONTHLY_SPEC_11_EMAIL,
"Super Cool Registry Monthly Threat Detector [2018-07-15]",
sampleThreatMatches());
// We inspect individual parameters because Message doesn't implement equals().
verify(gmailClient, times(3)).sendEmail(any(EmailMessage.class));
// Sleep once between two reports sent in a tight loop. No sleep before the final alert message.
verify(sleeper, times(1)).sleep(same(emailThrottleDuration));
}
@Test
void testSuccess_emailMonthlySpec11Reports() throws Exception {
emailUtils.emailSpec11Reports(
@@ -144,7 +165,7 @@ class Spec11EmailUtilsTest {
"the.registrar@example.com",
ImmutableList.of("abuse@test.com", "bcc@test.com"),
"Super Cool Registry Monthly Threat Detector [2018-07-15]",
String.format(MONTHLY_EMAIL_FORMAT, "<tr><td>a.com</td><td>MALWARE</td></tr>"),
String.format(MONTHLY_EMAIL_FORMAT, "<tr><td>a[.]com</td><td>MALWARE</td></tr>"),
Optional.of(MediaType.HTML_UTF_8));
validateMessage(
capturedContents.get(1),
@@ -154,7 +175,7 @@ class Spec11EmailUtilsTest {
"Super Cool Registry Monthly Threat Detector [2018-07-15]",
String.format(
MONTHLY_EMAIL_FORMAT,
"<tr><td>b.com</td><td>MALWARE</td></tr><tr><td>c.com</td><td>MALWARE</td></tr>"),
"<tr><td>b[.]com</td><td>MALWARE</td></tr><tr><td>c[.]com</td><td>MALWARE</td></tr>"),
Optional.of(MediaType.HTML_UTF_8));
validateMessage(
capturedContents.get(2),
@@ -182,7 +203,7 @@ class Spec11EmailUtilsTest {
"the.registrar@example.com",
ImmutableList.of("abuse@test.com", "bcc@test.com"),
"Super Cool Registry Daily Threat Detector [2018-07-15]",
String.format(DAILY_EMAIL_FORMAT, "<tr><td>a.com</td><td>MALWARE</td></tr>"),
String.format(DAILY_EMAIL_FORMAT, "<tr><td>a[.]com</td><td>MALWARE</td></tr>"),
Optional.of(MediaType.HTML_UTF_8));
validateMessage(
capturedMessages.get(1),
@@ -192,7 +213,7 @@ class Spec11EmailUtilsTest {
"Super Cool Registry Daily Threat Detector [2018-07-15]",
String.format(
DAILY_EMAIL_FORMAT,
"<tr><td>b.com</td><td>MALWARE</td></tr><tr><td>c.com</td><td>MALWARE</td></tr>"),
"<tr><td>b[.]com</td><td>MALWARE</td></tr><tr><td>c[.]com</td><td>MALWARE</td></tr>"),
Optional.of(MediaType.HTML_UTF_8));
validateMessage(
capturedMessages.get(2),
@@ -223,7 +244,7 @@ class Spec11EmailUtilsTest {
"new.registrar@example.com",
ImmutableList.of("abuse@test.com", "bcc@test.com"),
"Super Cool Registry Monthly Threat Detector [2018-07-15]",
String.format(MONTHLY_EMAIL_FORMAT, "<tr><td>c.com</td><td>MALWARE</td></tr>"),
String.format(MONTHLY_EMAIL_FORMAT, "<tr><td>c[.]com</td><td>MALWARE</td></tr>"),
Optional.of(MediaType.HTML_UTF_8));
validateMessage(
capturedContents.get(1),
@@ -256,7 +277,7 @@ class Spec11EmailUtilsTest {
"the.registrar@example.com",
ImmutableList.of("abuse@test.com", "bcc@test.com"),
"Super Cool Registry Monthly Threat Detector [2018-07-15]",
String.format(MONTHLY_EMAIL_FORMAT, "<tr><td>a.com</td><td>MALWARE</td></tr>"),
String.format(MONTHLY_EMAIL_FORMAT, "<tr><td>a[.]com</td><td>MALWARE</td></tr>"),
Optional.of(MediaType.HTML_UTF_8));
validateMessage(
capturedContents.get(1),
@@ -266,7 +287,7 @@ class Spec11EmailUtilsTest {
"Super Cool Registry Monthly Threat Detector [2018-07-15]",
String.format(
MONTHLY_EMAIL_FORMAT,
"<tr><td>b.com</td><td>MALWARE</td></tr><tr><td>c.com</td><td>MALWARE</td></tr>"),
"<tr><td>b[.]com</td><td>MALWARE</td></tr><tr><td>c[.]com</td><td>MALWARE</td></tr>"),
Optional.of(MediaType.HTML_UTF_8));
validateMessage(
capturedContents.get(2),
@@ -311,7 +332,7 @@ class Spec11EmailUtilsTest {
"the.registrar@example.com",
ImmutableList.of("abuse@test.com", "bcc@test.com"),
"Super Cool Registry Monthly Threat Detector [2018-07-15]",
String.format(MONTHLY_EMAIL_FORMAT, "<tr><td>a.com</td><td>MALWARE</td></tr>"),
String.format(MONTHLY_EMAIL_FORMAT, "<tr><td>a[.]com</td><td>MALWARE</td></tr>"),
Optional.of(MediaType.HTML_UTF_8));
validateMessage(
capturedMessages.get(1),
@@ -321,7 +342,7 @@ class Spec11EmailUtilsTest {
"Super Cool Registry Monthly Threat Detector [2018-07-15]",
String.format(
MONTHLY_EMAIL_FORMAT,
"<tr><td>b.com</td><td>MALWARE</td></tr><tr><td>c.com</td><td>MALWARE</td></tr>"),
"<tr><td>b[.]com</td><td>MALWARE</td></tr><tr><td>c[.]com</td><td>MALWARE</td></tr>"),
Optional.of(MediaType.HTML_UTF_8));
validateMessage(
capturedMessages.get(2),
@@ -417,7 +417,6 @@ class AuthenticatedRegistrarAccessorTest {
void testConsoleUser_admin() {
google.registry.model.console.User consoleUser =
new google.registry.model.console.User.Builder()
.setGaiaId("gaiaId")
.setEmailAddress("email@email.com")
.setUserRoles(
new UserRoles.Builder().setIsAdmin(true).setGlobalRole(GlobalRole.FTE).build())
@@ -444,7 +443,6 @@ class AuthenticatedRegistrarAccessorTest {
// not admins
google.registry.model.console.User consoleUser =
new google.registry.model.console.User.Builder()
.setGaiaId("gaiaId")
.setEmailAddress("email@email.com")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.SUPPORT_AGENT).build())
.build();
@@ -462,7 +460,6 @@ class AuthenticatedRegistrarAccessorTest {
// Registrar employees should have OWNER access to their registrars
google.registry.model.console.User consoleUser =
new google.registry.model.console.User.Builder()
.setGaiaId("gaiaId")
.setEmailAddress("email@email.com")
.setUserRoles(
new UserRoles.Builder()
@@ -61,7 +61,6 @@ public class OidcTokenAuthenticationMechanismTest {
private final User user =
new User.Builder()
.setEmailAddress(email)
.setGaiaId(gaiaId)
.setUserRoles(
new UserRoles.Builder().setIsAdmin(true).setGlobalRole(GlobalRole.FTE).build())
.build();
@@ -141,7 +140,6 @@ public class OidcTokenAuthenticationMechanismTest {
User serviceUser =
new User.Builder()
.setEmailAddress("service@email.test")
.setGaiaId("service-gaia-id")
.setUserRoles(
new UserRoles.Builder().setIsAdmin(true).setGlobalRole(GlobalRole.FTE).build())
.build();
@@ -61,7 +61,7 @@ import org.junit.runner.RunWith;
* and have at least one test method that persists a JPA entity declared in persistence.xml.
*
* <p>Note that with {@link JpaIntegrationWithCoverageExtension}, each method starts with an empty
* database. Therefore this is not the right place for verifying backwards data compatibility in
* database. Therefore, this is not the right place for verifying backwards data compatibility in
* end-to-end functional tests.
*
* <p>As of April 2020, none of the before/after annotations ({@code BeforeClass} and {@code
@@ -107,7 +107,9 @@ import org.junit.runner.RunWith;
// AfterSuiteTest must be the last entry. See class javadoc for details.
AfterSuiteTest.class
})
public class SqlIntegrationTestSuite {
public final class SqlIntegrationTestSuite {
private SqlIntegrationTestSuite() {}
@BeforeAll // Not yet supported in JUnit 5. Called through BeforeSuiteTest.
public static void initJpaEntityCoverage() {
@@ -144,7 +144,6 @@ public final class RegistryTestServerMain {
User user =
new User.Builder()
.setEmailAddress(loginEmail)
.setGaiaId("123457890")
.setUserRoles(userRoles)
.setRegistryLockPassword("registryLockPassword")
.build();
@@ -26,7 +26,7 @@ import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.client.util.store.AbstractDataStoreFactory;
import com.google.api.client.util.store.DataStore;
import com.google.common.collect.ImmutableList;
@@ -74,7 +74,7 @@ class AuthModuleTest {
// We need to set the following fields because they are checked when
// Credential#setRefreshToken is called. However they are not actually persisted in the
// DataStore and not actually used in tests.
.setJsonFactory(new JacksonFactory())
.setJsonFactory(new GsonFactory())
.setTransport(new NetHttpTransport())
.setTokenServerUrl(new GenericUrl("https://accounts.google.com/o/oauth2/token"))
.setClientAuthentication(new ClientParametersAuthentication(CLIENT_ID, CLIENT_SECRET))
@@ -146,7 +146,7 @@ class AuthModuleTest {
private Credential getCredential() {
// Reconstruct the entire dependency graph, injecting FakeDataStoreFactory and credential
// parameters.
JacksonFactory jsonFactory = new JacksonFactory();
GsonFactory jsonFactory = new GsonFactory();
GoogleClientSecrets clientSecrets = getSecrets();
ImmutableList<String> scopes = ImmutableList.of("scope1");
return AuthModule.provideCredential(
@@ -198,7 +198,7 @@ public abstract class CommandTestCase<C extends Command> {
}
void assertInStderr(String... expected) {
String stderror = new String(stderr.toByteArray(), UTF_8);
String stderror = getStderrAsString();
for (String line : expected) {
assertThat(stderror).contains(line);
}
@@ -14,14 +14,18 @@
package google.registry.tools;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.EntityYamlUtils.createObjectMapper;
import static google.registry.model.domain.token.AllocationToken.TokenType.DEFAULT_PROMO;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistPremiumList;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.LogsSubject.assertAboutLogs;
import static google.registry.testing.TestDataHelper.loadFile;
import static google.registry.tldconfig.idn.IdnTableEnum.EXTENDED_LATIN;
import static google.registry.tldconfig.idn.IdnTableEnum.JA;
import static google.registry.tldconfig.idn.IdnTableEnum.UNCONFUSABLE_LATIN;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.logging.Level.INFO;
import static org.joda.money.CurrencyUnit.JPY;
import static org.joda.money.CurrencyUnit.USD;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -34,12 +38,13 @@ import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.Files;
import google.registry.model.EntityYamlUtils;
import com.google.common.testing.TestLogHandler;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.tld.Tld;
import google.registry.model.tld.label.PremiumList;
import google.registry.model.tld.label.PremiumListDao;
import java.io.File;
import java.util.logging.Logger;
import org.joda.money.Money;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
@@ -50,13 +55,16 @@ import org.testcontainers.shaded.com.google.common.collect.ImmutableMap;
public class ConfigureTldCommandTest extends CommandTestCase<ConfigureTldCommand> {
PremiumList premiumList;
ObjectMapper objectMapper = EntityYamlUtils.createObjectMapper();
ObjectMapper objectMapper = createObjectMapper();
private final TestLogHandler logHandler = new TestLogHandler();
private final Logger logger = Logger.getLogger(ConfigureTldCommand.class.getCanonicalName());
@BeforeEach
void beforeEach() {
command.mapper = objectMapper;
premiumList = persistPremiumList("test", USD, "silver,USD 50", "gold,USD 80");
command.validDnsWriterNames = ImmutableSet.of("VoidDnsWriter", "FooDnsWriter");
logger.addHandler(logHandler);
}
private void testTldConfiguredSuccessfully(Tld tld, String filename)
@@ -75,6 +83,7 @@ public class ConfigureTldCommandTest extends CommandTestCase<ConfigureTldCommand
assertThat(tld.getDriveFolderId()).isEqualTo("driveFolder");
assertThat(tld.getCreateBillingCost()).isEqualTo(Money.of(USD, 25));
testTldConfiguredSuccessfully(tld, "tld.yaml");
assertThat(tld.getBreakglassMode()).isFalse();
}
@Test
@@ -87,6 +96,23 @@ public class ConfigureTldCommandTest extends CommandTestCase<ConfigureTldCommand
Tld updatedTld = Tld.get("tld");
assertThat(updatedTld.getCreateBillingCost()).isEqualTo(Money.of(USD, 25));
testTldConfiguredSuccessfully(updatedTld, "tld.yaml");
assertThat(updatedTld.getBreakglassMode()).isFalse();
}
@Test
void testSuccess_noDiff() throws Exception {
Tld tld = createTld("idns");
persistResource(
tld.asBuilder()
.setIdnTables(ImmutableSet.of(JA, UNCONFUSABLE_LATIN, EXTENDED_LATIN))
.setAllowedFullyQualifiedHostNames(ImmutableSet.of("zeta", "alpha", "gamma", "beta"))
.build());
File tldFile = tmpDir.resolve("idns.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "idns.yaml"));
runCommandForced("--input=" + tldFile);
assertAboutLogs()
.that(logHandler)
.hasLogAtLevelWithMessage(INFO, "TLD YAML file contains no new changes");
}
@Test
@@ -447,4 +473,101 @@ public class ConfigureTldCommandTest extends CommandTestCase<ConfigureTldCommand
assertThrows(IllegalArgumentException.class, () -> runCommandForced("--input=" + tldFile));
assertThat(thrown.getMessage()).isEqualTo("The premium list must use the TLD's currency");
}
@Test
void testFailure_breakglassFlag_NoChanges() throws Exception {
Tld tld = createTld("idns");
persistResource(
tld.asBuilder()
.setIdnTables(ImmutableSet.of(JA, UNCONFUSABLE_LATIN, EXTENDED_LATIN))
.setAllowedFullyQualifiedHostNames(ImmutableSet.of("zeta", "alpha", "gamma", "beta"))
.build());
File tldFile = tmpDir.resolve("idns.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "idns.yaml"));
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class, () -> runCommandForced("--input=" + tldFile, "-b"));
assertThat(thrown.getMessage())
.isEqualTo(
"Breakglass mode can only be set when making new changes to a TLD configuration");
}
@Test
void testSuccess_breakglassFlag_startsBreakglassMode() throws Exception {
Tld tld = createTld("tld");
assertThat(tld.getCreateBillingCost()).isEqualTo(Money.of(USD, 13));
File tldFile = tmpDir.resolve("tld.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "tld.yaml"));
runCommandForced("--input=" + tldFile, "--breakglass");
Tld updatedTld = Tld.get("tld");
assertThat(updatedTld.getCreateBillingCost()).isEqualTo(Money.of(USD, 25));
testTldConfiguredSuccessfully(updatedTld, "tld.yaml");
assertThat(updatedTld.getBreakglassMode()).isTrue();
}
@Test
void testSuccess_breakglassFlag_continuesBreakglassMode() throws Exception {
Tld tld = createTld("tld");
assertThat(tld.getCreateBillingCost()).isEqualTo(Money.of(USD, 13));
persistResource(tld.asBuilder().setBreakglassMode(true).build());
File tldFile = tmpDir.resolve("tld.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "tld.yaml"));
runCommandForced("--input=" + tldFile, "--breakglass");
Tld updatedTld = Tld.get("tld");
assertThat(updatedTld.getCreateBillingCost()).isEqualTo(Money.of(USD, 25));
testTldConfiguredSuccessfully(updatedTld, "tld.yaml");
assertThat(updatedTld.getBreakglassMode()).isTrue();
}
@Test
void testSuccess_NoDiffNoBreakglassFlag_endsBreakglassMode() throws Exception {
Tld tld = createTld("idns");
persistResource(
tld.asBuilder()
.setIdnTables(ImmutableSet.of(JA, UNCONFUSABLE_LATIN, EXTENDED_LATIN))
.setAllowedFullyQualifiedHostNames(ImmutableSet.of("zeta", "alpha", "gamma", "beta"))
.setBreakglassMode(true)
.build());
File tldFile = tmpDir.resolve("idns.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "idns.yaml"));
runCommandForced("--input=" + tldFile);
Tld updatedTld = Tld.get("idns");
assertThat(updatedTld.getBreakglassMode()).isFalse();
assertAboutLogs()
.that(logHandler)
.hasLogAtLevelWithMessage(INFO, "Breakglass mode removed from TLD: idns");
}
@Test
void testSuccess_noDiffBreakglassFlag_continuesBreakglassMode() throws Exception {
Tld tld = createTld("idns");
persistResource(
tld.asBuilder()
.setIdnTables(ImmutableSet.of(JA, UNCONFUSABLE_LATIN, EXTENDED_LATIN))
.setAllowedFullyQualifiedHostNames(ImmutableSet.of("zeta", "alpha", "gamma", "beta"))
.setBreakglassMode(true)
.build());
File tldFile = tmpDir.resolve("idns.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "idns.yaml"));
runCommandForced("--input=" + tldFile, "-b");
Tld updatedTld = Tld.get("idns");
assertThat(updatedTld.getBreakglassMode()).isTrue();
assertAboutLogs()
.that(logHandler)
.hasLogAtLevelWithMessage(INFO, "TLD YAML file contains no new changes");
}
@Test
void testFailure_noBreakglassFlag_inBreakglassMode() throws Exception {
Tld tld = createTld("tld");
persistResource(tld.asBuilder().setBreakglassMode(true).build());
File tldFile = tmpDir.resolve("tld.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "tld.yaml"));
IllegalArgumentException thrown =
assertThrows(IllegalArgumentException.class, () -> runCommandForced("--input=" + tldFile));
assertThat(thrown.getMessage())
.isEqualTo(
"Changes can not be applied since TLD is in breakglass mode but the breakglass flag"
+ " was not used");
}
}
@@ -38,6 +38,7 @@ class CreateDomainCommandTest extends EppToolCommandTestCase<CreateDomainCommand
@BeforeEach
void beforeEach() {
command.passwordGenerator = new DeterministicStringGenerator("abcdefghijklmnopqrstuvwxyz");
command.printStream = System.out;
}
@Test
@@ -68,6 +68,7 @@ class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarCommand>
2048,
ImmutableSet.of("secp256r1", "secp384r1"),
fakeClock);
command.printStream = System.out;
}
@Test
@@ -37,6 +37,7 @@ class CreateRegistrarGroupsCommandTest extends CommandTestCase<CreateRegistrarGr
@Test
void test_createGroupsForTwoRegistrars() throws Exception {
command.printStream = System.out;
runCommandForced("NewRegistrar", "TheRegistrar");
verify(connection)
.sendPostRequest(
@@ -521,6 +521,7 @@ class CreateTldCommandTest extends CommandTestCase<CreateTldCommand> {
@Test
void testSuccess_setCommonAndReservedListFromOtherTld_withOverride() throws Exception {
command.errorPrintStream = System.err;
runReservedListsTestOverride("common_abuse,tld_banned");
String errMsg =
"Error overridden: The reserved list(s) tld_banned "
@@ -53,6 +53,7 @@ class DeleteAllocationTokensCommandTest extends CommandTestCase<DeleteAllocation
preNot2 = persistToken("prefix8ZZZhs8", null, false);
othrRed = persistToken("h97987sasdfhh", null, true);
othrNot = persistToken("asdgfho7HASDS", null, false);
command.printStream = System.out;
}
@Test
@@ -45,7 +45,7 @@ class GetTldCommandTest extends CommandTestCase<GetTldCommand> {
PremiumList premiumList = persistPremiumList("test", USD, "silver,USD 50", "gold,USD 80");
persistResource(
tld.asBuilder()
.setDnsAPlusAaaaTtl(Duration.millis(900))
.setDnsAPlusAaaaTtl(Duration.standardMinutes(15))
.setDriveFolderId("driveFolder")
.setCreateBillingCost(Money.of(USD, 25))
.setPremiumList(premiumList)
@@ -37,6 +37,8 @@ class LoadTestCommandTest extends CommandTestCase<LoadTestCommand> {
command.setConnection(connection);
createTld("example");
persistNewRegistrar("acme", "ACME", Registrar.Type.REAL, 99L);
command.printStream = System.out;
command.errorPrintStream = System.err;
}
@Test
@@ -50,6 +50,7 @@ class LockDomainCommandTest extends CommandTestCase<LockDomainCommand> {
new DeterministicStringGenerator(Alphabets.BASE_58),
"adminreg",
new CloudTasksHelper(fakeClock).getTestCloudTasksUtils());
command.printStream = System.out;
}
@Test
@@ -64,7 +64,7 @@ public class RequestFactoryModuleTest {
RegistryConfig.CONFIG_SETTINGS.get().gcpProject.isLocal = true;
try {
HttpRequestFactory factory =
RequestFactoryModule.provideHttpRequestFactory(credentialsBundle, "client-id");
RequestFactoryModule.provideHttpRequestFactory(credentialsBundle, "client-id", false);
HttpRequestInitializer initializer = factory.getInitializer();
assertThat(initializer).isNotNull();
HttpRequest request = factory.buildGetRequest(new GenericUrl("http://localhost"));
@@ -97,14 +97,23 @@ public class RequestFactoryModuleTest {
boolean origIsLocal = RegistryConfig.CONFIG_SETTINGS.get().gcpProject.isLocal;
RegistryConfig.CONFIG_SETTINGS.get().gcpProject.isLocal = false;
try {
// With OAuth header.
HttpRequestFactory factory =
RequestFactoryModule.provideHttpRequestFactory(credentialsBundle, "clientId");
RequestFactoryModule.provideHttpRequestFactory(credentialsBundle, "clientId", true);
HttpRequest request = factory.buildGetRequest(new GenericUrl("http://localhost"));
assertThat(request.getHeaders().get("Proxy-Authorization")).isEqualTo("Bearer oidc.token");
assertThat(request.getConnectTimeout()).isEqualTo(REQUEST_TIMEOUT_MS);
assertThat(request.getReadTimeout()).isEqualTo(REQUEST_TIMEOUT_MS);
verify(httpRequestInitializer).initialize(request);
verifyNoMoreInteractions(httpRequestInitializer);
// No OAuth header.
factory =
RequestFactoryModule.provideHttpRequestFactory(credentialsBundle, "clientId", false);
request = factory.buildGetRequest(new GenericUrl("http://localhost"));
assertThat(request.getHeaders().get("Proxy-Authorization")).isEqualTo("Bearer oidc.token");
assertThat(request.getConnectTimeout()).isEqualTo(REQUEST_TIMEOUT_MS);
assertThat(request.getReadTimeout()).isEqualTo(REQUEST_TIMEOUT_MS);
verifyNoMoreInteractions(httpRequestInitializer);
} finally {
RegistryConfig.CONFIG_SETTINGS.get().gcpProject.isLocal = origIsLocal;
}
@@ -61,6 +61,7 @@ class UniformRapidSuspensionCommandTest
ImmutableSet.of(
DomainDsData.create(1, 2, 3, new HexBinaryAdapter().unmarshal("dead")),
DomainDsData.create(4, 5, 6, new HexBinaryAdapter().unmarshal("beef")));
command.printStream = System.out;
}
private void persistDomainWithHosts(
@@ -53,6 +53,7 @@ class UnlockDomainCommandTest extends CommandTestCase<UnlockDomainCommand> {
new DeterministicStringGenerator(Alphabets.BASE_58),
"adminreg",
new CloudTasksHelper(fakeClock).getTestCloudTasksUtils());
command.printStream = System.out;
}
private Domain persistLockedDomain(String domainName, String registrarId) {
@@ -55,6 +55,7 @@ public class UnrenewDomainCommandTest extends CommandTestCase<UnrenewDomainComma
createTld("tld");
fakeClock.setTo(DateTime.parse("2016-12-06T13:55:01Z"));
command.clock = fakeClock;
command.printStream = System.out;
}
@Test
@@ -178,6 +179,7 @@ public class UnrenewDomainCommandTest extends CommandTestCase<UnrenewDomainComma
@Test
void test_varietyOfInvalidDomains_displaysErrors() {
command.errorPrintStream = System.err;
DateTime now = fakeClock.nowUtc();
persistResource(
DatabaseHelper.newDomain("deleting.tld")
@@ -1048,6 +1048,7 @@ class UpdateTldCommandTest extends CommandTestCase<UpdateTldCommand> {
@Test
void testSuccess_setCommonAndReservedListFromOtherTld_withOverride() throws Exception {
command.errorPrintStream = System.err;
runReservedListsTestOverride("common_abuse,tld_banned");
String errMsg =
"Error overridden: The reserved list(s) tld_banned "
@@ -34,6 +34,7 @@ import google.registry.model.reporting.HistoryEntryDao;
import google.registry.persistence.VKey;
import google.registry.testing.DatabaseHelper;
import google.registry.tools.CommandTestCase;
import java.io.PrintStream;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.junit.jupiter.api.BeforeEach;
@@ -59,6 +60,7 @@ public class CreateCancellationsForBillingEventsCommandTest
fakeClock.nowUtc(),
fakeClock.nowUtc().plusYears(2));
billingEventToCancel = createBillingEvent();
command.printStream = System.out;
}
@Test
@@ -97,8 +99,10 @@ public class CreateCancellationsForBillingEventsCommandTest
@Test
void testAlreadyCancelled() throws Exception {
// multiple runs / cancellations should be a no-op
command.printStream = new PrintStream(tmpDir.resolve("test.txt").toFile());
runCommandForced(String.valueOf(billingEventToCancel.getId()));
assertBillingEventCancelled();
command.printStream = System.out;
runCommandForced(String.valueOf(billingEventToCancel.getId()));
assertBillingEventCancelled();
assertThat(DatabaseHelper.loadAllOf(BillingCancellation.class)).hasSize(1);
@@ -128,7 +128,6 @@ public class ConsoleDomainGetActionTest {
private User createUser(UserRoles userRoles) {
return new User.Builder()
.setEmailAddress("email@email.com")
.setGaiaId("gaiaId")
.setUserRoles(userRoles)
.build();
}
@@ -15,14 +15,12 @@
package google.registry.ui.server.console;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import com.google.api.client.http.HttpStatusCodes;
import com.google.gson.Gson;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.request.RequestModule;
import google.registry.request.auth.AuthResult;
@@ -31,15 +29,12 @@ import google.registry.request.auth.UserAuthInfo;
import google.registry.testing.FakeResponse;
import java.io.IOException;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Tests for {@link google.registry.ui.server.console.ConsoleUserDataAction}. */
class ConsoleUserDataActionTest {
private final HttpServletRequest request = mock(HttpServletRequest.class);
private RegistrarPoc testRegistrarPoc;
private static final Gson GSON = RequestModule.provideGson();
private FakeResponse response = new FakeResponse();
@@ -52,7 +47,6 @@ class ConsoleUserDataActionTest {
User user =
new User.Builder()
.setEmailAddress("email@email.com")
.setGaiaId("gaiaId")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build())
.build();
@@ -232,7 +232,6 @@ class RegistrarsActionTest {
private User createUser(UserRoles userRoles) {
return new User.Builder()
.setEmailAddress("email@email.com")
.setGaiaId("gaiaId")
.setUserRoles(userRoles)
.build();
}
@@ -226,7 +226,6 @@ class ContactActionTest {
private User createUser(UserRoles userRoles) {
return new User.Builder()
.setEmailAddress("email@email.com")
.setGaiaId("gaiaId")
.setUserRoles(userRoles)
.build();
}
@@ -109,7 +109,6 @@ class SecurityActionTest {
private User createUser(UserRoles userRoles) {
return new User.Builder()
.setEmailAddress("email@email.com")
.setGaiaId("TestUserId")
.setUserRoles(userRoles)
.build();
}
@@ -101,7 +101,6 @@ final class RegistryLockGetActionTest {
google.registry.model.console.User consoleUser =
new google.registry.model.console.User.Builder()
.setEmailAddress("johndoe@theregistrar.com")
.setGaiaId("gaiaId")
.setUserRoles(
new UserRoles.Builder()
.setRegistrarRoles(
@@ -231,7 +231,6 @@ final class RegistryLockPostActionTest {
google.registry.model.console.User consoleUser =
new google.registry.model.console.User.Builder()
.setEmailAddress("johndoe@theregistrar.com")
.setGaiaId("gaiaId")
.setUserRoles(
new UserRoles.Builder()
.setRegistrarRoles(
@@ -252,7 +251,6 @@ final class RegistryLockPostActionTest {
google.registry.model.console.User consoleUser =
new google.registry.model.console.User.Builder()
.setEmailAddress("johndoe@theregistrar.com")
.setGaiaId("gaiaId")
.setUserRoles(new UserRoles.Builder().setIsAdmin(true).build())
.build();
AuthResult consoleAuthResult =
@@ -447,7 +445,6 @@ final class RegistryLockPostActionTest {
google.registry.model.console.User consoleUser =
new google.registry.model.console.User.Builder()
.setEmailAddress("johndoe@theregistrar.com")
.setGaiaId("gaiaId")
.setUserRoles(
new UserRoles.Builder()
.setRegistrarRoles(
@@ -1,56 +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.xjc;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.TestDataHelper.loadFile;
import static java.nio.charset.StandardCharsets.UTF_8;
import google.registry.xjc.rdehost.XjcRdeHostElement;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link JaxbFragment}. */
class JaxbFragmentTest {
private static final String HOST_FRAGMENT = loadFile(XjcObjectTest.class, "host_fragment.xml");
/** Verifies that a {@link JaxbFragment} can be serialized and deserialized successfully. */
@SuppressWarnings("unchecked")
@Test
void testJavaSerialization() throws Exception {
// Load rdeHost xml fragment into a jaxb object, wrap it, marshal, unmarshal, verify host.
// The resulting host name should be "ns1.example1.test", from the original xml fragment.
try (InputStream source = new ByteArrayInputStream(HOST_FRAGMENT.getBytes(UTF_8))) {
// Load xml
JaxbFragment<XjcRdeHostElement> hostFragment =
JaxbFragment.create(XjcXmlTransformer.unmarshal(XjcRdeHostElement.class, source));
// Marshal
ByteArrayOutputStream bout = new ByteArrayOutputStream();
new ObjectOutputStream(bout).writeObject(hostFragment);
// Unmarshal
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bout.toByteArray()));
JaxbFragment<XjcRdeHostElement> restoredHostFragment =
(JaxbFragment<XjcRdeHostElement>) in.readObject();
// Verify host name
assertThat(restoredHostFragment.getInstance().getValue().getName())
.isEqualTo("ns1.example1.test");
}
}
}
@@ -1,10 +1,10 @@
addGracePeriodLength: 432000000
addGracePeriodLength: "PT432000S"
allowedFullyQualifiedHostNames:
- "foo"
allowedRegistrantContactIds: []
anchorTenantAddGracePeriodLength: 2592000000
autoRenewGracePeriodLength: 3888000000
automaticTransferLength: 432000000
anchorTenantAddGracePeriodLength: "PT2592000S"
autoRenewGracePeriodLength: "PT3888000S"
automaticTransferLength: "PT432000S"
claimsPeriodEnd: "294247-01-10T04:00:54.775Z"
createBillingCost:
currency: "USD"
@@ -13,13 +13,13 @@ creationTime: "1970-01-01T00:00:00.000Z"
currency: "USD"
defaultPromoTokens:
- "bbbbb"
dnsAPlusAaaaTtl: 3600000
dnsAPlusAaaaTtl: "PT3600S"
dnsDsTtl: null
dnsNsTtl: null
dnsPaused: false
dnsWriters:
- "baz"
- "bang"
- "baz"
driveFolderId: null
eapFeeSchedule:
"1970-01-01T00:00:00.000Z":
@@ -33,15 +33,15 @@ eapFeeSchedule:
amount: 0.00
escrowEnabled: false
idnTables:
- "JA"
- "EXTENDED_LATIN"
- "JA"
invoicingEnabled: false
lordnUsername: null
numDnsPublishLocks: 1
pendingDeleteLength: 432000000
pendingDeleteLength: "PT432000S"
premiumListName: "tld"
pricingEngineClassName: "google.registry.model.pricing.StaticPremiumListPricingEngine"
redemptionGracePeriodLength: 2592000000
redemptionGracePeriodLength: "PT2592000S"
registryLockOrUnlockBillingCost:
currency: "USD"
amount: 0.00
@@ -49,7 +49,7 @@ renewBillingCostTransitions:
"1970-01-01T00:00:00.000Z":
currency: "USD"
amount: 11.00
renewGracePeriodLength: 432000000
renewGracePeriodLength: "PT432000S"
reservedListNames: []
restoreBillingCost:
currency: "USD"
@@ -63,4 +63,4 @@ tldStateTransitions:
tldStr: "tld"
tldType: "REAL"
tldUnicode: "tld"
transferGracePeriodLength: 432000000
transferGracePeriodLength: "PT432000S"

Some files were not shown because too many files have changed in this diff Show More