1
0
mirror of https://github.com/google/nomulus synced 2026-07-19 22:42:23 +00:00

Compare commits

...

27 Commits

Author SHA1 Message Date
Lai Jiang 6952e0f653 Fix a typo (#2657) 2025-01-31 02:44:28 +00:00
Lai Jiang dcb55d27bb Upload gateway related manifests to GCS (#2655) 2025-01-30 16:12:31 +00:00
Pavlo Tkach 765bd9834a Add more accessible names to the console (#2652) 2025-01-29 20:19:00 +00:00
Lai Jiang 221088e738 Upload k8s manifests to GCS (#2654) 2025-01-29 17:07:10 +00:00
gbrodman 6649e00df7 Allow for particular flows to log all SQL statements executed (#2653)
We use this now for the DomainDeleteFlow in an attempt to figure out
what statements it's running (cross-referencing that with PSQL's own
statement logging to find slow statements).
2025-01-29 16:00:19 +00:00
gbrodman 2ceb52a7c4 Handle SPECIFIED renewal price w/token in check flow (#2651)
This is kinda nonsensical because this use case is trying to apply a
single use token multiple times in the same domain:check request --
like, trying to use a single-use token for both create, renew, and
transfer while having a $0 create price and a premium renewal price.

This change doesn't affect any actual business / costs, since SPECIFIED
token renewal prices were already set on the BillingRecurrence
2025-01-28 18:31:29 +00:00
Lai Jiang 120bcc33be Update cloud build configs to build nomulus images (#2650)
Also do appropriate text replacements for each environment.
2025-01-28 16:03:26 +00:00
Pavlo Tkach 8987fd37c2 Improve console accessibility (#2649) 2025-01-26 00:47:53 +00:00
gbrodman 653e092ad4 Add TLD identifier to premium terms filename and header (#2644)
https://b.corp.google.com/issues/390053672

This makes it easier to identify what file you're looking at, at a
glance
2025-01-24 19:54:35 +00:00
gbrodman 5e97a8b412 Refactor console domain actions to exist in separate files (#2638)
This means that we're not storing everything in one file, otherwise it
quickly becomes unwieldy
2025-01-23 16:46:53 +00:00
Weimin Yu 229fcf3946 UrlConnectionException loses error info (#2648)
It does not get the error message for 400+ status codes.

It fails to get the status code if the response has neither data nor
error.
2025-01-23 16:27:03 +00:00
Lai Jiang b775e4a178 Pull credentials from fleet for all clusters (#2647)
All clusters have switched to using private APIs.
2025-01-22 16:58:56 +00:00
Pavlo Tkach e3c386a8a7 Add console bulk delete (#2641)
* Add bulk actions to console

* Add console bulk delete

* Add console bulk delete
2025-01-22 15:54:59 +00:00
Lai Jiang 799f0449ad Only pull credential from the fleet on crash (#2645)
Only crash has the policy controller installed for now.
2025-01-21 18:40:52 +00:00
Lai Jiang bf025445d5 Record http request parameters in log metadata (#2642)
This allows us to search for logs for a given path using a filter like
this:

jsonPayload.httpRequest.requestUrl="/_dr/blah"

TESTED=tested on crash
2025-01-16 17:27:53 +00:00
Lai Jiang 9f22f2e8ae Pull nomulus cluster credentials from the fleet (#2643)
After private endpoint is enabled, we cannot pull the credentials
directly via `gcloud containers cluster get-credentials`.
2025-01-16 15:06:02 +00:00
gbrodman 45c8b81823 Map token renewal behavior directly onto BillingRecurrence (#2635)
Instead of using a separate RenewalPriceInfo object, just map the
behavior (if it exists) onto the BillingRecurrence with a special
carve-out, as always, for anchor tenants (note: this shouldn't matter
much since anchor tenants *should* use NONPREMIUM renewal tokens anyway,
but just in case, double-check).

This also fixes DomainPricingLogic to treat a multiyear create as a
one-year-create + n-minus-1-year-renewal for cases where either the
creation or the renewal (or both) are nonpremium.
2025-01-15 19:55:34 +00:00
Weimin Yu 4cfcc60655 Clean up keyring bindings (#2640)
Remove the config file's `keyring` section and the binding in java code.
2025-01-14 22:06:05 +00:00
Lai Jiang e4ee63b8f3 Make Cloud Tasks Utils canary-aware (#2639) 2025-01-14 17:39:51 +00:00
Weimin Yu f8407c74bc Make SecretManagerkeyring the only allowed keyring (#2636)
Remove the support for custom keyrings. There is no pressing use case,
and can be error-prone.
2025-01-13 19:32:24 +00:00
gbrodman 693467a165 Remove duplicate transaction in updateAllocTokens (#2637) 2025-01-13 19:12:06 +00:00
Lai Jiang cea3da01a0 Expose Web WHOIS redirects (#2634)
We are required to respond to HTTP(S) requests on port 80/443 on the
same domain where we serve port 43 WHOIS requests. The proxy already
does this by redirecting to the web WHOIS lookup page on the marketing
website.

This PR makes it so that requests to port 80/443 can be routed to the
proxy for redirect.

TESTED=tested on crash and the redirect works.
2025-01-10 17:25:16 +00:00
Weimin Yu c2030e5859 Fix keyring in BEAM pipeline (#2632)
SecretManager based keyring not included in keyring bindings, resulting
in runtime failure.

We should simply keyring bindings. There is no use case for multiple
implementations. See b/388835696.
2025-01-09 20:01:32 +00:00
Lai Jiang 1cbbc660d2 Explicity specify deployment order for queues and scheduler tasks (#2631)
If we deploy Nomulus, we should do that before queues and the scheduler
tasks are updated.
2025-01-08 21:11:24 +00:00
Lai Jiang e0bbff827e Upgrade to Gradle 8.12 (#2630) 2025-01-08 18:43:10 +00:00
Weimin Yu 10925f2447 Enable nested transaction warning in production (#2628)
Knonw nested transact calls found in sandbox have been refactored away.
Enable logging in production to catch any missing cases. Logging is
throttled at 1 message per minute per VM.
2025-01-03 20:52:25 +00:00
Lai Jiang 7641b05f12 Expose EPP and WHOIS endpoints on reginal load balancers (#2627)
k8s does not have a way to expose a global load balancer with TCP
endpoints, and setting up node port-based routing is a chore, even with
Terraform (which is what we did with the standalone proxy).

We will use Cloud DNS's geolocation routing policy to ensure that
clients connect to the endpoint closest to them.
2024-12-26 15:25:02 +00:00
132 changed files with 2061 additions and 1935 deletions
+5 -5
View File
@@ -7,6 +7,11 @@
></mat-progress-bar>
</div>
<mat-sidenav-container class="console-app__container">
<mat-sidenav-content class="console-app__content-wrapper">
<div class="console-app__content" role="main">
<router-outlet></router-outlet>
</div>
</mat-sidenav-content>
<mat-sidenav
[mode]="breakpointObserver.isMobileView() ? 'over' : 'side'"
[opened]="!breakpointObserver.isMobileView()"
@@ -15,10 +20,5 @@
>
<app-navigation />
</mat-sidenav>
<mat-sidenav-content class="console-app__content-wrapper">
<div class="console-app__content">
<router-outlet></router-outlet>
</div>
</mat-sidenav-content>
</mat-sidenav-container>
</div>
+8 -1
View File
@@ -20,12 +20,18 @@ import { AppComponent } from './app.component';
import { MaterialModule } from './material.module';
import { BackendService } from './shared/services/backend.service';
import { AppRoutingModule } from './app-routing.module';
import { AppModule } from './app.module';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AppComponent],
imports: [MaterialModule, BrowserAnimationsModule, AppRoutingModule],
imports: [
MaterialModule,
BrowserAnimationsModule,
AppRoutingModule,
AppModule,
],
providers: [
BackendService,
provideHttpClient(),
@@ -36,6 +42,7 @@ describe('AppComponent', () => {
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
+7 -1
View File
@@ -26,7 +26,11 @@ import { BackendService } from './shared/services/backend.service';
import { provideHttpClient } from '@angular/common/http';
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
import { BillingInfoComponent } from './billingInfo/billingInfo.component';
import { DomainListComponent } from './domains/domainList.component';
import {
DomainListComponent,
ReasonDialogComponent,
ResponseDialogComponent,
} from './domains/domainList.component';
import { RegistryLockComponent } from './domains/registryLock.component';
import { HeaderComponent } from './header/header.component';
import { HomeComponent } from './home/home.component';
@@ -92,6 +96,8 @@ export class SelectedRegistrarModule {}
TldsComponent,
WhoisComponent,
WhoisEditComponent,
ReasonDialogComponent,
ResponseDialogComponent,
],
bootstrap: [AppComponent],
imports: [
@@ -5,12 +5,16 @@
<div class="console-app__billing-subhead">
Billing records and information
</div>
<a class="text-l" href="{{ driveFolderUrl() }}" target="_blank"
<a
class="text-l"
href="{{ driveFolderUrl() }}"
target="_blank"
aria-label="View billing records on Google Drive"
>View on Google Drive</a
>
</div>
<div>
<img src="./assets/billing.png" />
<img src="./assets/billing.png" alt="Generic billing image" />
</div>
</div>
</app-selected-registrar-wrapper>
@@ -16,7 +16,7 @@
width: 100%;
}
&-subhead {
font-size: 20px;
font-size: 1.25rem;
margin-bottom: 20px;
}
}
@@ -25,7 +25,11 @@
} @else {
<mat-menu #actions="matMenu">
<ng-template matMenuContent let-domainName="domainName">
<button mat-menu-item (click)="openRegistryLock(domainName)">
<button
mat-menu-item
(click)="openRegistryLock(domainName)"
aria-label="Access registry lock for domain"
>
<mat-icon>key</mat-icon>
<span>Registry Lock</span>
</button>
@@ -65,16 +69,66 @@
/>
</mat-form-field>
<div
class="console-app__domains-selection"
[elementId]="getElementIdForBulkDelete()"
[ngClass]="{ active: selection.hasValue() }"
>
<div class="console-app__domains-selection-text">
{{ selection.selected.length }} Selected
</div>
<div class="console-app__domains-selection-actions">
<button
mat-flat-button
aria-label="Delete Selected Domains"
(click)="deleteSelectedDomains()"
>
Delete Selected Domains
</button>
</div>
</div>
<mat-table
[dataSource]="dataSource"
class="mat-elevation-z0"
class="console-app__domains-table"
>
<!-- Checkbox Column -->
<ng-container matColumnDef="select">
<mat-header-cell *matHeaderCellDef>
<mat-checkbox
(change)="$event ? toggleAllRows() : null"
[checked]="selection.hasValue() && isAllSelected"
[indeterminate]="selection.hasValue() && !isAllSelected"
[aria-label]="checkboxLabel()"
[elementId]="getElementIdForBulkDelete()"
>
</mat-checkbox>
</mat-header-cell>
<mat-cell *matCellDef="let row">
<mat-checkbox
(click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null"
[checked]="selection.isSelected(row)"
[aria-label]="checkboxLabel(row)"
[elementId]="getElementIdForBulkDelete()"
>
</mat-checkbox>
</mat-cell>
</ng-container>
<ng-container matColumnDef="domainName">
<mat-header-cell *matHeaderCellDef>Domain Name</mat-header-cell>
<mat-cell *matCellDef="let element">{{
element.domainName
}}</mat-cell>
<mat-cell *matCellDef="let element">
<mat-icon
*ngIf="getOperationMessage(element.domainName)"
[matTooltip]="getOperationMessage(element.domainName)"
matTooltipPosition="above"
class="primary-text"
>info</mat-icon
>
<span>{{ element.domainName }}</span>
</mat-cell>
</ng-container>
<ng-container matColumnDef="creationTime">
@@ -12,6 +12,22 @@
}
}
&__domains-selection {
height: 60px;
max-height: 0;
transition: max-height 0.2s linear;
display: flex;
align-items: center;
overflow: hidden;
gap: 20px;
&-text {
font-weight: bold;
}
&.active {
max-height: 60px;
}
}
&-domains__download {
position: absolute;
top: -55px;
@@ -41,6 +57,22 @@
overflow: hidden;
word-break: break-word;
}
.mat-column-select {
max-width: 60px;
padding-left: 15px;
}
.mat-column-domainName {
position: relative;
padding-left: 25px;
mat-icon {
position: absolute;
left: 0;
}
}
mat-cell:has([style*="display: none"]),
mat-header-cell:has([style*="display: none"]) {
display: none;
}
}
&__domains-spinner {
@@ -21,6 +21,7 @@ import { MaterialModule } from '../material.module';
import { BackendService } from '../shared/services/backend.service';
import { DomainListComponent } from './domainList.component';
import { FormsModule } from '@angular/forms';
import { AppModule } from '../app.module';
describe('DomainListComponent', () => {
let component: DomainListComponent;
@@ -29,7 +30,12 @@ describe('DomainListComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DomainListComponent],
imports: [MaterialModule, BrowserAnimationsModule, FormsModule],
imports: [
MaterialModule,
BrowserAnimationsModule,
FormsModule,
AppModule,
],
providers: [
BackendService,
provideHttpClient(),
@@ -12,16 +12,91 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { SelectionModel } from '@angular/cdk/collections';
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
import { Component, ViewChild, effect } from '@angular/core';
import { Component, ViewChild, effect, Inject } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTableDataSource } from '@angular/material/table';
import { Subject, debounceTime } from 'rxjs';
import { Subject, debounceTime, take, filter } from 'rxjs';
import { RegistrarService } from '../registrar/registrar.service';
import { Domain, DomainListService } from './domainList.service';
import { RegistryLockComponent } from './registryLock.component';
import { RegistryLockService } from './registryLock.service';
import {
MAT_DIALOG_DATA,
MatDialog,
MatDialogRef,
} from '@angular/material/dialog';
import { RESTRICTED_ELEMENTS } from '../shared/directives/userLevelVisiblity.directive';
interface DomainResponse {
message: string;
responseCode: string;
}
interface DomainData {
[domain: string]: DomainResponse;
}
@Component({
selector: 'app-response-dialog',
template: `
<h2 mat-dialog-title>{{ data.title }}</h2>
<mat-dialog-content [innerHTML]="data.content" />
<mat-dialog-actions>
<button mat-button (click)="onClose()">Close</button>
</mat-dialog-actions>
`,
})
export class ResponseDialogComponent {
constructor(
public dialogRef: MatDialogRef<ReasonDialogComponent>,
@Inject(MAT_DIALOG_DATA)
public data: { title: string; content: string }
) {}
onClose(): void {
this.dialogRef.close();
}
}
@Component({
selector: 'app-reason-dialog',
template: `
<h2 mat-dialog-title>
Please provide a reason for {{ data.operation }} the domain(s):
</h2>
<mat-dialog-content>
<mat-form-field appearance="outline" style="width:100%">
<textarea matInput [(ngModel)]="reason" rows="4"></textarea>
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button (click)="onCancel()">Cancel</button>
<button mat-button color="warn" (click)="onDelete()" [disabled]="!reason">
Delete
</button>
</mat-dialog-actions>
`,
})
export class ReasonDialogComponent {
reason: string = '';
constructor(
public dialogRef: MatDialogRef<ReasonDialogComponent>,
@Inject(MAT_DIALOG_DATA)
public data: { operation: 'deleting' | 'suspending' }
) {}
onDelete(): void {
this.dialogRef.close(this.reason);
}
onCancel(): void {
this.dialogRef.close();
}
}
@Component({
selector: 'app-domain-list',
@@ -31,8 +106,10 @@ import { RegistryLockService } from './registryLock.service';
export class DomainListComponent {
public static PATH = 'domain-list';
private readonly DEBOUNCE_MS = 500;
isAllSelected = false;
displayedColumns: string[] = [
'select',
'domainName',
'creationTime',
'registrationExpirationTime',
@@ -42,6 +119,7 @@ export class DomainListComponent {
];
dataSource: MatTableDataSource<Domain> = new MatTableDataSource();
selection = new SelectionModel<Domain>(true, [], undefined, this.isChecked());
isLoading = true;
searchTermSubject = new Subject<string>();
@@ -51,13 +129,18 @@ export class DomainListComponent {
resultsPerPage = 50;
totalResults?: number = 0;
reason: string = '';
operationResult: DomainData | undefined;
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
constructor(
protected domainListService: DomainListService,
protected registrarService: RegistrarService,
protected registryLockService: RegistryLockService,
private _snackBar: MatSnackBar
private _snackBar: MatSnackBar,
private dialog: MatDialog
) {
effect(() => {
this.pageNumber = 0;
@@ -134,6 +217,98 @@ export class DomainListComponent {
onPageChange(event: PageEvent) {
this.pageNumber = event.pageIndex;
this.resultsPerPage = event.pageSize;
this.selection.clear();
this.reloadData();
}
toggleAllRows() {
if (this.isAllSelected) {
this.selection.clear();
this.isAllSelected = false;
return;
}
this.selection.select(...this.dataSource.data);
this.isAllSelected = true;
}
checkboxLabel(row?: Domain): string {
if (!row) {
return `${this.isAllSelected ? 'deselect' : 'select'} all`;
}
return `${this.selection.isSelected(row) ? 'deselect' : 'select'} row ${
row.domainName
}`;
}
private isChecked(): ((o1: Domain, o2: Domain) => boolean) | undefined {
return (o1: Domain, o2: Domain) => {
if (!o1.domainName || !o2.domainName) {
return false;
}
return this.isAllSelected || o1.domainName === o2.domainName;
};
}
getElementIdForBulkDelete() {
return RESTRICTED_ELEMENTS.BULK_DELETE;
}
getOperationMessage(domain: string) {
if (this.operationResult && this.operationResult[domain])
return this.operationResult[domain].message;
return '';
}
sendDeleteRequest(reason: string) {
this.isLoading = true;
this.domainListService
.deleteDomains(
this.selection.selected,
reason,
this.registrarService.registrarId()
)
.pipe(take(1))
.subscribe({
next: (result: DomainData) => {
this.isLoading = false;
const successCount = Object.keys(result).filter((domainName) =>
result[domainName].responseCode.toString().startsWith('1')
).length;
const failureCount = Object.keys(result).length - successCount;
this.dialog.open(ResponseDialogComponent, {
data: {
title: 'Domain Deletion Results',
content: `Successfully deleted - ${successCount} domain(s)<br/>Failed to delete - ${failureCount} domain(s)<br/>${
failureCount
? 'Some domains could not be deleted due to ongoing processes or server errors. '
: ''
}Please check the table for more information.`,
},
});
this.selection.clear();
this.operationResult = result;
this.reloadData();
},
error: (err: HttpErrorResponse) =>
this._snackBar.open(err.error || err.message),
});
}
deleteSelectedDomains() {
const dialogRef = this.dialog.open(ReasonDialogComponent, {
data: {
operation: 'deleting',
},
});
dialogRef
.afterClosed()
.pipe(
take(1),
filter((reason) => !!reason)
)
.subscribe(this.sendDeleteRequest.bind(this));
}
}
@@ -48,7 +48,6 @@ export class DomainListService {
private backendService: BackendService,
private registrarService: RegistrarService
) {}
retrieveDomains(
pageNumber?: number,
resultsPerPage?: number,
@@ -71,4 +70,13 @@ export class DomainListService {
})
);
}
deleteDomains(domains: Domain[], reason: string, registrarId: string) {
return this.backendService.bulkDomainAction(
domains.map((d) => d.domainName),
reason,
'DELETE',
registrarId
);
}
}
@@ -49,6 +49,7 @@
color="primary"
type="submit"
[disabled]="!unlockDomain.valid"
aria-label="Submit domain unlock request"
>
Save
</button>
@@ -73,6 +74,7 @@
color="primary"
type="submit"
[disabled]="!lockDomain.valid"
aria-label="Submit domain lock request"
>
Save
</button>
@@ -2,7 +2,7 @@
<mat-toolbar>
<button
mat-icon-button
aria-label="Open menu"
aria-label="Open navigation menu"
(click)="toggleNavPane()"
*ngIf="breakpointObserver.isMobileView()"
class="console-app__menu-btn"
@@ -12,6 +12,7 @@
<a
[routerLink]="'/home'"
routerLinkActive="active"
aria-label="Google Registry logo"
class="console-app__logo"
>
<svg
@@ -17,6 +17,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HeaderComponent } from './header.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MaterialModule } from '../material.module';
import { ActivatedRoute } from '@angular/router';
import { AppModule, SelectedRegistrarModule } from '../app.module';
import { AppRoutingModule } from '../app-routing.module';
import { BackendService } from '../shared/services/backend.service';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
describe('HeaderComponent', () => {
let component: HeaderComponent;
@@ -24,7 +30,19 @@ describe('HeaderComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MaterialModule, BrowserAnimationsModule],
imports: [
SelectedRegistrarModule,
MaterialModule,
BrowserAnimationsModule,
AppRoutingModule,
AppModule,
],
providers: [
BackendService,
{ provide: ActivatedRoute, useValue: {} as ActivatedRoute },
provideHttpClient(),
provideHttpClientTesting(),
],
declarations: [HeaderComponent],
}).compileComponents();
@@ -16,6 +16,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeComponent } from './home.component';
import { MaterialModule } from '../material.module';
import { AppModule } from '../app.module';
describe('HomeComponent', () => {
let component: HomeComponent;
@@ -23,7 +24,7 @@ describe('HomeComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MaterialModule],
imports: [MaterialModule, AppModule],
declarations: [HomeComponent],
}).compileComponents();
@@ -3,7 +3,7 @@
margin-top: 30px;
}
&-subhead {
font-size: 20px;
font-size: 1.25rem;
margin-bottom: 20px;
}
}
@@ -6,7 +6,9 @@
<mat-tree-node
*matTreeNodeDef="let node"
matTreeNodeToggle
tabindex="0"
(click)="onClick(node)"
(keyup.enter)="onClick(node)"
[class.active]="router.url.includes(node.path)"
[elementId]="getElementId(node)"
>
@@ -30,7 +30,14 @@
>
</mat-form-field>
</p>
<button mat-flat-button color="primary" type="submit">Save</button>
<button
mat-flat-button
color="primary"
type="submit"
aria-label="Submit new OT&E account"
>
Save
</button>
</form>
}
</div>
@@ -175,6 +175,7 @@ JPY=billing-id-for-yen"
mat-flat-button
color="primary"
type="submit"
aria-label="Submit new registrar request"
>
Save
</button>
@@ -1,4 +1,8 @@
<div class="console-app__registrar-view">
<div
class="console-app__registrar-view"
cdkTrapFocus
[cdkTrapFocusAutoCapture]="true"
>
<h1 class="mat-headline-4">Registrars</h1>
<mat-divider></mat-divider>
<div class="console-app__registrar-view-content">
@@ -12,7 +16,7 @@
*ngIf="oteButtonVisible"
mat-stroked-button
(click)="checkOteStatus()"
aria-label="Check OT&E account"
aria-label="Check OT&E account status"
[elementId]="getElementIdForOteBlock()"
>
Check OT&E Status
@@ -11,6 +11,7 @@
[ngModelOptions]="{ standalone: true }"
(focus)="onFocus()"
[matAutocomplete]="auto"
spellcheck="false"
/>
<mat-autocomplete
autoActiveFirstOption
@@ -59,6 +59,8 @@
<mat-row
*matRowDef="let row; columns: displayedColumns"
(click)="openDetails(row.registrarId)"
tabindex="0"
(keyup.enter)="openDetails(row.registrarId)"
></mat-row>
</mat-table>
@@ -11,6 +11,6 @@
>
</div>
<div>
<img src="./assets/resources.png" />
<img src="./assets/resources.png" alt="Generic resources image" />
</div>
</div>
@@ -16,7 +16,7 @@
width: 100%;
}
&-subhead {
font-size: 20px;
font-size: 1.25rem;
margin-bottom: 20px;
}
}
@@ -32,7 +32,9 @@
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row
*matRowDef="let row; columns: displayedColumns"
tabindex="0"
(click)="openDetails(row)"
(keyup.enter)="openDetails(row)"
></mat-row>
</mat-table>
}
@@ -1,4 +1,9 @@
<div class="console-app__contact" *ngIf="contactService.contactInEdit">
<div
class="console-app__contact"
*ngIf="contactService.contactInEdit"
cdkTrapFocus
[cdkTrapFocusAutoCapture]="true"
>
<div class="console-app__contact-controls">
<button
mat-icon-button
@@ -123,6 +128,7 @@
mat-flat-button
color="primary"
type="submit"
aria-label="Save contact updates"
>
Save
</button>
@@ -1,4 +1,8 @@
<div class="settings-security__edit-password">
<div
class="settings-security__edit-password"
cdkTrapFocus
[cdkTrapFocusAutoCapture]="true"
>
<p>
<button
mat-icon-button
@@ -42,7 +42,7 @@ describe('SecurityComponent', () => {
fetchSecurityDetailsSpy =
securityServiceSpy.fetchSecurityDetails.and.returnValue(of());
saveSpy = securityServiceSpy.saveChanges;
saveSpy = securityServiceSpy.saveChanges.and.returnValue(of());
await TestBed.configureTestingModule({
declarations: [SecurityEditComponent, SecurityComponent],
@@ -1,4 +1,8 @@
<div class="settings-security__edit">
<div
class="settings-security__edit"
cdkTrapFocus
[cdkTrapFocusAutoCapture]="true"
>
<h1>IP Allowlist</h1>
<p>
Restrict access to EPP production servers to the following IP/IPv6
@@ -19,7 +23,7 @@
<button
matSuffix
mat-icon-button
aria-label="Remove"
[attr.aria-label]="'Remove IP entry ' + ip.value"
(click)="removeIpEntry(ip)"
[disabled]="isUpdating"
>
@@ -32,6 +36,7 @@
[disabled]="isUpdating"
color="primary"
(click)="createIpEntry()"
aria-label="Add new IP address"
type="button"
>
+ Add IP
@@ -10,22 +10,31 @@
<a
mat-tab-link
routerLink="contact"
routerLinkActive="active-link"
routerLinkActive
queryParamsHandling="merge"
#rla="routerLinkActive"
[active]="rla.isActive"
aria-label="Access contacts settings"
>Contacts</a
>
<a
mat-tab-link
routerLink="whois"
routerLinkActive="active-link"
routerLinkActive
queryParamsHandling="merge"
#rla2="routerLinkActive"
[active]="rla2.isActive"
aria-label="Access whois settings"
>WHOIS Info</a
>
<a
mat-tab-link
routerLink="security"
routerLinkActive="active-link"
routerLinkActive
queryParamsHandling="merge"
#rla3="routerLinkActive"
[active]="rla3.isActive"
aria-label="Access security settings"
>Security</a
>
</nav>
@@ -13,14 +13,6 @@
// limitations under the License.
.console-settings {
.mdc-tab {
&.active-link {
border-bottom: 2px solid var(--primary);
.mdc-tab__text-label {
color: var(--primary);
}
}
}
nav {
margin-bottom: 40px;
}
@@ -17,6 +17,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SettingsComponent } from './settings.component';
import { MaterialModule } from '../material.module';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ActivatedRoute } from '@angular/router';
import { AppModule, SelectedRegistrarModule } from '../app.module';
import { BackendService } from '../shared/services/backend.service';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { AppRoutingModule } from '../app-routing.module';
describe('SettingsComponent', () => {
let component: SettingsComponent;
@@ -24,7 +30,19 @@ describe('SettingsComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MaterialModule, BrowserAnimationsModule],
imports: [
SelectedRegistrarModule,
MaterialModule,
BrowserAnimationsModule,
AppRoutingModule,
AppModule,
],
providers: [
BackendService,
{ provide: ActivatedRoute, useValue: {} as ActivatedRoute },
provideHttpClient(),
provideHttpClientTesting(),
],
declarations: [SettingsComponent],
}).compileComponents();
@@ -1,4 +1,9 @@
<div class="console-app__whois-edit" *ngIf="registrarInEdit">
<div
class="console-app__whois-edit"
*ngIf="registrarInEdit"
cdkTrapFocus
[cdkTrapFocusAutoCapture]="true"
>
<button
mat-icon-button
class="console-app__whois-edit-back"
@@ -144,7 +149,14 @@
</mat-form-field>
}
<button mat-flat-button color="primary" type="submit">Save</button>
<button
mat-flat-button
color="primary"
type="submit"
aria-label="Save WHOIS settings"
>
Save
</button>
</form>
</div>
</div>
@@ -19,6 +19,7 @@ export enum RESTRICTED_ELEMENTS {
REGISTRAR_ELEMENT,
OTE,
USERS,
BULK_DELETE,
}
export const DISABLED_ELEMENTS_PER_ROLE = {
@@ -26,6 +27,7 @@ export const DISABLED_ELEMENTS_PER_ROLE = {
RESTRICTED_ELEMENTS.REGISTRAR_ELEMENT,
RESTRICTED_ELEMENTS.OTE,
RESTRICTED_ELEMENTS.USERS,
RESTRICTED_ELEMENTS.BULK_DELETE,
],
SUPPORT_LEAD: [RESTRICTED_ELEMENTS.USERS],
SUPPORT_AGENT: [RESTRICTED_ELEMENTS.USERS],
@@ -180,6 +180,23 @@ export class BackendService {
.pipe(catchError((err) => this.errorCatcher<any>(err)));
}
bulkDomainAction(
domainNames: string[],
reason: string,
bulkDomainAction: string,
registrarId: string
) {
return this.http
.post<any>(
`/console-api/bulk-domain?registrarId=${registrarId}&bulkDomainAction=${bulkDomainAction}`,
{
domainList: domainNames,
reason,
}
)
.pipe(catchError((err) => this.errorCatcher<any>(err)));
}
updateUser(registrarId: string, updatedUser: User): Observable<any> {
return this.http
.put<User>(`/console-api/users?registrarId=${registrarId}`, updatedUser)
@@ -10,7 +10,10 @@
For help with OT&E sandbox and certification, or new technical requirements
for any of our new TLD launches.
</p>
<a class="text-l" href="mailto:registry-integration@google.com"
<a
class="text-l"
href="mailto:registry-integration@google.com"
aria-label="Email us with OT&E sandbox/certification or new TLD technical requirements questions."
>registry-integration&#64;google.com</a
>
<p class="text-l">
@@ -19,6 +22,7 @@
</p>
<a
class="text-l"
aria-label="Email support with general purpose questions."
href="mailto:{{ userDataService.userData()?.supportEmail }}"
>{{ userDataService.userData()?.supportEmail }}</a
>
@@ -1,4 +1,8 @@
<div class="console-app__user-details">
<div
class="console-app__user-details"
cdkTrapFocus
[cdkTrapFocusAutoCapture]="true"
>
@if(isEditing) {
<h1 class="mat-headline-4">Editing {{ userDetails().emailAddress }}</h1>
<mat-divider></mat-divider>
@@ -86,11 +90,12 @@
<input
[type]="isPasswordVisible ? 'text' : 'password'"
[value]="userDetails().password"
disabled
aria-label="Password field"
readonly
/>
<button
mat-button
aria-label="Show password"
aria-hidden="true"
(click)="isPasswordVisible = !isPasswordVisible"
>
{{ isPasswordVisible ? "Hide" : "View" }} password
@@ -33,7 +33,13 @@
</mat-select>
</mat-form-field>
</p>
<button mat-flat-button color="primary" aria-label="Save user" type="submit">
<button
mat-flat-button
color="primary"
aria-label="Save user"
type="submit"
aria-label="Save changes to the user"
>
Save
</button>
</form>
@@ -1,4 +1,4 @@
<app-selected-registrar-wrapper>
<app-selected-registrar-wrapper cdkTrapFocus [cdkTrapFocusAutoCapture]="true">
@if(isLoading) {
<div class="console-app__users-spinner">
<mat-spinner />
@@ -88,12 +88,7 @@
<mat-icon>add</mat-icon>
Add existing user
</button>
<button
mat-flat-button
(click)="isNew = true"
aria-label="Create new user"
color="primary"
>
<button mat-flat-button (click)="isNew = true" color="primary">
Create New User
</button>
</div>
@@ -19,6 +19,8 @@
*matRowDef="let row; columns: displayedColumns"
[class.rowSelected]="isRowSelected(row)"
(click)="onClick(row)"
(keyup.enter)="onClick(row)"
tabindex="0"
></mat-row>
</mat-table>
</div>
+2 -2
View File
@@ -55,13 +55,13 @@ body {
font-weight: bold;
width: var(--list-item-title-width);
display: inline-block;
font-size: 14px;
font-size: 0.85rem;
white-space: pre-line;
color: #202124;
flex-shrink: 0;
}
.console-app__list-value {
font-size: 14px;
font-size: 0.85rem;
white-space: pre-line;
word-break: break-word;
color: var(--text);
+11 -7
View File
@@ -82,16 +82,11 @@ $typographyConfig: mat.m2-define-typography-config(
),
);
// Access and define a class with secondary color exposed
.secondary-text {
color: $secondary-color;
}
.text-xl {
font-size: 18px;
font-size: 1.1rem;
}
.text-l {
font-size: 16px;
font-size: 1rem;
}
mat-row:nth-child(odd) {
@@ -115,6 +110,15 @@ mat-row:hover {
--mat-sidenav-container-width: 280px;
}
// Access and define a class with secondary color exposed
.secondary-text {
color: #575757;
}
.primary-text {
color: var(--primary);
}
$theme: mat.define-theme(
(
color: (
+45 -41
View File
@@ -21,10 +21,10 @@ com.github.docker-java:docker-java-transport:3.4.0=compileClasspath,deploy_jar,n
com.github.jnr:jffi:1.3.13=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.jnr:jnr-a64asm:1.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.jnr:jnr-constants:0.10.4=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.jnr:jnr-enxio:0.32.17=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.jnr:jnr-ffi:2.2.16=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.jnr:jnr-posix:3.1.19=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.jnr:jnr-unixsocket:0.38.22=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.jnr:jnr-enxio:0.32.18=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.jnr:jnr-ffi:2.2.17=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.jnr:jnr-posix:3.1.20=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.jnr:jnr-unixsocket:0.38.23=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.jnr:jnr-x86asm:1.0.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,errorprone,nonprodAnnotationProcessor,testAnnotationProcessor
com.google.android:annotations:4.1.1.4=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
@@ -111,16 +111,16 @@ com.google.apis:google-api-services-bigquery:v2-rev20240815-2.0.0=compileClasspa
com.google.apis:google-api-services-cloudresourcemanager:v1-rev20240310-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-dataflow:v1b3-rev20241209-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-dns:v1-rev20240719-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-drive:v3-rev20241027-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-drive:v3-rev20241206-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-gmail:v1-rev20240520-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-groupssettings:v1-rev20220614-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-healthcare:v1-rev20240130-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-iam:v2-rev20240530-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-iam:v2-rev20241114-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-iamcredentials:v1-rev20211203-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-monitoring:v3-rev20241017-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-monitoring:v3-rev20241114-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-pubsub:v1-rev20220904-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-sheets:v4-rev20241203-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-sqladmin:v1beta4-rev20240925-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-sqladmin:v1beta4-rev20241108-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-storage:v1-rev20240706-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath
com.google.apis:google-api-services-storage:v1-rev20241206-2.0.0=testCompileClasspath,testRuntimeClasspath
com.google.auth:google-auth-library-credentials:1.30.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
@@ -140,8 +140,8 @@ com.google.cloud.opentelemetry:detector-resources-support:0.31.0=compileClasspat
com.google.cloud.opentelemetry:detector-resources-support:0.33.0=testRuntimeClasspath
com.google.cloud.opentelemetry:exporter-metrics:0.33.0=testCompileClasspath,testRuntimeClasspath
com.google.cloud.opentelemetry:shared-resourcemapping:0.33.0=testRuntimeClasspath
com.google.cloud.sql:jdbc-socket-factory-core:1.21.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.cloud.sql:postgres-socket-factory:1.21.0=deploy_jar,runtimeClasspath,testRuntimeClasspath
com.google.cloud.sql:jdbc-socket-factory-core:1.21.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.cloud.sql:postgres-socket-factory:1.21.2=deploy_jar,runtimeClasspath,testRuntimeClasspath
com.google.cloud:google-cloud-bigquerystorage:3.9.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath
com.google.cloud:google-cloud-bigquerystorage:3.9.2=testRuntimeClasspath
com.google.cloud:google-cloud-bigtable:2.43.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath
@@ -280,7 +280,6 @@ com.zaxxer:HikariCP:6.2.1=compileClasspath,deploy_jar,nonprodCompileClasspath,no
commons-beanutils:commons-beanutils:1.9.4=checkstyle
commons-codec:commons-codec:1.17.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
commons-collections:commons-collections:3.2.2=checkstyle
commons-dbutils:commons-dbutils:1.8.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
commons-io:commons-io:2.17.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
commons-logging:commons-logging:1.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
dev.failsafe:failsafe:3.3.2=testCompileClasspath,testRuntimeClasspath
@@ -408,10 +407,10 @@ javax.validation:validation-api:1.0.0.GA=compileClasspath,deploy_jar,nonprodComp
joda-time:joda-time:2.12.7=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
junit:junit:4.13.2=nonprodCompileClasspath,nonprodRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.arnx:nashorn-promise:0.1.1=testRuntimeClasspath
net.bytebuddy:byte-buddy-agent:1.15.4=testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy-agent:1.15.11=testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy:1.14.12=compileClasspath,nonprodCompileClasspath
net.bytebuddy:byte-buddy:1.14.15=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath
net.bytebuddy:byte-buddy:1.15.10=testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy:1.15.11=testCompileClasspath,testRuntimeClasspath
net.java.dev.jna:jna:5.13.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
net.ltgt.gradle.incap:incap:0.2=annotationProcessor,testAnnotationProcessor
net.sf.saxon:Saxon-HE:10.6=checkstyle
@@ -479,18 +478,18 @@ org.eclipse.angus:angus-activation:2.0.2=deploy_jar,jaxb,nonprodRuntimeClasspath
org.eclipse.angus:jakarta.mail:2.0.3=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.eclipse.collections:eclipse-collections-api:11.1.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.collections:eclipse-collections:11.1.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.ee10:jetty-ee10-servlet:12.1.0.alpha0=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.ee10:jetty-ee10-webapp:12.1.0.alpha0=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-ee:12.1.0.alpha0=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-http:12.1.0.alpha0=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-io:12.1.0.alpha0=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-security:12.1.0.alpha0=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-server:12.1.0.alpha0=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-session:12.1.0.alpha0=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-util:12.1.0.alpha0=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-xml:12.1.0.alpha0=testCompileClasspath,testRuntimeClasspath
org.flywaydb:flyway-core:11.1.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.flywaydb:flyway-database-postgresql:11.1.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.ee10:jetty-ee10-servlet:12.1.0.alpha1=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.ee10:jetty-ee10-webapp:12.1.0.alpha1=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-ee:12.1.0.alpha1=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-http:12.1.0.alpha1=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-io:12.1.0.alpha1=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-security:12.1.0.alpha1=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-server:12.1.0.alpha1=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-session:12.1.0.alpha1=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-util:12.1.0.alpha1=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-xml:12.1.0.alpha1=testCompileClasspath,testRuntimeClasspath
org.flywaydb:flyway-core:11.1.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.flywaydb:flyway-database-postgresql:11.1.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.glassfish.jaxb:codemodel:4.0.5=jaxb
org.glassfish.jaxb:jaxb-core:4.0.2=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.glassfish.jaxb:jaxb-core:4.0.5=jaxb
@@ -553,19 +552,24 @@ org.junit.platform:junit-platform-runner:1.11.4=testCompileClasspath,testRuntime
org.junit.platform:junit-platform-suite-api:1.11.4=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-suite-commons:1.11.4=testRuntimeClasspath
org.junit:junit-bom:5.11.4=testCompileClasspath,testRuntimeClasspath
org.mockito:mockito-core:5.14.2=testCompileClasspath,testRuntimeClasspath
org.mockito:mockito-junit-jupiter:5.14.2=testCompileClasspath,testRuntimeClasspath
org.mockito:mockito-core:5.15.2=testCompileClasspath,testRuntimeClasspath
org.mockito:mockito-junit-jupiter:5.15.2=testCompileClasspath,testRuntimeClasspath
org.objenesis:objenesis:3.3=testRuntimeClasspath
org.ogce:xpp3:1.1.6=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm-analysis:9.5=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,soy,testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm-commons:9.5=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,soy,testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm-analysis:9.5=soy
org.ow2.asm:asm-analysis:9.7.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm-commons:9.5=soy
org.ow2.asm:asm-commons:9.7=jacocoAnt
org.ow2.asm:asm-tree:9.5=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,soy,testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm-commons:9.7.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm-tree:9.5=soy
org.ow2.asm:asm-tree:9.7=jacocoAnt
org.ow2.asm:asm-util:9.5=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,soy,testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm:9.5=compileClasspath,nonprodCompileClasspath,soy
org.ow2.asm:asm:9.7=deploy_jar,jacocoAnt,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm-tree:9.7.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm-util:9.5=soy
org.ow2.asm:asm-util:9.7.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm:9.5=soy
org.ow2.asm:asm:9.7=jacocoAnt
org.ow2.asm:asm:9.7.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.pcollections:pcollections:3.1.4=annotationProcessor,errorprone,nonprodAnnotationProcessor,testAnnotationProcessor
org.postgresql:postgresql:42.7.4=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.reflections:reflections:0.10.2=checkstyle
@@ -604,13 +608,13 @@ org.w3c.css:sac:1.3=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodR
org.webjars.npm:viz.js-graphviz-java:2.1.3=testRuntimeClasspath
org.xerial.snappy:snappy-java:1.1.10.4=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.yaml:snakeyaml:2.3=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-api:16.24.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-diagram:16.24.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-loader:16.24.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-postgresql:16.24.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-text:16.24.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-tools:16.24.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-utility:16.24.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler:16.24.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-api:16.25.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-diagram:16.25.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-loader:16.25.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-postgresql:16.25.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-text:16.25.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-tools:16.25.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-utility:16.25.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler:16.25.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
xerces:xmlParserAPIs:2.6.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
empty=devtool,nomulus_test
@@ -17,6 +17,7 @@ package google.registry.batch;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.config.RegistryConfig.CANARY_HEADER;
import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.api.gax.rpc.ApiException;
@@ -190,6 +191,9 @@ public class CloudTasksUtils implements Serializable {
requestBuilder.setOidcToken(oidcTokenBuilder.build());
String totalPath = String.format("%s%s", service.getServiceUrl(), path);
requestBuilder.setUrl(totalPath);
if (RegistryEnvironment.isCanary()) {
requestBuilder.putHeaders(CANARY_HEADER, "true");
}
return Task.newBuilder().setHttpRequest(requestBuilder.build()).build();
}
@@ -200,7 +204,7 @@ public class CloudTasksUtils implements Serializable {
* default service account as the principal. That account must have permission to submit tasks to
* Cloud Tasks.
*
* <p>Prefer this overload over the one where the path and service are explicit defined, as this
* <p>Prefer this overload over the one where the path and service are explicitly defined, as this
* class will automatically determine the service to use based on the action and the runtime.
*
* @param actionClazz the action class to run, must be annotated with {@link Action}.
@@ -269,7 +273,7 @@ public class CloudTasksUtils implements Serializable {
/**
* Create a {@link Task} to be enqueued with a random delay up to {@code jitterSeconds}.
*
* <p>Prefer this overload over the one where the path and service are explicit defined, as this
* <p>Prefer this overload over the one where the path and service are explicitly defined, as this
* class will automatically determine the service to use based on the action and the runtime.
*
* @param actionClazz the action class to run, must be annotated with {@link Action}.
@@ -306,7 +310,7 @@ public class CloudTasksUtils implements Serializable {
* @param service the GAE/GKE service to route the request to.
* @param params a multimap of URL query parameters. Duplicate keys are saved as is, and it is up
* to the server to process the duplicate keys.
* @param delay the amount of time that a task needs to delayed for.
* @param delay the amount of time that a task needs to be delayed for.
* @return the enqueued task.
* @see <a
* href=ttps://cloud.google.com/appengine/docs/standard/java/taskqueue/push/creating-tasks#target>Specifyinig
@@ -330,14 +334,14 @@ public class CloudTasksUtils implements Serializable {
/**
* Create a {@link Task} to be enqueued with delay of {@code duration}.
*
* <p>Prefer this overload over the one where the path and service are explicit defined, as this
* <p>Prefer this overload over the one where the path and service are explicitly defined, as this
* class will automatically determine the service to use based on the action and the runtime.
*
* @param actionClazz the action class to run, must be annotated with {@link Action}.
* @param method the HTTP method to be used for the request.
* @param params a multimap of URL query parameters. Duplicate keys are saved as is, and it is up
* to the server to process the duplicate keys.
* @param delay the amount of time that a task needs to delayed for.
* @param delay the amount of time that a task needs to be delayed for.
* @return the enqueued task.
* @see <a
* href=ttps://cloud.google.com/appengine/docs/standard/java/taskqueue/push/creating-tasks#target>Specifyinig
@@ -20,6 +20,7 @@ import dagger.Lazy;
import google.registry.config.CredentialModule;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.keyring.KeyringModule;
import google.registry.persistence.PersistenceModule;
import google.registry.persistence.PersistenceModule.BeamJpaTm;
import google.registry.persistence.PersistenceModule.BeamReadOnlyReplicaJpaTm;
@@ -36,6 +37,7 @@ import javax.inject.Singleton;
modules = {
ConfigModule.class,
CredentialModule.class,
KeyringModule.class,
PersistenceModule.class,
SecretManagerModule.class,
UtilsModule.class
@@ -71,6 +71,7 @@ import org.joda.time.Duration;
*/
public final class RegistryConfig {
public static final String CANARY_HEADER = "canary";
private static final String ENVIRONMENT_CONFIG_FORMAT = "files/nomulus-config-%s.yaml";
private static final String YAML_CONFIG_PROD =
readResourceUtf8(RegistryConfig.class, "files/default-config.yaml");
@@ -1098,12 +1099,6 @@ public final class RegistryConfig {
return config.registryPolicy.greetingServerId;
}
@Provides
@Config("activeKeyring")
public static String provideKeyring(RegistryConfigSettings config) {
return config.keyring.activeKeyring;
}
@Provides
@Config("customLogicFactoryClass")
public static String provideCustomLogicFactoryClass(RegistryConfigSettings config) {
@@ -37,7 +37,6 @@ public class RegistryConfigSettings {
public Monitoring monitoring;
public Misc misc;
public Beam beam;
public Keyring keyring;
public RegistryTool registryTool;
public SslCertificateValidation sslCertificateValidation;
public ContactHistory contactHistory;
@@ -214,11 +213,6 @@ public class RegistryConfigSettings {
public int transientFailureRetries;
}
/** Configuration for keyrings (used to store secrets outside of source). */
public static class Keyring {
public String activeKeyring;
}
/** Configuration options for the registry tool. */
public static class RegistryTool {
public String clientId;
@@ -488,11 +488,6 @@ beam:
initialWorkerCount: 24
stagingBucketUrl: gcs-bucket-with-staged-templates
keyring:
# The name of the active keyring, either "Dummy" or "CSM". The latter stands
# for Cloud SecretManager.
activeKeyring: Dummy
# Configuration options relevant to the "nomulus" registry tool.
registryTool:
# OAuth client ID used by the tool.
@@ -54,7 +54,8 @@ public class ExportPremiumTermsAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
static final MediaType EXPORT_MIME_TYPE = MediaType.PLAIN_TEXT_UTF_8;
static final String PREMIUM_TERMS_FILENAME = "CONFIDENTIAL_premium_terms.txt";
static final String TLD_IDENTIFIER_FORMAT = "# TLD: %s";
static final String PREMIUM_TERMS_FILENAME_FORMAT = "CONFIDENTIAL_premium_terms_%s.txt";
@Inject DriveConnection driveConnection;
@@ -127,7 +128,7 @@ public class ExportPremiumTermsAction implements Runnable {
try {
String fileId =
driveConnection.createOrUpdateFile(
PREMIUM_TERMS_FILENAME,
String.format(PREMIUM_TERMS_FILENAME_FORMAT, tldStr),
EXPORT_MIME_TYPE,
tld.getDriveFolderId(),
getFormattedPremiumTerms(tld).getBytes(UTF_8));
@@ -150,11 +151,9 @@ public class ExportPremiumTermsAction implements Runnable {
.map(PremiumEntry::toString)
.collect(ImmutableSortedSet.toImmutableSortedSet(String::compareTo));
return Joiner.on("\n")
.appendTo(
new StringBuilder(),
Iterables.concat(ImmutableList.of(exportDisclaimer.trim()), premiumTerms))
.append("\n")
.toString();
String tldIdentifier = String.format(TLD_IDENTIFIER_FORMAT, tldStr);
Iterable<String> commentsAndTerms =
Iterables.concat(ImmutableList.of(exportDisclaimer.trim(), tldIdentifier), premiumTerms);
return Joiner.on("\n").join(commentsAndTerms) + "\n";
}
}
@@ -147,6 +147,13 @@ public class FlowModule {
.map(IsolationLevel::value);
}
@Provides
@FlowScope
@LogSqlStatements
boolean provideShouldLogSqlStatements(Class<? extends Flow> flowClass) {
return SqlStatementLoggingFlow.class.isAssignableFrom(flowClass);
}
@Provides
@FlowScope
@Superuser
@@ -370,4 +377,9 @@ public class FlowModule {
@Qualifier
@Documented
public @interface Transactional {}
/** Dagger qualifier for if we should log all SQL statements in a flow. */
@Qualifier
@Documented
public @interface LogSqlStatements {}
}
@@ -19,6 +19,7 @@ import static google.registry.xml.XmlTransformer.prettyPrint;
import com.google.common.flogger.FluentLogger;
import google.registry.flows.FlowModule.DryRun;
import google.registry.flows.FlowModule.InputXml;
import google.registry.flows.FlowModule.LogSqlStatements;
import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.FlowModule.Transactional;
@@ -49,6 +50,7 @@ public class FlowRunner {
@Inject @DryRun boolean isDryRun;
@Inject @Superuser boolean isSuperuser;
@Inject @Transactional boolean isTransactional;
@Inject @LogSqlStatements boolean logSqlStatements;
@Inject SessionMetadata sessionMetadata;
@Inject Trid trid;
@Inject FlowReporter flowReporter;
@@ -97,7 +99,8 @@ public class FlowRunner {
} catch (EppException e) {
throw new EppRuntimeException(e);
}
});
},
logSqlStatements);
} catch (DryRunException e) {
return e.output;
} catch (EppRuntimeException e) {
@@ -0,0 +1,24 @@
// Copyright 2025 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.flows;
/**
* Interface for a {@link Flow} that logs its SQL statements when running transactionally.
*
* <p>We don't wish to log all SQL statements ever executed (that'll create too much log bloat) but
* for some flows and some occasions we may wish to know precisely what SQL statements are being
* run.
*/
public interface SqlStatementLoggingFlow extends TransactionalFlow {}
@@ -14,7 +14,6 @@
package google.registry.flows.domain;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.dns.DnsUtils.requestDomainDnsRefresh;
import static google.registry.flows.FlowUtils.persistEntityChanges;
@@ -120,9 +119,7 @@ import google.registry.model.tmch.ClaimsList;
import google.registry.model.tmch.ClaimsListDao;
import google.registry.tmch.LordnTaskUtils.LordnPhase;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.inject.Inject;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.joda.time.Duration;
@@ -363,9 +360,7 @@ public final class DomainCreateFlow implements MutatingFlow {
// Create a new autorenew billing event and poll message starting at the expiration time.
BillingRecurrence autorenewBillingEvent =
createAutorenewBillingEvent(
domainHistoryId,
registrationExpirationTime,
getRenewalPriceInfo(isAnchorTenant, allocationToken));
domainHistoryId, registrationExpirationTime, isAnchorTenant, allocationToken);
PollMessage.Autorenew autorenewPollMessage =
createAutorenewPollMessage(domainHistoryId, registrationExpirationTime);
ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
@@ -625,7 +620,17 @@ public final class DomainCreateFlow implements MutatingFlow {
private BillingRecurrence createAutorenewBillingEvent(
HistoryEntryId domainHistoryId,
DateTime registrationExpirationTime,
RenewalPriceInfo renewalpriceInfo) {
boolean isAnchorTenant,
Optional<AllocationToken> allocationToken) {
// Non-standard renewal behaviors can occur for anchor tenants (always NONPREMIUM pricing) or if
// explicitly configured in the token (either NONPREMIUM or directly SPECIFIED). Use DEFAULT if
// none is configured.
RenewalPriceBehavior renewalPriceBehavior =
isAnchorTenant
? RenewalPriceBehavior.NONPREMIUM
: allocationToken
.map(AllocationToken::getRenewalPriceBehavior)
.orElse(RenewalPriceBehavior.DEFAULT);
return new BillingRecurrence.Builder()
.setReason(Reason.RENEW)
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
@@ -634,8 +639,8 @@ public final class DomainCreateFlow implements MutatingFlow {
.setEventTime(registrationExpirationTime)
.setRecurrenceEndTime(END_OF_TIME)
.setDomainHistoryId(domainHistoryId)
.setRenewalPriceBehavior(renewalpriceInfo.renewalPriceBehavior())
.setRenewalPrice(renewalpriceInfo.renewalPrice())
.setRenewalPriceBehavior(renewalPriceBehavior)
.setRenewalPrice(allocationToken.flatMap(AllocationToken::getRenewalPrice).orElse(null))
.build();
}
@@ -679,41 +684,6 @@ public final class DomainCreateFlow implements MutatingFlow {
.build();
}
/**
* Determines the {@link RenewalPriceBehavior} and the renewal price that needs be stored in the
* {@link BillingRecurrence} billing events.
*
* <p>By default, the renewal price is calculated during the process of renewal. Renewal price
* should be the createCost if and only if the renewal price behavior in the {@link
* AllocationToken} is 'SPECIFIED'.
*/
static RenewalPriceInfo getRenewalPriceInfo(
boolean isAnchorTenant, Optional<AllocationToken> allocationToken) {
if (isAnchorTenant) {
allocationToken.ifPresent(
token ->
checkArgument(
token.getRenewalPriceBehavior() != RenewalPriceBehavior.SPECIFIED,
"Renewal price behavior cannot be SPECIFIED for anchor tenant"));
return RenewalPriceInfo.create(RenewalPriceBehavior.NONPREMIUM, null);
} else if (allocationToken.isPresent()
&& allocationToken.get().getRenewalPriceBehavior() == RenewalPriceBehavior.SPECIFIED) {
return RenewalPriceInfo.create(
RenewalPriceBehavior.SPECIFIED, allocationToken.get().getRenewalPrice().get());
} else {
return RenewalPriceInfo.create(RenewalPriceBehavior.DEFAULT, null);
}
}
/** A record to store renewal info used in {@link BillingRecurrence} billing events. */
public record RenewalPriceInfo(
RenewalPriceBehavior renewalPriceBehavior, @Nullable Money renewalPrice) {
static RenewalPriceInfo create(
RenewalPriceBehavior renewalPriceBehavior, @Nullable Money renewalPrice) {
return new RenewalPriceInfo(renewalPriceBehavior, renewalPrice);
}
}
private static ImmutableList<FeeTransformResponseExtension> createResponseExtensions(
Optional<FeeCreateCommandExtension> feeCreate, FeesAndCredits feesAndCredits) {
return feeCreate
@@ -55,6 +55,7 @@ import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.MutatingFlow;
import google.registry.flows.SessionMetadata;
import google.registry.flows.SqlStatementLoggingFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.custom.DomainDeleteFlowCustomLogic;
import google.registry.flows.custom.DomainDeleteFlowCustomLogic.AfterValidationParameters;
@@ -117,7 +118,7 @@ import org.joda.time.Duration;
* @error {@link DomainFlowUtils.NotAuthorizedForTldException}
*/
@ReportingSpec(ActivityReportField.DOMAIN_DELETE)
public final class DomainDeleteFlow implements MutatingFlow {
public final class DomainDeleteFlow implements MutatingFlow, SqlStatementLoggingFlow {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -85,14 +85,10 @@ public final class DomainPricingLogic {
createFee = Fee.create(zeroInCurrency(currency), FeeType.CREATE, false);
} else {
DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime);
if (allocationToken.isPresent()
&& allocationToken
.get()
.getRegistrationBehavior()
.equals(RegistrationBehavior.NONPREMIUM_CREATE)) {
if (allocationToken.isPresent()) {
// Handle any special NONPREMIUM / SPECIFIED cases configured in the token
domainPrices =
DomainPrices.create(
false, tld.getCreateBillingCost(dateTime), domainPrices.getRenewCost());
applyTokenToDomainPrices(domainPrices, tld, dateTime, years, allocationToken.get());
}
Money domainCreateCost =
getDomainCreateCostWithDiscount(domainPrices, years, allocationToken, tld);
@@ -291,6 +287,9 @@ public final class DomainPricingLogic {
|| token.getRenewalPriceBehavior().equals(RenewalPriceBehavior.NONPREMIUM)) {
return tld.getStandardRenewCost(dateTime).multipliedBy(years);
}
if (token.getRenewalPriceBehavior().equals(RenewalPriceBehavior.SPECIFIED)) {
return token.getRenewalPrice().get();
}
}
return getDomainCostWithDiscount(
domainPrices.isPremium(),
@@ -357,6 +356,27 @@ public final class DomainPricingLogic {
return totalDomainFlowCost;
}
private DomainPrices applyTokenToDomainPrices(
DomainPrices domainPrices, Tld tld, DateTime dateTime, int years, AllocationToken token) {
// Convert to nonpremium iff no premium charges are included (either in create or any renewal)
boolean convertToNonPremium =
token.getRegistrationBehavior().equals(RegistrationBehavior.NONPREMIUM_CREATE)
&& (years == 1
|| !token.getRenewalPriceBehavior().equals(RenewalPriceBehavior.DEFAULT));
boolean isPremium = domainPrices.isPremium() && !convertToNonPremium;
Money createCost =
token.getRegistrationBehavior().equals(RegistrationBehavior.NONPREMIUM_CREATE)
? tld.getCreateBillingCost(dateTime)
: domainPrices.getCreateCost();
Money renewCost =
token.getRenewalPriceBehavior().equals(RenewalPriceBehavior.NONPREMIUM)
? tld.getStandardRenewCost(dateTime)
: token.getRenewalPriceBehavior().equals(RenewalPriceBehavior.SPECIFIED)
? token.getRenewalPrice().get()
: domainPrices.getRenewCost();
return DomainPrices.create(isPremium, createCost, renewCost);
}
/** An allocation token was provided that is invalid for premium domains. */
public static class AllocationTokenInvalidForPremiumNameException
extends CommandUseErrorException {
@@ -14,31 +14,22 @@
package google.registry.keyring;
import static com.google.common.base.Preconditions.checkState;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import google.registry.config.RegistryConfig.Config;
import google.registry.keyring.api.Keyring;
import java.util.Map;
import google.registry.keyring.secretmanager.SecretManagerKeyring;
import java.util.Optional;
import javax.inject.Singleton;
/** Dagger module for {@link Keyring} */
@Module
public final class KeyringModule {
public abstract class KeyringModule {
@Provides
@Binds
@Singleton
public static Keyring provideKeyring(
Map<String, Keyring> keyrings, @Config("activeKeyring") String activeKeyring) {
checkState(
keyrings.containsKey(activeKeyring),
"Invalid Keyring %s is configured; valid choices are %s",
activeKeyring,
keyrings.keySet());
return keyrings.get(activeKeyring);
}
public abstract Keyring provideKeyring(SecretManagerKeyring keyring);
@Provides
@Config("cloudSqlInstanceConnectionName")
@@ -1,205 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.keyring.api;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.flogger.FluentLogger;
import google.registry.util.ComparingInvocationHandler;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Objects;
import javax.annotation.Nullable;
import org.bouncycastle.bcpg.BCPGKey;
import org.bouncycastle.bcpg.PublicKeyPacket;
import org.bouncycastle.openpgp.PGPKeyPair;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
/**
* Checks that a second keyring returns the same result as the current one.
*
* <p>Will behave exactly like the "actualKeyring" - as in will throw / return the exact same values
* - no matter what the "secondKeyring" does. But will log a warning if "secondKeyring" acts
* differently than "actualKeyring".
*
* <p>If both keyrings threw exceptions, there is no check whether the exeptions are the same. The
* assumption is that an error happened in both, but they might report that error differently.
*/
public final class ComparatorKeyring extends ComparingInvocationHandler<Keyring> {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private ComparatorKeyring(Keyring original, Keyring second) {
super(Keyring.class, original, second);
}
/**
* Returns an instance of Keyring that is an exact proxy of "original".
*
* <p>This proxy will log any differences in return value or thrown exceptions with "second".
*/
public static Keyring create(Keyring original, Keyring second) {
return new ComparatorKeyring(original, second).makeProxy();
}
@Override
protected void log(Method method, String message) {
logger.atSevere().log("ComparatorKeyring.%s: %s", method.getName(), message);
}
/** Implements equals for the PGP classes. */
@Override
protected boolean compareResults(Method method, @Nullable Object a, @Nullable Object b) {
Class<?> clazz = method.getReturnType();
if (PGPPublicKey.class.equals(clazz)) {
return compare((PGPPublicKey) a, (PGPPublicKey) b);
}
if (PGPPrivateKey.class.equals(clazz)) {
return compare((PGPPrivateKey) a, (PGPPrivateKey) b);
}
if (PGPKeyPair.class.equals(clazz)) {
return compare((PGPKeyPair) a, (PGPKeyPair) b);
}
return super.compareResults(method, a, b);
}
/** Implements toString for the PGP classes. */
@Override
protected String stringifyResult(Method method, @Nullable Object a) {
Class<?> clazz = method.getReturnType();
if (PGPPublicKey.class.equals(clazz)) {
return stringify((PGPPublicKey) a);
}
if (PGPPrivateKey.class.equals(clazz)) {
return stringify((PGPPrivateKey) a);
}
if (PGPKeyPair.class.equals(clazz)) {
return stringify((PGPKeyPair) a);
}
return super.stringifyResult(method, a);
}
@Override
protected String stringifyThrown(Method method, Throwable throwable) {
StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
throwable.printStackTrace(printWriter);
return String.format("%s\nStack trace:\n%s", throwable.toString(), stringWriter.toString());
}
// .equals implementation for PGP types.
@VisibleForTesting
static boolean compare(@Nullable PGPKeyPair a, @Nullable PGPKeyPair b) {
if (a == null || b == null) {
return a == null && b == null;
}
return compare(a.getPublicKey(), b.getPublicKey())
&& compare(a.getPrivateKey(), b.getPrivateKey());
}
@VisibleForTesting
static boolean compare(@Nullable PGPPublicKey a, @Nullable PGPPublicKey b) {
if (a == null || b == null) {
return a == null && b == null;
}
try {
return Arrays.equals(a.getFingerprint(), b.getFingerprint())
&& Arrays.equals(a.getEncoded(), b.getEncoded());
} catch (IOException e) {
logger.atSevere().withCause(e).log(
"ComparatorKeyring error: PGPPublicKey.getEncoded failed.");
return false;
}
}
@VisibleForTesting
static boolean compare(@Nullable PGPPrivateKey a, @Nullable PGPPrivateKey b) {
if (a == null || b == null) {
return a == null && b == null;
}
return a.getKeyID() == b.getKeyID()
&& compare(a.getPrivateKeyDataPacket(), b.getPrivateKeyDataPacket())
&& compare(a.getPublicKeyPacket(), b.getPublicKeyPacket());
}
@VisibleForTesting
static boolean compare(PublicKeyPacket a, PublicKeyPacket b) {
if (a == null || b == null) {
return a == null && b == null;
}
try {
return Arrays.equals(a.getEncoded(), b.getEncoded());
} catch (IOException e) {
logger.atSevere().withCause(e).log(
"ComparatorKeyring error: PublicKeyPacket.getEncoded failed.");
return false;
}
}
@VisibleForTesting
static boolean compare(BCPGKey a, BCPGKey b) {
if (a == null || b == null) {
return a == null && b == null;
}
return Objects.equals(a.getFormat(), b.getFormat())
&& Arrays.equals(a.getEncoded(), b.getEncoded());
}
// toString implementations
@VisibleForTesting
static String stringify(PGPKeyPair a) {
if (a == null) {
return "null";
}
return MoreObjects.toStringHelper(PGPKeyPair.class)
.addValue(stringify(a.getPublicKey()))
.addValue(stringify(a.getPrivateKey()))
.toString();
}
@VisibleForTesting
static String stringify(PGPPublicKey a) {
if (a == null) {
return "null";
}
StringBuilder builder = new StringBuilder();
for (byte b : a.getFingerprint()) {
builder.append(String.format("%02x:", b));
}
return MoreObjects.toStringHelper(PGPPublicKey.class)
.add("fingerprint", builder.toString())
.toString();
}
@VisibleForTesting
static String stringify(PGPPrivateKey a) {
if (a == null) {
return "null";
}
// We need to be careful what information we output here. The private key should be private, and
// I'm not sure what is safe to put in the logs.
return MoreObjects.toStringHelper(PGPPrivateKey.class)
.add("keyId", a.getKeyID())
.toString();
}
}
@@ -1,133 +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.keyring.api;
import static com.google.common.io.Resources.getResource;
import static google.registry.keyring.api.PgpHelper.KeyRequirement.ENCRYPT_SIGN;
import static google.registry.keyring.api.PgpHelper.lookupKeyPair;
import com.google.common.base.VerifyException;
import com.google.common.io.ByteSource;
import com.google.common.io.Resources;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import dagger.multibindings.StringKey;
import java.io.IOException;
import java.io.InputStream;
import javax.annotation.concurrent.Immutable;
import javax.inject.Named;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPKeyPair;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.bc.BcPGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.bc.BcPGPSecretKeyRingCollection;
/**
* Dagger keyring module that provides an {@link InMemoryKeyring} instance populated with dummy
* values.
*
* <p>This dummy module allows the domain registry code to compile and run in an unmodified state,
* with all attempted outgoing connections failing because the supplied dummy credentials aren't
* valid. For a real system that needs to connect with external services, you should replace this
* module with one that loads real credentials from secure sources.
*
* <p>The dummy PGP keyrings are created using gnupg1/pgp1 roughly like the following (using
* gnupg2/pgp2 is an exercise left for the developer):
*
* <pre>{@code
* # mkdir gpg
* # chmod 700 gpg
* # gpg1 --homedir gpg --gen-key <<<EOF
* 1
* 1024
* 0
* Y
* Test Registry
* test-registry@example.com
*
* O
* EOF
* [press enter twice at keyring password prompts]
* # gpg1 --homedir gpg -a -o pgp-public-keyring.asc --export test-registry@example.com
* # gpg1 --homedir gpg -a -o pgp-private-keyring.asc --export-secret-keys test-registry@example.com
* # mv pgp*keyring.asc java/google/registry/keyring/api
* # rm -rf gpg
* }</pre>
*/
@Module
@Immutable
public abstract class DummyKeyringModule {
public static final String NAME = "Dummy";
/** The contents of a dummy PGP public key stored in a file. */
private static final ByteSource PGP_PUBLIC_KEYRING =
Resources.asByteSource(getResource(InMemoryKeyring.class, "pgp-public-keyring.asc"));
/** The contents of a dummy PGP private key stored in a file. */
private static final ByteSource PGP_PRIVATE_KEYRING =
Resources.asByteSource(getResource(InMemoryKeyring.class, "pgp-private-keyring.asc"));
/** The email address of the aforementioned PGP key. */
private static final String EMAIL_ADDRESS = "test-registry@example.com";
@Binds
@IntoMap
@StringKey(NAME)
abstract Keyring provideKeyring(@Named("DummyKeyring") InMemoryKeyring keyring);
/** Always returns a {@link InMemoryKeyring} instance. */
@Provides
@Named("DummyKeyring")
static InMemoryKeyring provideDummyKeyring() {
PGPKeyPair dummyKey;
try (InputStream publicInput = PGP_PUBLIC_KEYRING.openStream();
InputStream privateInput = PGP_PRIVATE_KEYRING.openStream()) {
PGPPublicKeyRingCollection publicKeys =
new BcPGPPublicKeyRingCollection(PGPUtil.getDecoderStream(publicInput));
PGPSecretKeyRingCollection privateKeys =
new BcPGPSecretKeyRingCollection(PGPUtil.getDecoderStream(privateInput));
dummyKey = lookupKeyPair(publicKeys, privateKeys, EMAIL_ADDRESS, ENCRYPT_SIGN);
} catch (PGPException | IOException e) {
throw new VerifyException("Failed to load PGP keys from jar", e);
}
// Use the same dummy PGP keypair for all required PGP keys -- a real production system would
// have different values for these keys. Pass dummy values for all Strings.
return new InMemoryKeyring(
dummyKey,
dummyKey,
dummyKey.getPublicKey(),
dummyKey,
dummyKey.getPublicKey(),
"not a real key",
"not a real key",
"not a real password",
"not a real API key",
"not a real login",
"not a real password",
"not a real login",
"not a real credential",
"not a real password",
"not a real password",
"not the real primary connection",
"not the real replica connection");
}
private DummyKeyringModule() {}
}
@@ -1,175 +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.keyring.api;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import javax.annotation.concurrent.Immutable;
import org.bouncycastle.openpgp.PGPKeyPair;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
/** A {@link Keyring} that uses in-memory values for all credentials. */
@Immutable
public final class InMemoryKeyring implements Keyring {
private final PGPKeyPair rdeStagingKey;
private final PGPKeyPair rdeSigningKey;
private final PGPPublicKey rdeReceiverKey;
private final PGPKeyPair brdaSigningKey;
private final PGPPublicKey brdaEncryptionKey;
private final String rdeSshClientPublicKey;
private final String rdeSshClientPrivateKey;
private final String icannReportingPassword;
private final String safeBrowsingAPIKey;
private final String marksdbDnlLoginAndPassword;
private final String marksdbLordnPassword;
private final String marksdbSmdrlLoginAndPassword;
private final String bsaApiKey;
private final String sqlPrimaryConnectionName;
private final String sqlReplicaConnectionName;
public InMemoryKeyring(
PGPKeyPair rdeStagingKey,
PGPKeyPair rdeSigningKey,
PGPPublicKey rdeReceiverKey,
PGPKeyPair brdaSigningKey,
PGPPublicKey brdaEncryptionKey,
String rdeSshClientPublicKey,
String rdeSshClientPrivateKey,
String icannReportingPassword,
String safeBrowsingAPIKey,
String marksdbDnlLoginAndPassword,
String marksdbLordnPassword,
String marksdbSmdrlLoginAndPassword,
String cloudSqlPassword,
String toolsCloudSqlPassword,
String bsaApiKey,
String sqlPrimaryConnectionName,
String sqlReplicaConnectionName) {
checkArgument(PgpHelper.isSigningKey(rdeSigningKey.getPublicKey()),
"RDE signing key must support signing: %s", rdeSigningKey.getKeyID());
checkArgument(rdeStagingKey.getPublicKey().isEncryptionKey(),
"staging key must support encryption: %s", rdeStagingKey.getKeyID());
checkArgument(rdeReceiverKey.isEncryptionKey(),
"receiver key must support encryption: %s", rdeReceiverKey.getKeyID());
checkArgument(PgpHelper.isSigningKey(brdaSigningKey.getPublicKey()),
"BRDA signing key must support signing: %s", brdaSigningKey.getKeyID());
checkArgument(brdaEncryptionKey.isEncryptionKey(),
"encryption key must support encryption: %s", brdaEncryptionKey.getKeyID());
this.rdeStagingKey = rdeStagingKey;
this.rdeSigningKey = rdeSigningKey;
this.rdeReceiverKey = rdeReceiverKey;
this.brdaSigningKey = brdaSigningKey;
this.brdaEncryptionKey = brdaEncryptionKey;
this.rdeSshClientPublicKey = checkNotNull(rdeSshClientPublicKey, "rdeSshClientPublicKey");
this.rdeSshClientPrivateKey = checkNotNull(rdeSshClientPrivateKey, "rdeSshClientPrivateKey");
this.icannReportingPassword = checkNotNull(icannReportingPassword, "icannReportingPassword");
this.safeBrowsingAPIKey = checkNotNull(safeBrowsingAPIKey, "safeBrowsingAPIKey");
this.marksdbDnlLoginAndPassword =
checkNotNull(marksdbDnlLoginAndPassword, "marksdbDnlLoginAndPassword");
this.marksdbLordnPassword = checkNotNull(marksdbLordnPassword, "marksdbLordnPassword");
this.marksdbSmdrlLoginAndPassword =
checkNotNull(marksdbSmdrlLoginAndPassword, "marksdbSmdrlLoginAndPassword");
this.bsaApiKey = checkNotNull(bsaApiKey, "bsaApiKey");
this.sqlPrimaryConnectionName = sqlPrimaryConnectionName;
this.sqlReplicaConnectionName = sqlReplicaConnectionName;
}
@Override
public PGPKeyPair getRdeSigningKey() {
return rdeSigningKey;
}
@Override
public PGPPublicKey getRdeStagingEncryptionKey() {
return rdeStagingKey.getPublicKey();
}
@Override
public PGPPrivateKey getRdeStagingDecryptionKey() {
return rdeStagingKey.getPrivateKey();
}
@Override
public PGPPublicKey getRdeReceiverKey() {
return rdeReceiverKey;
}
@Override
public PGPKeyPair getBrdaSigningKey() {
return brdaSigningKey;
}
@Override
public PGPPublicKey getBrdaReceiverKey() {
return brdaEncryptionKey;
}
@Override
public String getRdeSshClientPublicKey() {
return rdeSshClientPublicKey;
}
@Override
public String getRdeSshClientPrivateKey() {
return rdeSshClientPrivateKey;
}
@Override
public String getIcannReportingPassword() {
return icannReportingPassword;
}
@Override
public String getSafeBrowsingAPIKey() {
return safeBrowsingAPIKey;
}
@Override
public String getMarksdbDnlLoginAndPassword() {
return marksdbDnlLoginAndPassword;
}
@Override
public String getMarksdbLordnPassword() {
return marksdbLordnPassword;
}
@Override
public String getMarksdbSmdrlLoginAndPassword() {
return marksdbSmdrlLoginAndPassword;
}
@Override
public String getBsaApiKey() {
return bsaApiKey;
}
@Override
public String getSqlPrimaryConnectionName() {
return sqlPrimaryConnectionName;
}
@Override
public String getSqlReplicaConnectionName() {
return sqlReplicaConnectionName;
}
/** Does nothing. */
@Override
public void close() {}
}
@@ -1,40 +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.keyring.secretmanager;
import dagger.Binds;
import dagger.Module;
import dagger.multibindings.IntoMap;
import dagger.multibindings.StringKey;
import google.registry.keyring.api.Keyring;
/** Dagger module for {@link Keyring} backed by the Cloud SecretManager. */
@Module
public abstract class SecretManagerKeyringModule {
public static final String NAME = "CSM";
// TODO(b/257276342): Remove after configs in nomulus-internal are updated.
public static final String DEPRECATED_NAME = "KMS";
@Binds
@IntoMap
@StringKey(DEPRECATED_NAME)
abstract Keyring provideDeprecatedKeyring(SecretManagerKeyring keyring);
@Binds
@IntoMap
@StringKey(NAME)
abstract Keyring provideKeyring(SecretManagerKeyring keyring);
}
@@ -35,9 +35,7 @@ import google.registry.groups.GmailModule;
import google.registry.groups.GroupsModule;
import google.registry.groups.GroupssettingsModule;
import google.registry.keyring.KeyringModule;
import google.registry.keyring.api.DummyKeyringModule;
import google.registry.keyring.api.KeyModule;
import google.registry.keyring.secretmanager.SecretManagerKeyringModule;
import google.registry.module.RegistryComponent.RegistryModule;
import google.registry.module.RequestComponent.RequestComponentModule;
import google.registry.monitoring.whitebox.StackdriverModule;
@@ -69,7 +67,6 @@ import javax.inject.Singleton;
CustomLogicFactoryModule.class,
DirectoryModule.class,
DriveModule.class,
DummyKeyringModule.class,
GmailModule.class,
GroupsModule.class,
GroupssettingsModule.class,
@@ -81,7 +78,6 @@ import javax.inject.Singleton;
PersistenceModule.class,
RegistryModule.class,
RequestComponentModule.class,
SecretManagerKeyringModule.class,
SecretManagerModule.class,
ServerTridProviderModule.class,
SheetsServiceModule.class,
@@ -14,7 +14,9 @@
package google.registry.module;
import static google.registry.util.GcpJsonFormatter.setCurrentRequest;
import static google.registry.util.GcpJsonFormatter.setCurrentTraceId;
import static google.registry.util.GcpJsonFormatter.unsetCurrentRequest;
import static google.registry.util.RandomStringGenerator.insecureRandomStringGenerator;
import static google.registry.util.StringGenerator.Alphabets.HEX_DIGITS_ONLY;
@@ -67,10 +69,16 @@ public class RegistryServlet extends ServletBase {
@Override
public void service(HttpServletRequest req, HttpServletResponse rsp) throws IOException {
setCurrentTraceId(traceId());
String requestMethod = req.getMethod();
String requestUrl = req.getRequestURI();
String userAgent = String.valueOf(req.getHeader("User-Agent"));
String protocol = req.getProtocol();
setCurrentRequest(requestMethod, requestUrl, userAgent, protocol);
try {
super.service(req, rsp);
} finally {
setCurrentTraceId(null);
unsetCurrentRequest();
}
}
@@ -121,6 +121,7 @@ import google.registry.ui.server.console.ConsoleUpdateRegistrarAction;
import google.registry.ui.server.console.ConsoleUserDataAction;
import google.registry.ui.server.console.ConsoleUsersAction;
import google.registry.ui.server.console.RegistrarsAction;
import google.registry.ui.server.console.domains.ConsoleBulkDomainAction;
import google.registry.ui.server.console.settings.ContactAction;
import google.registry.ui.server.console.settings.SecurityAction;
import google.registry.ui.server.console.settings.WhoisRegistrarFieldsAction;
@@ -174,6 +175,8 @@ interface RequestComponent {
CheckApiAction checkApiAction();
ConsoleBulkDomainAction consoleBulkDomainAction();
ConsoleDomainGetAction consoleDomainGetAction();
ConsoleDomainListAction consoleDomainListAction();
@@ -32,9 +32,7 @@ import google.registry.groups.GmailModule;
import google.registry.groups.GroupsModule;
import google.registry.groups.GroupssettingsModule;
import google.registry.keyring.KeyringModule;
import google.registry.keyring.api.DummyKeyringModule;
import google.registry.keyring.api.KeyModule;
import google.registry.keyring.secretmanager.SecretManagerKeyringModule;
import google.registry.module.backend.BackendRequestComponent.BackendRequestComponentModule;
import google.registry.monitoring.whitebox.StackdriverModule;
import google.registry.persistence.PersistenceModule;
@@ -60,7 +58,6 @@ import javax.inject.Singleton;
CredentialModule.class,
CustomLogicFactoryModule.class,
DirectoryModule.class,
DummyKeyringModule.class,
DriveModule.class,
GmailModule.class,
GroupsModule.class,
@@ -71,7 +68,6 @@ import javax.inject.Singleton;
KeyringModule.class,
NetHttpTransportModule.class,
PersistenceModule.class,
SecretManagerKeyringModule.class,
SecretManagerModule.class,
ServerTridProviderModule.class,
SheetsServiceModule.class,
@@ -21,7 +21,6 @@ import google.registry.config.CredentialModule;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.groups.GmailModule;
import google.registry.keyring.KeyringModule;
import google.registry.keyring.secretmanager.SecretManagerKeyringModule;
import google.registry.module.bsa.BsaRequestComponent.BsaRequestComponentModule;
import google.registry.monitoring.whitebox.StackdriverModule;
import google.registry.persistence.PersistenceModule;
@@ -43,7 +42,6 @@ import javax.inject.Singleton;
GsonModule.class,
PersistenceModule.class,
KeyringModule.class,
SecretManagerKeyringModule.class,
SecretManagerModule.class,
StackdriverModule.class,
UrlConnectionServiceModule.class,
@@ -27,9 +27,7 @@ import google.registry.groups.GmailModule;
import google.registry.groups.GroupsModule;
import google.registry.groups.GroupssettingsModule;
import google.registry.keyring.KeyringModule;
import google.registry.keyring.api.DummyKeyringModule;
import google.registry.keyring.api.KeyModule;
import google.registry.keyring.secretmanager.SecretManagerKeyringModule;
import google.registry.module.frontend.FrontendRequestComponent.FrontendRequestComponentModule;
import google.registry.monitoring.whitebox.StackdriverModule;
import google.registry.privileges.secretmanager.SecretManagerModule;
@@ -52,7 +50,6 @@ import javax.inject.Singleton;
CustomLogicFactoryModule.class,
CloudTasksUtilsModule.class,
DirectoryModule.class,
DummyKeyringModule.class,
FrontendRequestComponentModule.class,
GmailModule.class,
GroupsModule.class,
@@ -61,7 +58,6 @@ import javax.inject.Singleton;
KeyModule.class,
KeyringModule.class,
NetHttpTransportModule.class,
SecretManagerKeyringModule.class,
SecretManagerModule.class,
ServerTridProviderModule.class,
StackdriverModule.class,
@@ -37,6 +37,7 @@ import google.registry.ui.server.console.ConsoleUpdateRegistrarAction;
import google.registry.ui.server.console.ConsoleUserDataAction;
import google.registry.ui.server.console.ConsoleUsersAction;
import google.registry.ui.server.console.RegistrarsAction;
import google.registry.ui.server.console.domains.ConsoleBulkDomainAction;
import google.registry.ui.server.console.settings.ContactAction;
import google.registry.ui.server.console.settings.SecurityAction;
import google.registry.ui.server.console.settings.WhoisRegistrarFieldsAction;
@@ -53,6 +54,8 @@ import google.registry.ui.server.console.settings.WhoisRegistrarFieldsAction;
WhiteboxModule.class,
})
public interface FrontendRequestComponent {
ConsoleBulkDomainAction consoleBulkDomainAction();
ConsoleDomainGetAction consoleDomainGetAction();
ConsoleDomainListAction consoleDomainListAction();
@@ -25,9 +25,7 @@ import google.registry.groups.DirectoryModule;
import google.registry.groups.GroupsModule;
import google.registry.groups.GroupssettingsModule;
import google.registry.keyring.KeyringModule;
import google.registry.keyring.api.DummyKeyringModule;
import google.registry.keyring.api.KeyModule;
import google.registry.keyring.secretmanager.SecretManagerKeyringModule;
import google.registry.module.pubapi.PubApiRequestComponent.PubApiRequestComponentModule;
import google.registry.monitoring.whitebox.StackdriverModule;
import google.registry.persistence.PersistenceModule;
@@ -47,7 +45,6 @@ import javax.inject.Singleton;
CredentialModule.class,
CustomLogicFactoryModule.class,
DirectoryModule.class,
DummyKeyringModule.class,
GroupsModule.class,
GroupssettingsModule.class,
GsonModule.class,
@@ -56,7 +53,6 @@ import javax.inject.Singleton;
NetHttpTransportModule.class,
PersistenceModule.class,
PubApiRequestComponentModule.class,
SecretManagerKeyringModule.class,
SecretManagerModule.class,
ServerTridProviderModule.class,
StackdriverModule.class,
@@ -27,9 +27,7 @@ import google.registry.groups.DirectoryModule;
import google.registry.groups.GroupsModule;
import google.registry.groups.GroupssettingsModule;
import google.registry.keyring.KeyringModule;
import google.registry.keyring.api.DummyKeyringModule;
import google.registry.keyring.api.KeyModule;
import google.registry.keyring.secretmanager.SecretManagerKeyringModule;
import google.registry.module.tools.ToolsRequestComponent.ToolsRequestComponentModule;
import google.registry.monitoring.whitebox.StackdriverModule;
import google.registry.privileges.secretmanager.SecretManagerModule;
@@ -49,7 +47,6 @@ import javax.inject.Singleton;
CustomLogicFactoryModule.class,
CloudTasksUtilsModule.class,
DirectoryModule.class,
DummyKeyringModule.class,
DriveModule.class,
GroupsModule.class,
GroupssettingsModule.class,
@@ -57,7 +54,6 @@ import javax.inject.Singleton;
KeyModule.class,
KeyringModule.class,
NetHttpTransportModule.class,
SecretManagerKeyringModule.class,
SecretManagerModule.class,
ServerTridProviderModule.class,
StackdriverModule.class,
@@ -17,7 +17,7 @@ package google.registry.persistence;
import dagger.Component;
import google.registry.config.CredentialModule;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.keyring.secretmanager.SecretManagerKeyringModule;
import google.registry.keyring.KeyringModule;
import google.registry.persistence.PersistenceModule.DefaultJpaTm;
import google.registry.persistence.PersistenceModule.ReadOnlyReplicaJpaTm;
import google.registry.persistence.transaction.JpaTransactionManager;
@@ -32,8 +32,8 @@ import javax.inject.Singleton;
modules = {
ConfigModule.class,
CredentialModule.class,
KeyringModule.class,
PersistenceModule.class,
SecretManagerKeyringModule.class,
SecretManagerModule.class,
UtilsModule.class
})
@@ -34,13 +34,10 @@ import dagger.BindsOptionalOf;
import dagger.Module;
import dagger.Provides;
import google.registry.config.RegistryConfig.Config;
import google.registry.keyring.KeyringModule;
import google.registry.keyring.api.DummyKeyringModule;
import google.registry.persistence.transaction.CloudSqlCredentialSupplier;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.persistence.transaction.JpaTransactionManagerImpl;
import google.registry.persistence.transaction.TransactionManager;
import google.registry.privileges.secretmanager.SecretManagerModule;
import google.registry.privileges.secretmanager.SqlCredential;
import google.registry.privileges.secretmanager.SqlCredentialStore;
import google.registry.privileges.secretmanager.SqlUser;
@@ -66,7 +63,7 @@ import javax.inject.Singleton;
import org.hibernate.cfg.Environment;
/** Dagger module class for the persistence layer. */
@Module(includes = {KeyringModule.class, SecretManagerModule.class, DummyKeyringModule.class})
@Module
public abstract class PersistenceModule {
// This name must be the same as the one defined in persistence.xml.
@@ -21,6 +21,7 @@ import jakarta.persistence.Query;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.metamodel.Metamodel;
import java.util.concurrent.Callable;
/** Sub-interface of {@link TransactionManager} which defines JPA related methods. */
public interface JpaTransactionManager extends TransactionManager {
@@ -92,4 +93,15 @@ public interface JpaTransactionManager extends TransactionManager {
/** Return the {@link TransactionIsolationLevel} used in the current transaction. */
TransactionIsolationLevel getCurrentTransactionIsolationLevel();
/** Executes the work with the given isolation level, possibly logging all SQL statements used. */
<T> T transact(
TransactionIsolationLevel isolationLevel, Callable<T> work, boolean logSqlStatements);
/**
* Executes the work with the given isolation level without retry, possibly logging all SQL
* statements used.
*/
<T> T transactNoRetry(
TransactionIsolationLevel isolationLevel, Callable<T> work, boolean logSqlStatements);
}
@@ -71,12 +71,14 @@ import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import javax.annotation.Nullable;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Environment;
import org.joda.time.DateTime;
@@ -88,6 +90,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
private static final String NESTED_TRANSACTION_MESSAGE =
"Nested transaction detected. Try refactoring to avoid nested transactions. If unachievable,"
+ " use reTransact() in nested transactions";
private static final String SQL_STATEMENT_LOG_SENTINEL_FORMAT = "SQL_STATEMENT_LOG: %s";
// EntityManagerFactory is thread safe.
private final EntityManagerFactory emf;
@@ -173,19 +176,26 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
@Override
public <T> T transact(TransactionIsolationLevel isolationLevel, Callable<T> work) {
return transact(isolationLevel, work, false);
}
@Override
public <T> T transact(
TransactionIsolationLevel isolationLevel, Callable<T> work, boolean logSqlStatements) {
if (inTransaction()) {
if (!getHibernateAllowNestedTransactions()) {
throw new IllegalStateException(NESTED_TRANSACTION_MESSAGE);
}
if (RegistryEnvironment.get() != RegistryEnvironment.PRODUCTION
&& RegistryEnvironment.get() != RegistryEnvironment.UNITTEST) {
logger.atWarning().withStackTrace(StackSize.MEDIUM).log(NESTED_TRANSACTION_MESSAGE);
if (RegistryEnvironment.get() != RegistryEnvironment.UNITTEST) {
logger.atWarning().withStackTrace(StackSize.MEDIUM).atMostEvery(1, TimeUnit.MINUTES).log(
NESTED_TRANSACTION_MESSAGE);
}
// This prevents inner transaction from retrying, thus avoiding a cascade retry effect.
return transactNoRetry(isolationLevel, work);
}
return retrier.callWithRetry(
() -> transactNoRetry(isolationLevel, work), JpaRetries::isFailedTxnRetriable);
() -> transactNoRetry(isolationLevel, work, logSqlStatements),
JpaRetries::isFailedTxnRetriable);
}
@Override
@@ -201,6 +211,14 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
@Override
public <T> T transactNoRetry(
@Nullable TransactionIsolationLevel isolationLevel, Callable<T> work) {
return transactNoRetry(isolationLevel, work, false);
}
@Override
public <T> T transactNoRetry(
@Nullable TransactionIsolationLevel isolationLevel,
Callable<T> work,
boolean logSqlStatements) {
if (inTransaction()) {
// This check will no longer be necessary when the transact() method always throws
// inside a nested transaction, as the only way to pass a non-null isolation level
@@ -223,7 +241,18 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
}
}
TransactionInfo txnInfo = transactionInfo.get();
txnInfo.entityManager = emf.createEntityManager();
txnInfo.entityManager =
logSqlStatements
? emf.unwrap(SessionFactory.class)
.withOptions()
.statementInspector(
s -> {
logger.atInfo().log(SQL_STATEMENT_LOG_SENTINEL_FORMAT, s);
return s;
})
.openSession()
: emf.createEntityManager();
if (readOnly) {
// Disable Hibernate's dirty object check on flushing, it has become more aggressive in v6.
txnInfo.entityManager.unwrap(Session.class).setDefaultReadOnly(true);
@@ -52,7 +52,7 @@ final class DeleteAllocationTokensCommand extends UpdateOrDeleteAllocationTokens
@Override
public void init() {
tokensToDelete = getTokenKeys(tokens, prefix);
tokensToDelete = tm().transact(() -> getTokenKeys(tokens, prefix));
}
@Override
@@ -136,7 +136,7 @@ abstract class EppToolCommand extends ConfirmingCommand implements CommandWithCo
params.put("dryRun", dryRun);
params.put("clientId", command.clientId);
params.put("superuser", superuser);
params.put("xml", URLEncoder.encode(command.xml, UTF_8.toString()));
params.put("xml", URLEncoder.encode(command.xml, UTF_8));
String requestBody =
Joiner.on('&').withKeyValueSeparator("=").join(filterValues(params, Objects::nonNull));
responses.add(
@@ -43,7 +43,8 @@ import org.postgresql.util.PSQLException;
final class RegistryCli implements CommandRunner {
private static final ImmutableSet<RegistryToolEnvironment> DEFAULT_GKE_ENVIRONMENTS =
ImmutableSet.of(RegistryToolEnvironment.ALPHA, RegistryToolEnvironment.QA);
ImmutableSet.of(
RegistryToolEnvironment.ALPHA, RegistryToolEnvironment.CRASH, RegistryToolEnvironment.QA);
// The environment parameter is parsed twice: once here, and once with {@link
// RegistryToolEnvironment#parseFromArgs} in the {@link RegistryTool#main} function.
@@ -27,9 +27,7 @@ import google.registry.dns.writer.VoidDnsWriterModule;
import google.registry.dns.writer.clouddns.CloudDnsWriterModule;
import google.registry.dns.writer.dnsupdate.DnsUpdateWriterModule;
import google.registry.keyring.KeyringModule;
import google.registry.keyring.api.DummyKeyringModule;
import google.registry.keyring.api.KeyModule;
import google.registry.keyring.secretmanager.SecretManagerKeyringModule;
import google.registry.model.ModelModule;
import google.registry.persistence.PersistenceModule;
import google.registry.persistence.PersistenceModule.NomulusToolJpaTm;
@@ -60,7 +58,6 @@ import javax.inject.Singleton;
ConfigModule.class,
CloudDnsWriterModule.class,
CloudTasksUtilsModule.class,
DummyKeyringModule.class,
DnsUpdateWriterModule.class,
GsonModule.class,
KeyModule.class,
@@ -71,7 +68,6 @@ import javax.inject.Singleton;
RdeModule.class,
RegistryToolDataflowModule.class,
RequestFactoryModule.class,
SecretManagerKeyringModule.class,
SecretManagerModule.class,
UrlConnectionServiceModule.class,
UtilsModule.class,
@@ -20,6 +20,7 @@ import static com.google.common.base.Verify.verify;
import static com.google.common.net.HttpHeaders.X_REQUESTED_WITH;
import static com.google.common.net.MediaType.JSON_UTF_8;
import static google.registry.config.ConfigUtils.makeUrl;
import static google.registry.config.RegistryConfig.CANARY_HEADER;
import static google.registry.security.JsonHttp.JSON_SAFETY_PREFIX;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -58,8 +59,6 @@ public class ServiceConnection {
/** Pattern to heuristically extract title tag contents in HTML responses. */
protected static final Pattern HTML_TITLE_TAG_PATTERN = Pattern.compile("<title>(.*?)</title>");
private static final String CANARY_HEADER = "canary";
private final Service service;
private final boolean useCanary;
private final HttpRequestFactory requestFactory;
@@ -59,21 +59,16 @@ abstract class UpdateOrDeleteAllocationTokensCommand extends ConfirmingCommand {
.map(token -> VKey.create(AllocationToken.class, token))
.collect(toImmutableList());
ImmutableList<VKey<AllocationToken>> nonexistentKeys =
tm().transact(
() -> keys.stream().filter(key -> !tm().exists(key)).collect(toImmutableList()));
keys.stream().filter(key -> !tm().exists(key)).collect(toImmutableList());
checkState(nonexistentKeys.isEmpty(), "Tokens with keys %s did not exist", nonexistentKeys);
return keys;
} else {
checkArgument(!prefix.isEmpty(), "Provided prefix should not be blank");
return tm().transact(
() ->
tm().query(
"SELECT token FROM AllocationToken WHERE token LIKE :prefix",
String.class)
.setParameter("prefix", String.format("%s%%", prefix))
.getResultStream()
.map(token -> VKey.create(AllocationToken.class, token))
.collect(toImmutableList()));
return tm().query("SELECT token FROM AllocationToken WHERE token LIKE :prefix", String.class)
.setParameter("prefix", String.format("%s%%", prefix))
.getResultStream()
.map(token -> VKey.create(AllocationToken.class, token))
.collect(toImmutableList());
}
}
}
@@ -1,228 +0,0 @@
// Copyright 2024 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.ui.server.console;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.ImmutableMap;
import com.google.common.escape.Escaper;
import com.google.common.xml.XmlEscapers;
import com.google.gson.JsonElement;
import com.google.gson.annotations.Expose;
import google.registry.flows.EppController;
import google.registry.flows.EppRequestSource;
import google.registry.flows.PasswordOnlyTransportCredentials;
import google.registry.flows.StatelessRequestSessionMetadata;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.User;
import google.registry.model.eppcommon.ProtocolDefinition;
import google.registry.model.eppoutput.EppOutput;
import google.registry.model.eppoutput.Result;
import google.registry.request.Action;
import google.registry.request.OptionalJsonPayload;
import google.registry.request.Parameter;
import google.registry.request.auth.Auth;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.inject.Inject;
/**
* Console endpoint to perform the same action to a list of domains.
*
* <p>All requests must include the {@link BulkAction} to perform as well as a {@link
* BulkDomainList} of domains on which to apply the action. The remaining contents of the request
* body depend on the type of action -- some requests may require more data than others.
*/
@Action(
service = Action.GaeService.DEFAULT,
gkeService = Action.GkeService.CONSOLE,
path = ConsoleBulkDomainAction.PATH,
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public class ConsoleBulkDomainAction extends ConsoleApiAction {
public static final String PATH = "/console-api/bulk-domain";
private static Escaper XML_ESCAPER = XmlEscapers.xmlContentEscaper();
public enum BulkAction {
DELETE,
SUSPEND
}
/** All requests must include at least a list of domain names on which to perform the action. */
public record BulkDomainList(@Expose List<String> domainList) {}
public record BulkDomainDeleteRequest(@Expose String reason) {}
public record BulkDomainSuspendRequest(@Expose String reason) {}
private static final String DOMAIN_DELETE_XML =
"""
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<delete>
<domain:delete
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>%DOMAIN_NAME%</domain:name>
</domain:delete>
</delete>
<extension>
<metadata:metadata xmlns:metadata="urn:google:params:xml:ns:metadata-1.0">
<metadata:reason>%REASON%</metadata:reason>
<metadata:requestedByRegistrar>true</metadata:requestedByRegistrar>
</metadata:metadata>
</extension>
<clTRID>RegistryConsole</clTRID>
</command>
</epp>""";
private static final String DOMAIN_SUSPEND_XML =
"""
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<epp
xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<update>
<domain:update
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>%DOMAIN_NAME%</domain:name>
<domain:add>
<domain:status s="serverDeleteProhibited" lang="en"></domain:status>
<domain:status s="serverHold" lang="en"></domain:status>
<domain:status s="serverRenewProhibited" lang="en"></domain:status>
<domain:status s="serverTransferProhibited" lang="en"></domain:status>
<domain:status s="serverUpdateProhibited" lang="en"></domain:status>
</domain:add>
<domain:rem></domain:rem>
</domain:update>
</update>
<extension>
<metadata:metadata
xmlns:metadata="urn:google:params:xml:ns:metadata-1.0">
<metadata:reason>Console suspension: %REASON%</metadata:reason>
<metadata:requestedByRegistrar>false</metadata:requestedByRegistrar>
</metadata:metadata>
</extension>
<clTRID>RegistryTool</clTRID>
</command>
</epp>""";
private final EppController eppController;
private final String registrarId;
private final String bulkDomainAction;
private final Optional<JsonElement> optionalJsonPayload;
@Inject
public ConsoleBulkDomainAction(
ConsoleApiParams consoleApiParams,
EppController eppController,
@Parameter("registrarId") String registrarId,
@Parameter("bulkDomainAction") String bulkDomainAction,
@OptionalJsonPayload Optional<JsonElement> optionalJsonPayload) {
super(consoleApiParams);
this.eppController = eppController;
this.registrarId = registrarId;
this.bulkDomainAction = bulkDomainAction;
this.optionalJsonPayload = optionalJsonPayload;
}
@Override
protected void postHandler(User user) {
BulkAction bulkAction = BulkAction.valueOf(bulkDomainAction);
JsonElement jsonPayload =
optionalJsonPayload.orElseThrow(
() -> new IllegalArgumentException("Bulk action payload must be present"));
BulkDomainList domainList = consoleApiParams.gson().fromJson(jsonPayload, BulkDomainList.class);
checkPermission(user, registrarId, ConsolePermission.EXECUTE_EPP_COMMANDS);
ImmutableMap<String, ConsoleEppOutput> result =
switch (bulkAction) {
case DELETE -> handleBulkDelete(jsonPayload, domainList, user);
case SUSPEND -> handleBulkSuspend(jsonPayload, domainList, user);
};
// Front end should parse situations where only some commands worked
consoleApiParams.response().setPayload(consoleApiParams.gson().toJson(result));
consoleApiParams.response().setStatus(SC_OK);
}
private ImmutableMap<String, ConsoleEppOutput> handleBulkDelete(
JsonElement jsonPayload, BulkDomainList domainList, User user) {
String reason =
consoleApiParams.gson().fromJson(jsonPayload, BulkDomainDeleteRequest.class).reason;
return runCommandOverDomains(
domainList,
DOMAIN_DELETE_XML,
new ImmutableMap.Builder<String, String>().put("REASON", reason),
user);
}
private ImmutableMap<String, ConsoleEppOutput> handleBulkSuspend(
JsonElement jsonPayload, BulkDomainList domainList, User user) {
String reason =
consoleApiParams.gson().fromJson(jsonPayload, BulkDomainSuspendRequest.class).reason;
return runCommandOverDomains(
domainList,
DOMAIN_SUSPEND_XML,
new ImmutableMap.Builder<String, String>().put("REASON", reason),
user);
}
/** Runs the provided XML template and substitutions over a provided list of domains. */
private ImmutableMap<String, ConsoleEppOutput> runCommandOverDomains(
BulkDomainList domainList,
String xmlTemplate,
ImmutableMap.Builder<String, String> replacements,
User user) {
return domainList.domainList.stream()
.collect(
toImmutableMap(
d -> d,
d ->
executeEpp(
fillSubstitutions(xmlTemplate, replacements.put("DOMAIN_NAME", d)), user)));
}
private ConsoleEppOutput executeEpp(String xml, User user) {
return ConsoleEppOutput.fromEppOutput(
eppController.handleEppCommand(
new StatelessRequestSessionMetadata(
registrarId, ProtocolDefinition.getVisibleServiceExtensionUris()),
new PasswordOnlyTransportCredentials(),
EppRequestSource.CONSOLE,
false,
user.getUserRoles().isAdmin(),
xml.getBytes(UTF_8)));
}
/** Fills the provided XML template with the replacement values, including escaping the values. */
private String fillSubstitutions(
String xmlTemplate, ImmutableMap.Builder<String, String> replacements) {
String xml = xmlTemplate;
for (Map.Entry<String, String> entry : replacements.buildKeepingLast().entrySet()) {
xml = xml.replaceAll("%" + entry.getKey() + "%", XML_ESCAPER.escape(entry.getValue()));
}
return xml;
}
public record ConsoleEppOutput(@Expose String message, @Expose int responseCode) {
static ConsoleEppOutput fromEppOutput(EppOutput eppOutput) {
Result result = eppOutput.getResponse().getResult();
return new ConsoleEppOutput(result.getMsg(), result.getCode().code);
}
}
}
@@ -0,0 +1,125 @@
// Copyright 2024 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.ui.server.console.domains;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.ImmutableMap;
import com.google.gson.JsonElement;
import com.google.gson.annotations.Expose;
import google.registry.flows.EppController;
import google.registry.flows.EppRequestSource;
import google.registry.flows.PasswordOnlyTransportCredentials;
import google.registry.flows.StatelessRequestSessionMetadata;
import google.registry.model.console.User;
import google.registry.model.eppcommon.ProtocolDefinition;
import google.registry.model.eppoutput.EppOutput;
import google.registry.model.eppoutput.Result;
import google.registry.request.Action;
import google.registry.request.OptionalJsonPayload;
import google.registry.request.Parameter;
import google.registry.request.auth.Auth;
import google.registry.ui.server.console.ConsoleApiAction;
import google.registry.ui.server.console.ConsoleApiParams;
import java.util.List;
import java.util.Optional;
import javax.inject.Inject;
/**
* Console endpoint to perform the same action to a list of domains.
*
* <p>All requests must include the {@link ConsoleDomainActionType.BulkAction} to perform as well as
* a {@link BulkDomainList} of domains on which to apply the action. The remaining contents of the
* request body depend on the type of action -- some requests may require more data than others.
*/
@Action(
service = Action.GaeService.DEFAULT,
gkeService = Action.GkeService.CONSOLE,
path = ConsoleBulkDomainAction.PATH,
method = Action.Method.POST,
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public class ConsoleBulkDomainAction extends ConsoleApiAction {
public static final String PATH = "/console-api/bulk-domain";
/** All requests must include at least a list of domain names on which to perform the action. */
public record BulkDomainList(@Expose List<String> domainList) {}
private final EppController eppController;
private final String registrarId;
private final String bulkDomainAction;
private final Optional<JsonElement> optionalJsonPayload;
@Inject
public ConsoleBulkDomainAction(
ConsoleApiParams consoleApiParams,
EppController eppController,
@Parameter("registrarId") String registrarId,
@Parameter("bulkDomainAction") String bulkDomainAction,
@OptionalJsonPayload Optional<JsonElement> optionalJsonPayload) {
super(consoleApiParams);
this.eppController = eppController;
this.registrarId = registrarId;
this.bulkDomainAction = bulkDomainAction;
this.optionalJsonPayload = optionalJsonPayload;
}
@Override
protected void postHandler(User user) {
// Temporary flag while testing
if (!user.getUserRoles().isAdmin()) {
consoleApiParams.response().setStatus(SC_FORBIDDEN);
return;
}
JsonElement jsonPayload =
optionalJsonPayload.orElseThrow(
() -> new IllegalArgumentException("Bulk action payload must be present"));
BulkDomainList domainList = consoleApiParams.gson().fromJson(jsonPayload, BulkDomainList.class);
ConsoleDomainActionType actionType =
ConsoleDomainActionType.parseActionType(bulkDomainAction, jsonPayload);
checkPermission(user, registrarId, actionType.getNecessaryPermission());
ImmutableMap<String, ConsoleEppOutput> result =
domainList.domainList.stream()
.collect(
toImmutableMap(d -> d, d -> executeEpp(actionType.getXmlContentsToRun(d), user)));
// Front end should parse situations where only some commands worked
consoleApiParams.response().setPayload(consoleApiParams.gson().toJson(result));
consoleApiParams.response().setStatus(SC_OK);
}
private ConsoleEppOutput executeEpp(String xml, User user) {
return ConsoleEppOutput.fromEppOutput(
eppController.handleEppCommand(
new StatelessRequestSessionMetadata(
registrarId, ProtocolDefinition.getVisibleServiceExtensionUris()),
new PasswordOnlyTransportCredentials(),
EppRequestSource.CONSOLE,
false,
user.getUserRoles().isAdmin(),
xml.getBytes(UTF_8)));
}
public record ConsoleEppOutput(@Expose String message, @Expose int responseCode) {
static ConsoleEppOutput fromEppOutput(EppOutput eppOutput) {
Result result = eppOutput.getResponse().getResult();
return new ConsoleEppOutput(result.getMsg(), result.getCode().code);
}
}
}
@@ -0,0 +1,61 @@
// Copyright 2025 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.ui.server.console.domains;
import com.google.common.collect.ImmutableMap;
import com.google.gson.JsonElement;
import google.registry.model.console.ConsolePermission;
/** An action that will run a delete EPP command on the given domain. */
public class ConsoleBulkDomainDeleteActionType implements ConsoleDomainActionType {
private static final String DOMAIN_DELETE_XML =
"""
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<delete>
<domain:delete
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>%DOMAIN_NAME%</domain:name>
</domain:delete>
</delete>
<extension>
<metadata:metadata xmlns:metadata="urn:google:params:xml:ns:metadata-1.0">
<metadata:reason>%REASON%</metadata:reason>
<metadata:requestedByRegistrar>true</metadata:requestedByRegistrar>
</metadata:metadata>
</extension>
<clTRID>RegistryConsole</clTRID>
</command>
</epp>""";
private final String reason;
public ConsoleBulkDomainDeleteActionType(JsonElement jsonElement) {
this.reason = jsonElement.getAsJsonObject().get("reason").getAsString();
}
@Override
public String getXmlContentsToRun(String domainName) {
return ConsoleDomainActionType.fillSubstitutions(
DOMAIN_DELETE_XML, ImmutableMap.of("DOMAIN_NAME", domainName, "REASON", reason));
}
@Override
public ConsolePermission getNecessaryPermission() {
return ConsolePermission.EXECUTE_EPP_COMMANDS;
}
}
@@ -0,0 +1,71 @@
// Copyright 2025 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.ui.server.console.domains;
import com.google.common.collect.ImmutableMap;
import com.google.gson.JsonElement;
import google.registry.model.console.ConsolePermission;
/** An action that will suspend the given domain, assigning all 5 server*Prohibited statuses. */
public class ConsoleBulkDomainSuspendActionType implements ConsoleDomainActionType {
private static final String DOMAIN_SUSPEND_XML =
"""
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<epp
xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<update>
<domain:update
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>%DOMAIN_NAME%</domain:name>
<domain:add>
<domain:status s="serverDeleteProhibited" lang="en"></domain:status>
<domain:status s="serverHold" lang="en"></domain:status>
<domain:status s="serverRenewProhibited" lang="en"></domain:status>
<domain:status s="serverTransferProhibited" lang="en"></domain:status>
<domain:status s="serverUpdateProhibited" lang="en"></domain:status>
</domain:add>
<domain:rem></domain:rem>
</domain:update>
</update>
<extension>
<metadata:metadata
xmlns:metadata="urn:google:params:xml:ns:metadata-1.0">
<metadata:reason>Console suspension: %REASON%</metadata:reason>
<metadata:requestedByRegistrar>false</metadata:requestedByRegistrar>
</metadata:metadata>
</extension>
<clTRID>RegistryConsole</clTRID>
</command>
</epp>""";
private final String reason;
public ConsoleBulkDomainSuspendActionType(JsonElement jsonElement) {
this.reason = jsonElement.getAsJsonObject().get("reason").getAsString();
}
@Override
public String getXmlContentsToRun(String domainName) {
return ConsoleDomainActionType.fillSubstitutions(
DOMAIN_SUSPEND_XML, ImmutableMap.of("DOMAIN_NAME", domainName, "REASON", reason));
}
@Override
public ConsolePermission getNecessaryPermission() {
return ConsolePermission.SUSPEND_DOMAIN;
}
}
@@ -0,0 +1,69 @@
// Copyright 2025 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.ui.server.console.domains;
import com.google.common.collect.ImmutableMap;
import com.google.common.escape.Escaper;
import com.google.common.xml.XmlEscapers;
import com.google.gson.JsonElement;
import google.registry.model.console.ConsolePermission;
import java.util.Map;
/**
* A type of EPP action to perform on domain(s), run by the {@link ConsoleBulkDomainAction}.
*
* <p>Each {@link BulkAction} defines the class that implements that action, including the EPP XML
* that will be run and the permission required.
*/
public interface ConsoleDomainActionType {
enum BulkAction {
DELETE(ConsoleBulkDomainDeleteActionType.class),
SUSPEND(ConsoleBulkDomainSuspendActionType.class);
private final Class<? extends ConsoleDomainActionType> actionClass;
BulkAction(Class<? extends ConsoleDomainActionType> actionClass) {
this.actionClass = actionClass;
}
public Class<? extends ConsoleDomainActionType> getActionClass() {
return actionClass;
}
}
Escaper XML_ESCAPER = XmlEscapers.xmlContentEscaper();
static String fillSubstitutions(String xmlTemplate, ImmutableMap<String, String> replacements) {
String xml = xmlTemplate;
for (Map.Entry<String, String> entry : replacements.entrySet()) {
xml = xml.replaceAll("%" + entry.getKey() + "%", XML_ESCAPER.escape(entry.getValue()));
}
return xml;
}
String getXmlContentsToRun(String domainName);
ConsolePermission getNecessaryPermission();
static ConsoleDomainActionType parseActionType(String bulkDomainAction, JsonElement jsonElement) {
BulkAction bulkAction = BulkAction.valueOf(bulkDomainAction);
try {
return bulkAction.getActionClass().getConstructor(JsonElement.class).newInstance(jsonElement);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e); // shouldn't happen
}
}
}
@@ -17,7 +17,6 @@ package google.registry.export;
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.export.ExportPremiumTermsAction.EXPORT_MIME_TYPE;
import static google.registry.export.ExportPremiumTermsAction.PREMIUM_TERMS_FILENAME;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.deleteTld;
import static google.registry.testing.DatabaseHelper.persistResource;
@@ -52,11 +51,10 @@ import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link ExportPremiumTermsAction}. */
public class ExportPremiumTermsActionTest {
private static final String DISCLAIMER_WITH_NEWLINE = "# Premium Terms Export Disclaimer\n";
private static final ImmutableList<String> PREMIUM_NAMES =
ImmutableList.of("2048,USD 549", "0,USD 549");
private static final String EXPECTED_FILE_CONTENT =
DISCLAIMER_WITH_NEWLINE + "0, 549.00\n" + "2048, 549.00\n";
"# Premium Terms Export Disclaimer\n# TLD: tld\n0, 549.00\n" + "2048, 549.00\n";
@RegisterExtension
final JpaIntegrationTestExtension jpa =
@@ -69,7 +67,7 @@ public class ExportPremiumTermsActionTest {
ExportPremiumTermsAction action = new ExportPremiumTermsAction();
action.response = response;
action.driveConnection = driveConnection;
action.exportDisclaimer = DISCLAIMER_WITH_NEWLINE;
action.exportDisclaimer = "# Premium Terms Export Disclaimer\n";
action.tldStr = tld;
action.run();
}
@@ -94,7 +92,7 @@ public class ExportPremiumTermsActionTest {
verify(driveConnection)
.createOrUpdateFile(
PREMIUM_TERMS_FILENAME,
"CONFIDENTIAL_premium_terms_tld.txt",
EXPORT_MIME_TYPE,
"folder_id",
EXPECTED_FILE_CONTENT.getBytes(UTF_8));
@@ -157,7 +155,7 @@ public class ExportPremiumTermsActionTest {
verify(driveConnection)
.createOrUpdateFile(
PREMIUM_TERMS_FILENAME,
"CONFIDENTIAL_premium_terms_tld.txt",
EXPORT_MIME_TYPE,
"bad_folder_id",
EXPECTED_FILE_CONTENT.getBytes(UTF_8));
@@ -56,7 +56,7 @@ import org.junit.jupiter.api.extension.RegisterExtension;
public abstract class ResourceFlowTestCase<F extends Flow, R extends EppResource>
extends FlowTestCase<F> {
private final TestLogHandler logHandler = new TestLogHandler();
protected final TestLogHandler logHandler = new TestLogHandler();
@RegisterExtension
public final TestCacheExtension testCacheExtension =
@@ -22,7 +22,6 @@ import static google.registry.flows.FlowTestCase.UserPrivileges.SUPERUSER;
import static google.registry.model.billing.BillingBase.Flag.ANCHOR_TENANT;
import static google.registry.model.billing.BillingBase.Flag.RESERVED;
import static google.registry.model.billing.BillingBase.Flag.SUNRISE;
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.DEFAULT;
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.NONPREMIUM;
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.SPECIFIED;
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL;
@@ -87,7 +86,6 @@ import google.registry.flows.domain.DomainCreateFlow.BulkDomainRegisteredForTooM
import google.registry.flows.domain.DomainCreateFlow.MustHaveSignedMarksInCurrentPhaseException;
import google.registry.flows.domain.DomainCreateFlow.NoGeneralRegistrationsInCurrentPhaseException;
import google.registry.flows.domain.DomainCreateFlow.NoTrademarkedRegistrationsBeforeSunriseException;
import google.registry.flows.domain.DomainCreateFlow.RenewalPriceInfo;
import google.registry.flows.domain.DomainCreateFlow.SignedMarksOnlyDuringSunriseException;
import google.registry.flows.domain.DomainFlowTmchUtils.FoundMarkExpiredException;
import google.registry.flows.domain.DomainFlowTmchUtils.FoundMarkNotYetValidException;
@@ -303,10 +301,8 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
boolean isAnchorTenant = expectedBillingFlags.contains(ANCHOR_TENANT);
// Set up the creation cost.
BigDecimal createCost =
isDomainPremium(getUniqueIdFromCommand(), clock.nowUtc())
? BigDecimal.valueOf(200)
: BigDecimal.valueOf(24);
boolean isDomainPremium = isDomainPremium(getUniqueIdFromCommand(), clock.nowUtc());
BigDecimal createCost = isDomainPremium ? BigDecimal.valueOf(200) : BigDecimal.valueOf(24);
if (isAnchorTenant) {
createCost = BigDecimal.ZERO;
}
@@ -315,6 +311,26 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
createCost.multiply(
BigDecimal.valueOf(1 - RegistryConfig.getSunriseDomainCreateDiscount()));
}
if (allocationToken != null) {
if (allocationToken
.getRegistrationBehavior()
.equals(RegistrationBehavior.NONPREMIUM_CREATE)) {
createCost =
createCost.subtract(
BigDecimal.valueOf(isDomainPremium ? 87 : 0)); // premium is 100, standard 13
}
if (allocationToken.getRenewalPriceBehavior().equals(NONPREMIUM)) {
createCost =
createCost.subtract(
BigDecimal.valueOf(isDomainPremium ? 89 : 0)); // premium is 100, standard 11
}
if (allocationToken.getRenewalPriceBehavior().equals(SPECIFIED)) {
createCost =
createCost
.subtract(BigDecimal.valueOf(isDomainPremium ? 100 : 11))
.add(allocationToken.getRenewalPrice().get().getAmount());
}
}
FeesAndCredits feesAndCredits =
new FeesAndCredits.Builder()
.setCurrency(USD)
@@ -343,8 +359,12 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.hasType(HistoryEntry.Type.DOMAIN_CREATE)
.and()
.hasPeriodYears(2);
RenewalPriceInfo renewalPriceInfo =
DomainCreateFlow.getRenewalPriceInfo(isAnchorTenant, Optional.ofNullable(allocationToken));
RenewalPriceBehavior expectedRenewalPriceBehavior =
isAnchorTenant
? RenewalPriceBehavior.NONPREMIUM
: Optional.ofNullable(allocationToken)
.map(AllocationToken::getRenewalPriceBehavior)
.orElse(RenewalPriceBehavior.DEFAULT);
// There should be one bill for the create and one for the recurrence autorenew event.
BillingEvent createBillingEvent =
new BillingEvent.Builder()
@@ -369,8 +389,11 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.setEventTime(domain.getRegistrationExpirationTime())
.setRecurrenceEndTime(END_OF_TIME)
.setDomainHistory(historyEntry)
.setRenewalPriceBehavior(renewalPriceInfo.renewalPriceBehavior())
.setRenewalPrice(renewalPriceInfo.renewalPrice())
.setRenewalPriceBehavior(expectedRenewalPriceBehavior)
.setRenewalPrice(
Optional.ofNullable(allocationToken)
.flatMap(AllocationToken::getRenewalPrice)
.orElse(null))
.build();
ImmutableSet.Builder<BillingBase> expectedBillingEvents =
@@ -3187,85 +3210,62 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
}
@Test
void testGetRenewalPriceInfo_isAnchorTenantWithoutToken_returnsNonPremiumAndNullPrice() {
assertThat(DomainCreateFlow.getRenewalPriceInfo(true, Optional.empty()))
.isEqualTo(RenewalPriceInfo.create(NONPREMIUM, null));
}
@Test
void testGetRenewalPriceInfo_isAnchorTenantWithDefaultToken_returnsNonPremiumAndNullPrice() {
assertThat(DomainCreateFlow.getRenewalPriceInfo(true, Optional.of(allocationToken)))
.isEqualTo(RenewalPriceInfo.create(NONPREMIUM, null));
}
@Test
void testGetRenewalPriceInfo_isNotAnchorTenantWithDefaultToken_returnsDefaultAndNullPrice() {
assertThat(DomainCreateFlow.getRenewalPriceInfo(false, Optional.of(allocationToken)))
.isEqualTo(RenewalPriceInfo.create(DEFAULT, null));
}
@Test
void testGetRenewalPriceInfo_isNotAnchorTenantWithoutToken_returnsDefaultAndNullPrice() {
assertThat(DomainCreateFlow.getRenewalPriceInfo(false, Optional.empty()))
.isEqualTo(RenewalPriceInfo.create(DEFAULT, null));
}
@Test
void
testGetRenewalPriceInfo_isNotAnchorTenantWithSpecifiedInToken_returnsSpecifiedAndCreatePrice() {
void testSuccess_anchorTenant_nonPremiumRenewal() throws Exception {
AllocationToken token =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(USD, 5))
.setDomainName("example.tld")
.setRegistrationBehavior(RegistrationBehavior.ANCHOR_TENANT)
.build());
assertThat(DomainCreateFlow.getRenewalPriceInfo(false, Optional.of(token)))
.isEqualTo(RenewalPriceInfo.create(SPECIFIED, Money.of(USD, 5)));
persistContactsAndHosts();
setEppInput(
"domain_create_allocationtoken.xml",
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2"));
runFlow();
assertSuccessfulCreate("tld", ImmutableSet.of(ANCHOR_TENANT), token);
}
@Test
void testGetRenewalPriceInfo_isAnchorTenantWithSpecifiedStateInToken_throwsError() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
DomainCreateFlow.getRenewalPriceInfo(
true,
Optional.of(
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.build()))));
assertThat(thrown)
.hasMessageThat()
.isEqualTo("Renewal price behavior cannot be SPECIFIED for anchor tenant");
void testSuccess_nonAnchorTenant_nonPremiumRenewal() throws Exception {
createTld("example");
AllocationToken token =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("rich.example")
.setRenewalPriceBehavior(NONPREMIUM)
.build());
persistContactsAndHosts();
// Creation is still $100 but it'll create a NONPREMIUM renewal
setEppInput(
"domain_create_premium_allocationtoken.xml",
ImmutableMap.of("YEARS", "2", "FEE", "111.00"));
runFlow();
assertSuccessfulCreate("example", ImmutableSet.of(), token);
}
@Test
void testGetRenewalPriceInfo_withInvalidRenewalPriceBehavior_throwsError() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
DomainCreateFlow.getRenewalPriceInfo(
true,
Optional.of(
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRenewalPriceBehavior(RenewalPriceBehavior.valueOf("INVALID"))
.build()))));
assertThat(thrown)
.hasMessageThat()
.isEqualTo(
"No enum constant"
+ " google.registry.model.billing.BillingBase.RenewalPriceBehavior.INVALID");
void testSuccess_specifiedRenewalPriceToken_specifiedRecurrencePrice() throws Exception {
createTld("example");
AllocationToken token =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("rich.example")
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(USD, 1))
.build());
persistContactsAndHosts();
// Creation is still $100 but it'll create a $1 renewal
setEppInput(
"domain_create_premium_allocationtoken.xml",
ImmutableMap.of("YEARS", "2", "FEE", "101.00"));
runFlow();
assertSuccessfulCreate("example", ImmutableSet.of(), token);
}
@Test
@@ -102,8 +102,10 @@ import google.registry.model.transfer.TransferResponse;
import google.registry.model.transfer.TransferStatus;
import google.registry.testing.CloudTasksHelper.TaskMatcher;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.LogsSubject;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.joda.time.Duration;
@@ -1258,4 +1260,17 @@ class DomainDeleteFlowTest extends ResourceFlowTestCase<DomainDeleteFlow, Domain
runFlowAssertResponse(loadFile("domain_delete_response_pending.xml"));
assertPollMessages();
}
@Test
void testSuccess_logsSqlStatements() throws Exception {
setUpSuccessfulTest();
runFlowAssertResponse(loadFile("domain_delete_response_pending.xml"));
LogsSubject.assertAboutLogs()
.that(logHandler)
.hasLogAtLevelWithMessage(
Level.INFO,
"SQL_STATEMENT_LOG: insert into \"DomainHistory\" (history_by_superuser,"
+ "history_registrar_id,history_modification_time,history_other_registrar_id,"
+ "history_period_unit,history_period_value,history_reason,history");
}
}
@@ -1127,7 +1127,7 @@ public class DomainPricingLogicTest {
.setCurrency(USD)
.addFeeOrCredit(Fee.create(new BigDecimal("13.00"), CREATE, false))
.build());
// Two-year create should be 13 (standard price) + 100 (premium price)
// Two-year create should be 13 (standard price) + 100 (premium price), and it's premium
assertThat(
domainPricingLogic.getCreatePrice(
tld,
@@ -1140,7 +1140,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(new BigDecimal("113.00"), CREATE, false))
.addFeeOrCredit(Fee.create(new BigDecimal("113.00"), CREATE, true))
.build());
assertThat(
domainPricingLogic.getRenewPrice(
@@ -1156,4 +1156,109 @@ public class DomainPricingLogicTest {
.addFeeOrCredit(Fee.create(new BigDecimal("100.00"), RENEW, true))
.build());
}
@Test
void testGetDomainCreatePrice_premium_multiYear_nonpremiumCreateAndRenewal() throws Exception {
AllocationToken allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("premium.example")
.setRegistrationBehavior(AllocationToken.RegistrationBehavior.NONPREMIUM_CREATE)
.setRenewalPriceBehavior(NONPREMIUM)
.build());
// Two-year create should be standard create (13) + renewal (10) because both create and renewal
// are standard
assertThat(
domainPricingLogic.getCreatePrice(
tld,
"premium.example",
clock.nowUtc(),
2,
false,
false,
Optional.of(allocationToken)))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(new BigDecimal("23.00"), CREATE, false))
.build());
// Similarly, 3 years should be 13 + 10 + 10
assertThat(
domainPricingLogic.getCreatePrice(
tld,
"premium.example",
clock.nowUtc(),
3,
false,
false,
Optional.of(allocationToken)))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(new BigDecimal("33.00"), CREATE, false))
.build());
}
@Test
void testGetDomainCreatePrice_premium_multiYear_onlyNonpremiumRenewal() throws Exception {
AllocationToken allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("premium.example")
.setRenewalPriceBehavior(NONPREMIUM)
.build());
// Two-year create should be 100 (premium 1st year) plus 10 (nonpremium 2nd year)
assertThat(
domainPricingLogic.getCreatePrice(
tld,
"premium.example",
clock.nowUtc(),
2,
false,
false,
Optional.of(allocationToken)))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(new BigDecimal("110.00"), CREATE, true))
.build());
// Similarly, 3 years should be 100 + 10 + 10
assertThat(
domainPricingLogic.getCreatePrice(
tld,
"premium.example",
clock.nowUtc(),
3,
false,
false,
Optional.of(allocationToken)))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(new BigDecimal("120.00"), CREATE, true))
.build());
}
@Test
void testDomainRenewPrice_specifiedToken() throws Exception {
AllocationToken allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("premium.example")
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(USD, 5))
.build());
assertThat(
domainPricingLogic
.getRenewPrice(
tld, "premium.example", clock.nowUtc(), 1, null, Optional.of(allocationToken))
.getRenewCost())
.isEqualTo(Money.of(USD, 5));
}
}
@@ -1,335 +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.keyring.api;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.LogsSubject.assertAboutLogs;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.google.common.testing.TestLogHandler;
import google.registry.util.JdkLoggerConfig;
import java.io.IOException;
import java.util.logging.Level;
import org.bouncycastle.bcpg.BCPGKey;
import org.bouncycastle.bcpg.PublicKeyPacket;
import org.bouncycastle.openpgp.PGPKeyPair;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link ComparatorKeyring}. */
class ComparatorKeyringTest {
private static final String PUBLIC_KEY_FINGERPRINT = "fingerprint";
private static final String PUBLIC_KEY_TO_STRING =
"PGPPublicKey{fingerprint=66:69:6e:67:65:72:70:72:69:6e:74:}";
private static final String PRIVATE_KEY_TO_STRING =
"PGPPrivateKey{keyId=1}";
private static final String KEY_PAIR_TO_STRING =
String.format("PGPKeyPair{%s, %s}", PUBLIC_KEY_TO_STRING, PRIVATE_KEY_TO_STRING);
private static PGPPublicKey mockPublicKey(
boolean altFingerprint,
boolean altEncoded) throws IOException {
PGPPublicKey publicKey = mock(PGPPublicKey.class);
String fingerprint = altFingerprint ? "alternate" : PUBLIC_KEY_FINGERPRINT;
String encoded = altEncoded ? "alternate" : "publicKeyEncoded";
when(publicKey.getFingerprint()).thenReturn(fingerprint.getBytes(UTF_8));
when(publicKey.getEncoded()).thenReturn(encoded.getBytes(UTF_8));
return publicKey;
}
private static PGPPrivateKey mockPrivateKey(
boolean altId,
boolean altBcpgKeyFormat,
boolean altBcpgKeyEncoded,
boolean altPublicKeyPacketEncoded)
throws IOException {
String bcpgKeyFormat = altBcpgKeyFormat ? "alternate" : "bcpgFormat";
String bcpgKeyEncoded = altBcpgKeyEncoded ? "alternate" : "bcpgEncoded";
String publicKeyPacketEncoded = altPublicKeyPacketEncoded ? "alternate" : "packetEncoded";
BCPGKey bcpgKey = mock(BCPGKey.class);
PublicKeyPacket publicKeyPacket = mock(PublicKeyPacket.class);
when(bcpgKey.getFormat()).thenReturn(bcpgKeyFormat);
when(bcpgKey.getEncoded()).thenReturn(bcpgKeyEncoded.getBytes(UTF_8));
when(publicKeyPacket.getEncoded()).thenReturn(publicKeyPacketEncoded.getBytes(UTF_8));
return new PGPPrivateKey(altId ? 2 : 1, publicKeyPacket, bcpgKey);
}
private final TestLogHandler testLogHandler = new TestLogHandler();
@BeforeEach
void beforeEach() {
JdkLoggerConfig.getConfig(ComparatorKeyring.class).addHandler(testLogHandler);
}
@AfterEach
void afterEach() {
JdkLoggerConfig.getConfig(ComparatorKeyring.class).removeHandler(testLogHandler);
}
@Test
void testPublicKeyToString() throws Exception {
assertThat(
ComparatorKeyring.stringify(
mockPublicKey(false, false)))
.isEqualTo(PUBLIC_KEY_TO_STRING);
}
@Test
void testPublicKeyEquals() throws Exception {
assertThat(
ComparatorKeyring.compare(
mockPublicKey(false, false),
mockPublicKey(false, false)))
.isTrue();
}
@Test
void testPublicKeyDifferFingerprint_notEqual() throws Exception {
assertThat(
ComparatorKeyring.compare(
mockPublicKey(false, false),
mockPublicKey(true, false)))
.isFalse();
}
@Test
void testPublicKeyDifferEncoded_notEqual() throws Exception {
assertThat(
ComparatorKeyring.compare(
mockPublicKey(false, false),
mockPublicKey(false, true)))
.isFalse();
}
@Test
void testPrivateKeyToString() throws Exception {
assertThat(
ComparatorKeyring.stringify(
mockPrivateKey(false, false, false, false)))
.isEqualTo(PRIVATE_KEY_TO_STRING);
}
@Test
void testPrivateKeyEquals() throws Exception {
assertThat(
ComparatorKeyring.compare(
mockPrivateKey(false, false, false, false),
mockPrivateKey(false, false, false, false)))
.isTrue();
}
@Test
void testPrivateKeyDifferId_notEquals() throws Exception {
assertThat(
ComparatorKeyring.compare(
mockPrivateKey(false, false, false, false),
mockPrivateKey(true, false, false, false)))
.isFalse();
}
@Test
void testPrivateKeyDifferBcpgFormat_notEquals() throws Exception {
assertThat(
ComparatorKeyring.compare(
mockPrivateKey(false, false, false, false),
mockPrivateKey(false, true, false, false)))
.isFalse();
}
@Test
void testPrivateKeyDifferBcpgEncoding_notEquals() throws Exception {
assertThat(
ComparatorKeyring.compare(
mockPrivateKey(false, false, false, false),
mockPrivateKey(false, false, true, false)))
.isFalse();
}
@Test
void testPrivateKeyDifferPublicKeyEncoding_notEquals() throws Exception {
assertThat(
ComparatorKeyring.compare(
mockPrivateKey(false, false, false, false),
mockPrivateKey(false, false, false, true)))
.isFalse();
}
@Test
void testKeyPairToString() throws Exception {
assertThat(
ComparatorKeyring.stringify(
new PGPKeyPair(
mockPublicKey(false, false),
mockPrivateKey(false, false, false, false))))
.isEqualTo(KEY_PAIR_TO_STRING);
}
@Test
void testKeyPairEquals() throws Exception {
assertThat(
ComparatorKeyring.compare(
new PGPKeyPair(
mockPublicKey(false, false),
mockPrivateKey(false, false, false, false)),
new PGPKeyPair(
mockPublicKey(false, false),
mockPrivateKey(false, false, false, false))))
.isTrue();
}
@Test
void testKeyPairDifferPublicKey_notEqual() throws Exception {
assertThat(
ComparatorKeyring.compare(
new PGPKeyPair(
mockPublicKey(false, false),
mockPrivateKey(false, false, false, false)),
new PGPKeyPair(
mockPublicKey(true, false),
mockPrivateKey(false, false, false, false))))
.isFalse();
}
@Test
void testKeyPairDifferPrivateKey_notEqual() throws Exception {
assertThat(
ComparatorKeyring.compare(
new PGPKeyPair(
mockPublicKey(false, false),
mockPrivateKey(false, false, false, false)),
new PGPKeyPair(
mockPublicKey(false, false),
mockPrivateKey(true, false, false, false))))
.isFalse();
}
// We don't need to check every single method in the generated instance to see that it behaves
// correctly. This should have been tested in ComparatorGenerator.
//
// We will fully test a single method just to make sure everything is "connected" correctly.
@Test
void testRdeSigningKey_actualThrows() throws Exception {
Keyring actualKeyring = mock(Keyring.class);
Keyring secondKeyring = mock(Keyring.class);
PGPKeyPair keyPair =
new PGPKeyPair(
mockPublicKey(false, false),
mockPrivateKey(false, false, false, false));
when(actualKeyring.getRdeSigningKey()).thenThrow(new KeyringException("message"));
when(secondKeyring.getRdeSigningKey()).thenReturn(keyPair);
Keyring comparatorKeyring = ComparatorKeyring.create(actualKeyring, secondKeyring);
assertThrows(KeyringException.class, comparatorKeyring::getRdeSigningKey);
assertAboutLogs()
.that(testLogHandler)
.hasLogAtLevelWithMessage(
Level.SEVERE, ".getRdeSigningKey: Only actual implementation threw exception");
}
@Test
void testRdeSigningKey_secondThrows() throws Exception {
Keyring actualKeyring = mock(Keyring.class);
Keyring secondKeyring = mock(Keyring.class);
PGPKeyPair keyPair =
new PGPKeyPair(
mockPublicKey(false, false),
mockPrivateKey(false, false, false, false));
when(actualKeyring.getRdeSigningKey()).thenReturn(keyPair);
when(secondKeyring.getRdeSigningKey()).thenThrow(new KeyringException("message"));
Keyring comparatorKeyring = ComparatorKeyring.create(actualKeyring, secondKeyring);
assertThat(comparatorKeyring.getRdeSigningKey()).isSameInstanceAs(keyPair);
assertAboutLogs()
.that(testLogHandler)
.hasLogAtLevelWithMessage(
Level.SEVERE, ".getRdeSigningKey: Only second implementation threw exception");
}
@Test
void testRdeSigningKey_bothThrow() {
Keyring actualKeyring = mock(Keyring.class);
Keyring secondKeyring = mock(Keyring.class);
when(actualKeyring.getRdeSigningKey()).thenThrow(new KeyringException("message"));
when(secondKeyring.getRdeSigningKey()).thenThrow(new KeyringException("message"));
Keyring comparatorKeyring = ComparatorKeyring.create(actualKeyring, secondKeyring);
assertThrows(KeyringException.class, comparatorKeyring::getRdeSigningKey);
assertAboutLogs().that(testLogHandler).hasNoLogsAtLevel(Level.SEVERE);
}
@Test
void testRdeSigningKey_same() throws Exception {
Keyring actualKeyring = mock(Keyring.class);
Keyring secondKeyring = mock(Keyring.class);
PGPKeyPair keyPair =
new PGPKeyPair(
mockPublicKey(false, false),
mockPrivateKey(false, false, false, false));
PGPKeyPair keyPairCopy =
new PGPKeyPair(
mockPublicKey(false, false),
mockPrivateKey(false, false, false, false));
when(actualKeyring.getRdeSigningKey()).thenReturn(keyPair);
when(secondKeyring.getRdeSigningKey()).thenReturn(keyPairCopy);
Keyring comparatorKeyring = ComparatorKeyring.create(actualKeyring, secondKeyring);
assertThat(comparatorKeyring.getRdeSigningKey()).isSameInstanceAs(keyPair);
assertAboutLogs().that(testLogHandler).hasNoLogsAtLevel(Level.SEVERE);
}
@Test
void testRdeSigningKey_different() throws Exception {
Keyring actualKeyring = mock(Keyring.class);
Keyring secondKeyring = mock(Keyring.class);
PGPKeyPair keyPair =
new PGPKeyPair(
mockPublicKey(false, false),
mockPrivateKey(false, false, false, false));
PGPKeyPair keyPairDifferent =
new PGPKeyPair(
mockPublicKey(false, false),
mockPrivateKey(true, false, false, false));
when(actualKeyring.getRdeSigningKey()).thenReturn(keyPair);
when(secondKeyring.getRdeSigningKey()).thenReturn(keyPairDifferent);
Keyring comparatorKeyring = ComparatorKeyring.create(actualKeyring, secondKeyring);
assertThat(comparatorKeyring.getRdeSigningKey()).isSameInstanceAs(keyPair);
String alternateKeyPairString = String.format(
"PGPKeyPair{%s, %s}", PUBLIC_KEY_TO_STRING, "PGPPrivateKey{keyId=2}");
assertAboutLogs()
.that(testLogHandler)
.hasLogAtLevelWithMessage(
Level.SEVERE,
String.format(
".getRdeSigningKey: Got different results! '%s' vs '%s'",
KEY_PAIR_TO_STRING,
alternateKeyPairString));
}
}
@@ -20,7 +20,6 @@ import dagger.Component;
import google.registry.config.CredentialModule;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.keyring.secretmanager.SecretManagerKeyringModule;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import google.registry.privileges.secretmanager.SecretManagerModule;
import google.registry.util.UtilsModule;
@@ -84,7 +83,6 @@ class PersistenceModuleTest {
ConfigModule.class,
CredentialModule.class,
PersistenceModule.class,
SecretManagerKeyringModule.class,
SecretManagerModule.class,
UtilsModule.class
})
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.ui.server.console;
package google.registry.ui.server.console.domains;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL;
@@ -33,7 +33,6 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import google.registry.flows.DaggerEppTestComponent;
import google.registry.flows.EppController;
import google.registry.flows.EppTestComponent;
@@ -50,7 +49,7 @@ import google.registry.testing.ConsoleApiParamsUtils;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeResponse;
import google.registry.tools.GsonUtils;
import java.util.Map;
import google.registry.ui.server.console.ConsoleApiParams;
import java.util.Optional;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
@@ -168,13 +167,13 @@ public class ConsoleBulkDomainActionTest {
@Test
void testFailure_badActionString() {
ConsoleBulkDomainAction action = createAction("bad", null);
ConsoleBulkDomainAction action = createAction("bad", GSON.toJsonTree(ImmutableMap.of()));
action.run();
assertThat(fakeResponse.getStatus()).isEqualTo(SC_BAD_REQUEST);
assertThat(fakeResponse.getPayload())
.isEqualTo(
"No enum constant"
+ " google.registry.ui.server.console.ConsoleBulkDomainAction.BulkAction.bad");
+ " google.registry.ui.server.console.domains.ConsoleDomainActionType.BulkAction.bad");
}
@Test
@@ -188,7 +187,8 @@ public class ConsoleBulkDomainActionTest {
@Test
void testFailure_noPermission() {
JsonElement payload =
GSON.toJsonTree(ImmutableMap.of("domainList", ImmutableList.of("domain.tld")));
GSON.toJsonTree(
ImmutableMap.of("domainList", ImmutableList.of("domain.tld"), "reason", "reason"));
ConsoleBulkDomainAction action =
createAction(
"DELETE",
@@ -205,29 +205,33 @@ public class ConsoleBulkDomainActionTest {
assertThat(fakeResponse.getStatus()).isEqualTo(SC_FORBIDDEN);
}
@Test
void testFailure_suspend_nonAdmin() {
ConsoleBulkDomainAction action =
createAction(
"SUSPEND",
GSON.toJsonTree(
ImmutableMap.of("domainList", ImmutableList.of("example.tld"), "reason", "test")));
action.run();
assertThat(fakeResponse.getStatus()).isEqualTo(SC_OK);
Map<String, ConsoleBulkDomainAction.ConsoleEppOutput> payload =
GSON.fromJson(fakeResponse.getPayload(), new TypeToken<>() {});
assertThat(payload).containsKey("example.tld");
assertThat(payload.get("example.tld").responseCode()).isEqualTo(2004);
assertThat(payload.get("example.tld").message()).contains("cannot be set by clients");
assertThat(loadByEntity(domain)).isEqualTo(domain);
}
// @ptkach - reenable with suspend change
// @Test
// void testFailure_suspend_nonAdmin() {
// ConsoleBulkDomainAction action =
// createAction(
// "SUSPEND",
// GSON.toJsonTree(
// ImmutableMap.of("domainList", ImmutableList.of("example.tld"), "reason",
// "test")),
// user);
// action.run();
// assertThat(fakeResponse.getStatus()).isEqualTo(SC_OK);
// Map<String, ConsoleBulkDomainAction.ConsoleEppOutput> payload =
// GSON.fromJson(fakeResponse.getPayload(), new TypeToken<>() {});
// assertThat(payload).containsKey("example.tld");
// assertThat(payload.get("example.tld").responseCode()).isEqualTo(2004);
// assertThat(payload.get("example.tld").message()).contains("cannot be set by clients");
// assertThat(loadByEntity(domain)).isEqualTo(domain);
// }
private ConsoleBulkDomainAction createAction(String action, JsonElement payload) {
User user =
persistResource(
new User.Builder()
.setEmailAddress("email@email.com")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build())
.setUserRoles(
new UserRoles.Builder().setIsAdmin(true).setGlobalRole(GlobalRole.FTE).build())
.build());
return createAction(action, payload, user);
}
@@ -77,8 +77,10 @@ public class ConsoleScreenshotTest extends WebDriverTestCase {
assertThat(driver.getCurrentUrl()).endsWith("?registrarId=TheRegistrar");
}
@RetryingTest(3)
void dums_mainPage() throws Exception {
server.setGlobalRole(GlobalRole.FTE);
clickSidebarElementByName("Domains");
driver.diffPage("noRegistrarSelected");
selectRegistrar();
@@ -90,20 +92,19 @@ public class ConsoleScreenshotTest extends WebDriverTestCase {
driver.diffPage("actionsButtonClicked");
}
// TODO: Reenable failing test after kokoro issue is resolved
// @RetryingTest(3)
// void settingsPage() throws Exception {
// clickSidebarElementByName("Settings");
// driver.diffPage("noRegistrarSelected");
// selectRegistrar();
// driver.diffPage("registrarSelected_contacts");
// driver.findElement(By.cssSelector("a[routerLink=\"whois\"]")).click();
// Thread.sleep(500);
// driver.diffPage("registrarSelected_whois");
// driver.findElement(By.cssSelector("a[routerLink=\"security\"]")).click();
// Thread.sleep(500);
// driver.diffPage("registrarSelected_security");
// }
@RetryingTest(3)
void settingsPage() throws Exception {
clickSidebarElementByName("Settings");
driver.diffPage("noRegistrarSelected");
selectRegistrar();
driver.diffPage("registrarSelected_contacts");
driver.findElement(By.cssSelector("a[routerLink=\"whois\"]")).click();
Thread.sleep(500);
driver.diffPage("registrarSelected_whois");
driver.findElement(By.cssSelector("a[routerLink=\"security\"]")).click();
Thread.sleep(500);
driver.diffPage("registrarSelected_security");
}
@RetryingTest(3)
void billingInfo() throws Exception {
@@ -1,5 +1,6 @@
SERVICE PATH CLASS METHODS OK MIN USER_POLICY
FRONTEND /_dr/epp EppTlsAction POST n APP ADMIN
CONSOLE /console-api/bulk-domain ConsoleBulkDomainAction POST n USER PUBLIC
CONSOLE /console-api/domain ConsoleDomainGetAction GET n USER PUBLIC
CONSOLE /console-api/domain-list ConsoleDomainListAction GET n USER PUBLIC
CONSOLE /console-api/dum-download ConsoleDumDownloadAction GET n USER PUBLIC
@@ -13,4 +14,4 @@ CONSOLE /console-api/settings/contacts ContactAction GET,
CONSOLE /console-api/settings/security SecurityAction POST n USER PUBLIC
CONSOLE /console-api/settings/whois-fields WhoisRegistrarFieldsAction POST n USER PUBLIC
CONSOLE /console-api/userdata ConsoleUserDataAction GET n USER PUBLIC
CONSOLE /console-api/users ConsoleUsersAction GET,POST,DELETE,PUT n USER PUBLIC
CONSOLE /console-api/users ConsoleUsersAction GET,POST,DELETE,PUT n USER PUBLIC
@@ -66,6 +66,7 @@ PUBAPI /rdap/ip/(*) RdapIpAction
PUBAPI /rdap/nameserver/(*) RdapNameserverAction GET,HEAD n NONE PUBLIC
PUBAPI /rdap/nameservers RdapNameserverSearchAction GET,HEAD n NONE PUBLIC
PUBAPI /whois/(*) WhoisHttpAction GET n NONE PUBLIC
CONSOLE /console-api/bulk-domain ConsoleBulkDomainAction POST n USER PUBLIC
CONSOLE /console-api/domain ConsoleDomainGetAction GET n USER PUBLIC
CONSOLE /console-api/domain-list ConsoleDomainListAction GET n USER PUBLIC
CONSOLE /console-api/dum-download ConsoleDumDownloadAction GET n USER PUBLIC
@@ -79,4 +80,4 @@ CONSOLE /console-api/settings/contacts ContactAction
CONSOLE /console-api/settings/security SecurityAction POST n USER PUBLIC
CONSOLE /console-api/settings/whois-fields WhoisRegistrarFieldsAction POST n USER PUBLIC
CONSOLE /console-api/userdata ConsoleUserDataAction GET n USER PUBLIC
CONSOLE /console-api/users ConsoleUsersAction GET,POST,DELETE,PUT n USER PUBLIC
CONSOLE /console-api/users ConsoleUsersAction GET,POST,DELETE,PUT n USER PUBLIC
Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

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