1
0
mirror of https://github.com/google/nomulus synced 2026-01-26 07:32:24 +00:00

Compare commits

..

28 Commits

Author SHA1 Message Date
Ben McIlwain
0e8cd75a58 Add the ability to configure a ratio of proxy metrics to be recorded (#2772)
This ratio defaults to 1.0 (i.e. all metrics will be recorded), but we will set
it much lower in sandbox and production, probably something closer to 0.01. This
will reduce recorded metrics volume and thus StackDriver cost, while still
retaining enough data for overall performance monitoring.

This is handled stochastically, so as to not require any coordination between
Java threads or GKE pods/clusters, as alternative approaches would (i.e. using a
counter and recording every Nth, or throttling to a max metrics qps).
2025-06-27 05:03:59 +00:00
gbrodman
2a1748ba9c Cache history values for RDAP domain requests (#2777)
In RDAP, domain queries are the most common by a factor of like 40,000
so we should optimize these as much as possible. We already have an EPP
resource / foreign key cache which does improve performance somewhat but
looking at some sample logs, it only cuts the RDAP request times by like
40% (looking at requests for the same domain a few seconds apart).

History entries don't change often, so we should cache them to make
subsequent queries faster as well. In addition, we're only caching two
fields per repo ID (modification time, registrar ID) so we can cache
more entries than we can for the EPP resource cache (which stores large
objects).
2025-06-25 19:33:36 +00:00
Weimin Yu
f4889191a4 Fix prober cert renewal scripts (#2776)
Scripts needed by cron jobs wrongly removed by PR 2661.

TESTED: in crash.
2025-06-25 13:51:06 +00:00
Weimin Yu
9eddecf70f Bypass config check for caching when safe (#2773)
Pubapi actions should always use cache, regardless of the config
settings on caching.

In EppResource.java, the original `loadCached(Iterable<VKey>)`
method is renamed to `loadByCacheIfEnabled`. The original
`loadCached(Vkey)` method is renamed to `loadByCache` and always
uses cache.

In EppResourceUtils.java, the original `loadByForeignKeyCached`
method is renamed to `loadByForeignKeyByCacheIfEnabled`. A new
`loadByForeignKeyByCache` method, which always uses cache.

In ForeighKeyUtils.java, the original `loadCached` method is
renamed to `loadByCacheIfEnabled`, and a new `loadCached` method
is added which always uses cache.

Also added a `getContactsFromReplica` method in Registrar,
for use by RDAP actions.
2025-06-20 21:25:02 +00:00
gbrodman
d4bcff0c31 Add password reset Java object (#2765)
A future PR will add the actions that save and use this object. That
future PR will also require loading RegistrarPoc objects given the
registrar ID, hence the change in that class.
2025-06-17 19:00:50 +00:00
Ben McIlwain
62065f88fb Remove spurious parenthesis in URS command output (#2767)
It was making the undo nomulus command look like this:

)nomulus ...
2025-06-16 20:23:48 +00:00
Pavlo Tkach
c9ac9437fd Add java code for RegitrarPoc id (#2770) 2025-06-14 17:37:11 +00:00
gbrodman
1f6a09182d Add some changes related to RDAP Feb 2024 profile (#2759)
This implements two type of changes:
1. changing the link type for things like the terms of service
2. adding the request URL to each and every link with the "value" field.
   This is a bit tricky to implement because the links are generated in
various places, but we can implement it by adding it to the results
after generation.

See b/418782147 for more information
2025-06-11 20:30:15 +00:00
Weimin Yu
a0eff00031 Add an aggregate module for DNS writers (#2769)
Add a new DnsWritersModule for use by the component classes.

To override the set of writers installed, we can easily overwrite this
file with a private version.
2025-06-09 14:46:54 +00:00
gbrodman
89698c6ed6 Update version of google-java-format (#2766)
This picks up a few changes including aligning the placement of quotes
in text blocks with the Google style guide.
2025-06-06 18:11:54 +00:00
gbrodman
a7696c3fac Add console action test base case (#2762)
We can probably improve on this in the future if we want, but there's a
lot of boilerplate that we don't need to repeat over and over
2025-06-04 15:36:22 +00:00
Weimin Yu
7ec599f849 Fix create_cdns_tld command (#2760)
The Cloud DNS rest api is now case-sensitive about enum names (must be
lower case, counterintuitively).
2025-06-03 15:17:43 +00:00
Pavlo Tkach
70291af9ad Add RegistrarPoc id column (#2761) 2025-06-02 15:43:03 +00:00
gbrodman
5fb95f38ed Don't always require contacts in CreateDomainCommand (#2755)
If contacts are optional, they should be optional in the command too.
2025-05-15 20:22:07 +00:00
gbrodman
dfe8e24761 Add registrar_id col to password reset requests (#2756)
This is just so that we can add an additional layer of security on
verification
2025-05-15 20:13:27 +00:00
Juan Celhay
bd30fcc81c Remove registrar id from invoice grouping key (#2749)
* Remove registrar id from invoice grouping key

* Fix formatting issues

* Update BillingEventTests
2025-05-13 20:29:25 +00:00
gbrodman
8cecc8d3a8 Use the primary DB for DomainInfoFlow (#2750)
This avoids potential replication lag issues when requesting info on
domains that were just created.
2025-05-13 18:00:30 +00:00
Pavlo Tkach
c5a39bccc5 Add Console POC reminder front-end (#2754) 2025-05-12 20:14:56 +00:00
gbrodman
a90a117341 Add SQL table for password resets (#2751)
We plan on using this for EPP password resets and registry lock password
resets for now.
2025-05-08 19:16:08 +00:00
Weimin Yu
b40ad54daf Hardcode beam pipelines to use GKE for tasks (#2753) 2025-05-08 17:29:30 +00:00
Pavlo Tkach
b4d239c329 Add console POC reminder backend support (#2747) 2025-04-30 14:15:43 +00:00
gbrodman
daa7ab3bfa Disable primary-contact editing in console (#2745)
This is necessary because we'll use primary-contact emails as a way of
resetting passwords.

In the UI, don't allow editing of email address for primary contacts,
and don't allow addition/removal of the primary contact field
post-creation.

In the backend, make sure that all emails previously added still exist.
2025-04-29 17:32:29 +00:00
gbrodman
56cd2ad282 Change AllocationToken behavior in non-catastrophic situations (#2730)
We're changing the way that allocation tokens work in suboptimal (i.e. incorrect) situations in the domain check, creation, and renewal process. Currently, if a token is not applicable, in any way, to any of the operations (including when a check has multiple operations requested) we return some variation of "Allocation token not valid" for all of those options. We wish to allow for a more lenient process, where if a token is "not applicable" instead of "invalid", we just pass through that part of the request as if the token were not there.

Types of errors that will remain catastrophic, where we'll basically return a token error immediately in all cases:
- nonexistent or null token
- token is assigned to a particular domain and the request isn't for that domain
- token is not valid for this registrar
- token is a single-use token that has already been redeemed
- token has a promotional schedule and it's no longer valid

Types of errors that will now be a silent pass-through, as if the user did not issue a token:
- token is not allowed for this TLD
- token has a discount, is not valid for premium names, and the domain name is premium
- token does not allow the provided EPP action

Currently, the last three types of errors cause that generic "token invalid" message but in the future, we'll pass the requests through as if the user did not pass in a token. This does allow for a default token to apply to these requests if available, meaning that it's possible that a single DomainCheckFlow with multiple check requests could use the provided token for some check(s), and a default token for others.

The flip side of this is that if the user passes in a catastrophically invalid token (the first five error messages above), we will return that result to any/all checks that they request, even if there are other issues with that request (e.g. the domain is reserved or already registered).

See b/315504612 for more details and background
2025-04-23 15:09:37 +00:00
gbrodman
0472dda860 Remove transaction duration logging (#2748)
We suspected this could be a cause of optimistic locking failures
(because long transactions would lead to optimistic locks not being
released) but this didn't end up being the case. Let's remove this to
reduce log spam.
2025-04-22 18:53:21 +00:00
gbrodman
083a9dc8c9 Remove old console history Java classes (#2726)
1. This doesn't remove the SQL tables yet (this is necessary to pass
   tests and also good practice just in case we need or want to look at
history for a little bit)
2. This also removes the Registrar, RegistrarPoc, and User base classes
   that were only necessary because we were saving copies of those
objects in the old history classes.
2025-04-18 22:05:29 +00:00
gbrodman
0153c6284a Add user objects for local test server (#2744)
Also don't try to do anything related to Google admin directory objects
when running the local test server, for obvious reasons
2025-04-18 15:48:06 +00:00
Pavlo Tkach
ca240adfb6 Add new last_poc_verification_date field to Registrar object (#2746) 2025-04-17 19:41:10 +00:00
Pavlo Tkach
b17125ae9a Disable k8s whois routing (#2740) 2025-04-17 15:20:32 +00:00
274 changed files with 8626 additions and 8784 deletions

View File

@@ -14,30 +14,71 @@
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { AppComponent } from './app.component';
import { MaterialModule } from './material.module';
import { BackendService } from './shared/services/backend.service';
import { AppRoutingModule } from './app-routing.module';
import { routes } from './app-routing.module';
import { AppModule } from './app.module';
import { PocReminderComponent } from './shared/components/pocReminder/pocReminder.component';
import { RouterModule } from '@angular/router';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { UserData, UserDataService } from './shared/services/userData.service';
import { Registrar, RegistrarService } from './registrar/registrar.service';
import { MatSidenavModule } from '@angular/material/sidenav';
import { signal, WritableSignal } from '@angular/core';
describe('AppComponent', () => {
let component: AppComponent;
let fixture: ComponentFixture<AppComponent>;
let mockRegistrarService: {
registrar: WritableSignal<Partial<Registrar> | null | undefined>;
registrarId: WritableSignal<string>;
registrars: WritableSignal<Array<Partial<Registrar>>>;
};
let mockUserDataService: { userData: WritableSignal<Partial<UserData>> };
let mockSnackBar: jasmine.SpyObj<MatSnackBar>;
const dummyPocReminderComponent = class {}; // Dummy class for type checking
beforeEach(async () => {
mockRegistrarService = {
registrar: signal<Registrar | null | undefined>(undefined),
registrarId: signal('123'),
registrars: signal([]),
};
mockUserDataService = {
userData: signal({
globalRole: 'NONE',
}),
};
mockSnackBar = jasmine.createSpyObj('MatSnackBar', ['openFromComponent']);
await TestBed.configureTestingModule({
declarations: [AppComponent],
imports: [
MaterialModule,
BrowserAnimationsModule,
AppRoutingModule,
MatSidenavModule,
NoopAnimationsModule,
MatSnackBarModule,
AppModule,
RouterModule.forRoot(routes),
],
providers: [
BackendService,
{ provide: RegistrarService, useValue: mockRegistrarService },
{ provide: UserDataService, useValue: mockUserDataService },
{ provide: MatSnackBar, useValue: mockSnackBar },
{ provide: PocReminderComponent, useClass: dummyPocReminderComponent },
provideHttpClient(),
provideHttpClientTesting(),
],
}).compileComponents();
fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('should create the app', () => {
@@ -46,4 +87,51 @@ describe('AppComponent', () => {
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
describe('PoC Verification Reminder', () => {
beforeEach(() => {
jasmine.clock().install();
});
it('should open snackbar if lastPocVerificationDate is older than one year', fakeAsync(() => {
const MOCK_TODAY = new Date('2024-07-15T10:00:00.000Z');
jasmine.clock().mockDate(MOCK_TODAY);
const twoYearsAgo = new Date(MOCK_TODAY);
twoYearsAgo.setFullYear(MOCK_TODAY.getFullYear() - 2);
mockRegistrarService.registrar.set({
lastPocVerificationDate: twoYearsAgo.toISOString(),
});
fixture.detectChanges();
TestBed.flushEffects();
expect(mockSnackBar.openFromComponent).toHaveBeenCalledWith(
PocReminderComponent,
{
horizontalPosition: 'center',
verticalPosition: 'top',
duration: 1000000000,
}
);
}));
it('should NOT open snackbar if lastPocVerificationDate is within last year', fakeAsync(() => {
const MOCK_TODAY = new Date('2024-07-15T10:00:00.000Z');
jasmine.clock().mockDate(MOCK_TODAY);
const sixMonthsAgo = new Date(MOCK_TODAY);
sixMonthsAgo.setMonth(MOCK_TODAY.getMonth() - 6);
mockRegistrarService.registrar.set({
lastPocVerificationDate: sixMonthsAgo.toISOString(),
});
fixture.detectChanges();
TestBed.flushEffects();
expect(mockSnackBar.openFromComponent).not.toHaveBeenCalled();
}));
});
});

View File

@@ -12,13 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { AfterViewInit, Component, effect, ViewChild } from '@angular/core';
import { MatSidenav } from '@angular/material/sidenav';
import { NavigationEnd, Router } from '@angular/router';
import { RegistrarService } from './registrar/registrar.service';
import { BreakPointObserverService } from './shared/services/breakPoint.service';
import { GlobalLoaderService } from './shared/services/globalLoader.service';
import { UserDataService } from './shared/services/userData.service';
import { MatSnackBar } from '@angular/material/snack-bar';
import { PocReminderComponent } from './shared/components/pocReminder/pocReminder.component';
@Component({
selector: 'app-root',
@@ -35,8 +37,28 @@ export class AppComponent implements AfterViewInit {
protected userDataService: UserDataService,
protected globalLoader: GlobalLoaderService,
protected breakpointObserver: BreakPointObserverService,
private router: Router
) {}
private router: Router,
private _snackBar: MatSnackBar
) {
effect(() => {
const registrar = this.registrarService.registrar();
const oneYearAgo = new Date();
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
oneYearAgo.setHours(0, 0, 0, 0);
if (
registrar &&
registrar.lastPocVerificationDate &&
new Date(registrar.lastPocVerificationDate) < oneYearAgo &&
this.userDataService?.userData()?.globalRole === 'NONE'
) {
this._snackBar.openFromComponent(PocReminderComponent, {
horizontalPosition: 'center',
verticalPosition: 'top',
duration: 1000000000,
});
}
});
}
ngAfterViewInit() {
this.router.events.subscribe((event) => {

View File

@@ -60,6 +60,7 @@ import { TldsComponent } from './tlds/tlds.component';
import { ForceFocusDirective } from './shared/directives/forceFocus.directive';
import RdapComponent from './settings/rdap/rdap.component';
import RdapEditComponent from './settings/rdap/rdapEdit.component';
import { PocReminderComponent } from './shared/components/pocReminder/pocReminder.component';
@NgModule({
declarations: [SelectedRegistrarWrapper],
@@ -86,6 +87,7 @@ export class SelectedRegistrarModule {}
RdapComponent,
RdapEditComponent,
ReasonDialogComponent,
PocReminderComponent,
RegistrarComponent,
RegistrarDetailsComponent,
RegistrarSelectorComponent,

View File

@@ -57,7 +57,7 @@ export class NavigationComponent {
}
ngOnDestroy() {
this.subscription.unsubscribe();
this.subscription && this.subscription.unsubscribe();
}
getElementId(node: RouteWithIcon) {

View File

@@ -71,6 +71,7 @@ export interface Registrar
registrarName: string;
registryLockAllowed?: boolean;
type?: string;
lastPocVerificationDate?: string;
}
@Injectable({

View File

@@ -16,11 +16,7 @@ import { Component, effect, ViewEncapsulation } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { take } from 'rxjs';
import { RegistrarService } from 'src/app/registrar/registrar.service';
import {
ContactService,
contactTypeToViewReadyContact,
ViewReadyContact,
} from './contact.service';
import { ContactService, ViewReadyContact } from './contact.service';
@Component({
selector: 'app-contact',

View File

@@ -24,7 +24,7 @@ export type contactType =
| 'LEGAL'
| 'MARKETING'
| 'TECH'
| 'RDAP';
| 'WHOIS';
type contactTypesToUserFriendlyTypes = { [type in contactType]: string };
@@ -35,7 +35,7 @@ export const contactTypeToTextMap: contactTypesToUserFriendlyTypes = {
LEGAL: 'Legal contact',
MARKETING: 'Marketing contact',
TECH: 'Technical contact',
RDAP: 'RDAP-Inquiry contact',
WHOIS: 'RDAP-Inquiry contact',
};
type UserFriendlyType = (typeof contactTypeToTextMap)[contactType];
@@ -59,7 +59,10 @@ export interface ViewReadyContact extends Contact {
export function contactTypeToViewReadyContact(c: Contact): ViewReadyContact {
return {
...c,
userFriendlyTypes: c.types?.map((cType) => contactTypeToTextMap[cType]),
userFriendlyTypes: (c.types || []).map(
(cType) => contactTypeToTextMap[cType]
),
types: c.types || [],
};
}
@@ -83,7 +86,7 @@ export class ContactService {
: contactTypeToViewReadyContact({
emailAddress: '',
name: '',
types: ['ADMIN'],
types: ['TECH'],
faxNumber: '',
phoneNumber: '',
registrarId: '',
@@ -98,19 +101,21 @@ export class ContactService {
);
}
saveContacts(contacts: ViewReadyContact[]): Observable<Contact[]> {
updateContact(contact: ViewReadyContact) {
return this.backend
.postContacts(this.registrarService.registrarId(), contacts)
.updateContact(this.registrarService.registrarId(), contact)
.pipe(switchMap((_) => this.fetchContacts()));
}
addContact(contact: ViewReadyContact) {
const newContacts = this.contacts().concat([contact]);
return this.saveContacts(newContacts);
return this.backend
.createContact(this.registrarService.registrarId(), contact)
.pipe(switchMap((_) => this.fetchContacts()));
}
deleteContact(contact: ViewReadyContact) {
const newContacts = this.contacts().filter((c) => c !== contact);
return this.saveContacts(newContacts);
return this.backend
.deleteContact(this.registrarService.registrarId(), contact)
.pipe(switchMap((_) => this.fetchContacts()));
}
}

View File

@@ -56,6 +56,7 @@
[required]="true"
[(ngModel)]="contactService.contactInEdit.emailAddress"
[ngModelOptions]="{ standalone: true }"
[disabled]="emailAddressIsDisabled()"
/>
</mat-form-field>
@@ -85,14 +86,18 @@
<mat-icon color="accent">error</mat-icon>Required to select at least one
</p>
<div class="">
<mat-checkbox
<ng-container
*ngFor="let contactType of contactTypeToTextMap | keyvalue"
[checked]="checkboxIsChecked(contactType.key)"
(change)="checkboxOnChange($event, contactType.key)"
[disabled]="checkboxIsDisabled(contactType.key)"
>
{{ contactType.value }}
</mat-checkbox>
<mat-checkbox
*ngIf="shouldDisplayCheckbox(contactType.key)"
[checked]="checkboxIsChecked(contactType.key)"
(change)="checkboxOnChange($event, contactType.key)"
[disabled]="checkboxIsDisabled(contactType.key)"
>
{{ contactType.value }}
</mat-checkbox>
</ng-container>
</div>
</section>

View File

@@ -69,9 +69,13 @@ export class ContactDetailsComponent {
save(e: SubmitEvent) {
e.preventDefault();
if ((this.contactService.contactInEdit.types || []).length === 0) {
this._snackBar.open('Required to select contact type');
return;
}
const request = this.contactService.isContactNewView
? this.contactService.addContact(this.contactService.contactInEdit)
: this.contactService.saveContacts(this.contactService.contacts());
: this.contactService.updateContact(this.contactService.contactInEdit);
request.subscribe({
complete: () => {
this.goBack();
@@ -82,6 +86,10 @@ export class ContactDetailsComponent {
});
}
shouldDisplayCheckbox(type: string) {
return type !== 'ADMIN' || this.checkboxIsChecked(type);
}
checkboxIsChecked(type: string) {
return this.contactService.contactInEdit.types.includes(
type as contactType
@@ -89,6 +97,9 @@ export class ContactDetailsComponent {
}
checkboxIsDisabled(type: string) {
if (type === 'ADMIN') {
return true;
}
return (
this.contactService.contactInEdit.types.length === 1 &&
this.contactService.contactInEdit.types[0] === (type as contactType)
@@ -105,4 +116,8 @@ export class ContactDetailsComponent {
);
}
}
emailAddressIsDisabled() {
return this.contactService.contactInEdit.types.includes('ADMIN');
}
}

View File

@@ -0,0 +1,14 @@
<div class="console-app__pocReminder">
<p class="">
Please take a moment to complete annual review of
<a routerLink="/settings">contacts</a>.
</p>
<span matSnackBarActions>
<button mat-button matSnackBarAction (click)="confirmReviewed()">
Confirm reviewed
</button>
<button mat-button matSnackBarAction (click)="snackBarRef.dismiss()">
Close
</button>
</span>
</div>

View File

@@ -0,0 +1,5 @@
.console-app__pocReminder {
a {
color: white !important;
}
}

View File

@@ -0,0 +1,53 @@
// 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.
import { Component } from '@angular/core';
import { MatSnackBar, MatSnackBarRef } from '@angular/material/snack-bar';
import { RegistrarService } from '../../../registrar/registrar.service';
import { HttpErrorResponse } from '@angular/common/http';
@Component({
selector: 'app-poc-reminder',
templateUrl: './pocReminder.component.html',
styleUrls: ['./pocReminder.component.scss'],
standalone: false,
})
export class PocReminderComponent {
constructor(
public snackBarRef: MatSnackBarRef<PocReminderComponent>,
private registrarService: RegistrarService,
private _snackBar: MatSnackBar
) {}
confirmReviewed() {
if (this.registrarService.registrar()) {
const todayMidnight = new Date();
todayMidnight.setHours(0, 0, 0, 0);
this.registrarService
// @ts-ignore - if check above won't allow empty object to be submitted
.updateRegistrar({
...this.registrarService.registrar(),
lastPocVerificationDate: todayMidnight.toISOString(),
})
.subscribe({
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error || err.message);
},
next: () => {
this.snackBarRef.dismiss();
},
});
}
}
}

View File

@@ -70,13 +70,26 @@ export class BackendService {
.pipe(catchError((err) => this.errorCatcher<Contact[]>(err)));
}
postContacts(
registrarId: string,
contacts: Contact[]
): Observable<Contact[]> {
return this.http.post<Contact[]>(
updateContact(registrarId: string, contact: Contact): Observable<Contact> {
return this.http.put<Contact>(
`/console-api/settings/contacts?registrarId=${registrarId}`,
contacts
contact
);
}
createContact(registrarId: string, contact: Contact): Observable<Contact> {
return this.http.post<Contact>(
`/console-api/settings/contacts?registrarId=${registrarId}`,
contact
);
}
deleteContact(registrarId: string, contact: Contact): Observable<Contact> {
return this.http.delete<Contact>(
`/console-api/settings/contacts?registrarId=${registrarId}`,
{
body: JSON.stringify(contact),
}
);
}

View File

@@ -61,6 +61,7 @@ def fragileTestPatterns = [
// Currently changes a global configuration parameter that for some reason
// results in timestamp inversions for other tests. TODO(mmuller): fix.
"google/registry/flows/host/HostInfoFlowTest.*",
"google/registry/beam/common/RegistryPipelineWorkerInitializerTest.*",
] + dockerIncompatibleTestPatterns
sourceSets {

View File

@@ -33,7 +33,7 @@ import google.registry.flows.certs.CertificateChecker;
import google.registry.groups.GmailClient;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.registrar.RegistrarPocBase.Type;
import google.registry.model.registrar.RegistrarPoc.Type;
import google.registry.request.Action;
import google.registry.request.Action.GaeService;
import google.registry.request.Response;

View File

@@ -172,7 +172,7 @@ public record BillingEvent(
.minusDays(1)
.toString(),
billingId(),
registrarId(),
"",
String.format("%s | TLD: %s | TERM: %d-year", action(), tld(), years()),
amount(),
currency(),

View File

@@ -36,8 +36,6 @@ import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.flows.custom.CustomLogicFactoryModule;
import google.registry.flows.custom.CustomLogicModule;
import google.registry.flows.domain.DomainPricingLogic;
import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForCurrencyException;
import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException;
import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingBase.Flag;
import google.registry.model.billing.BillingCancellation;
@@ -389,38 +387,30 @@ public class ExpandBillingRecurrencesPipeline implements Serializable {
// It is OK to always create a OneTime, even though the domain might be deleted or transferred
// later during autorenew grace period, as a cancellation will always be written out in those
// instances.
BillingEvent billingEvent = null;
try {
billingEvent =
new BillingEvent.Builder()
.setBillingTime(billingTime)
.setRegistrarId(billingRecurrence.getRegistrarId())
// Determine the cost for a one-year renewal.
.setCost(
domainPricingLogic
.getRenewPrice(
tld,
billingRecurrence.getTargetId(),
eventTime,
1,
billingRecurrence,
Optional.empty())
.getRenewCost())
.setEventTime(eventTime)
.setFlags(union(billingRecurrence.getFlags(), Flag.SYNTHETIC))
.setDomainHistory(historyEntry)
.setPeriodYears(1)
.setReason(billingRecurrence.getReason())
.setSyntheticCreationTime(endTime)
.setCancellationMatchingBillingEvent(billingRecurrence)
.setTargetId(billingRecurrence.getTargetId())
.build();
} catch (AllocationTokenInvalidForCurrencyException
| AllocationTokenInvalidForPremiumNameException e) {
// This should not be reached since we are not using an allocation token
return;
}
results.add(billingEvent);
results.add(
new BillingEvent.Builder()
.setBillingTime(billingTime)
.setRegistrarId(billingRecurrence.getRegistrarId())
// Determine the cost for a one-year renewal.
.setCost(
domainPricingLogic
.getRenewPrice(
tld,
billingRecurrence.getTargetId(),
eventTime,
1,
billingRecurrence,
Optional.empty())
.getRenewCost())
.setEventTime(eventTime)
.setFlags(union(billingRecurrence.getFlags(), Flag.SYNTHETIC))
.setDomainHistory(historyEntry)
.setPeriodYears(1)
.setReason(billingRecurrence.getReason())
.setSyntheticCreationTime(endTime)
.setCancellationMatchingBillingEvent(billingRecurrence)
.setTargetId(billingRecurrence.getTargetId())
.build());
}
results.add(
billingRecurrence

View File

@@ -40,6 +40,8 @@ public class RegistryPipelineWorkerInitializer implements JvmInitializer {
@Override
public void beforeProcessing(PipelineOptions options) {
// TODO(b/416299900): remove next line after GAE is removed.
System.setProperty("google.registry.jetty", "true");
RegistryPipelineOptions registryOptions = options.as(RegistryPipelineOptions.class);
RegistryEnvironment environment = registryOptions.getRegistryEnvironment();
if (environment == null || environment.equals(RegistryEnvironment.UNITTEST)) {

View File

@@ -58,7 +58,7 @@ import google.registry.model.host.Host;
import google.registry.model.host.HostHistory;
import google.registry.model.rde.RdeMode;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarBase.Type;
import google.registry.model.registrar.Registrar.Type;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;

View File

@@ -144,7 +144,6 @@ public abstract class CredentialModule {
Duration tokenRefreshDelay,
Clock clock) {
GoogleCredentials signer = credentialsBundle.getGoogleCredentials();
checkArgument(
signer instanceof ServiceAccountSigner,
"Expecting a ServiceAccountSigner, found %s.",

View File

@@ -50,7 +50,6 @@ import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.registrar.RegistrarPocBase;
import google.registry.model.tld.Tld;
import google.registry.request.Action;
import google.registry.request.Action.GaeService;
@@ -296,7 +295,7 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
ImmutableList<InternetAddress> recipients =
registrar.get().getContacts().stream()
.filter(c -> c.getTypes().contains(RegistrarPocBase.Type.ADMIN))
.filter(c -> c.getTypes().contains(RegistrarPoc.Type.ADMIN))
.map(RegistrarPoc::getEmailAddress)
.map(PublishDnsUpdatesAction::emailToInternetAddress)
.collect(toImmutableList());

View File

@@ -0,0 +1,29 @@
// 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.dns.writer;
import dagger.Module;
import google.registry.dns.writer.clouddns.CloudDnsWriterModule;
import google.registry.dns.writer.dnsupdate.DnsUpdateWriterModule;
/**
* Groups all {@link DnsWriter} implementations to be installed.
*
* <p>To cherry-pick the DNS writers to install, overwrite this file with your private version in
* the release process.
*/
@Module(
includes = {CloudDnsWriterModule.class, DnsUpdateWriterModule.class, VoidDnsWriterModule.class})
public class DnsWritersModule {}

View File

@@ -33,7 +33,6 @@ import google.registry.groups.GroupsConnection;
import google.registry.groups.GroupsConnection.Role;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.registrar.RegistrarPocBase;
import google.registry.request.Action;
import google.registry.request.Action.GaeService;
import google.registry.request.Response;
@@ -101,7 +100,7 @@ public final class SyncGroupMembersAction implements Runnable {
* Returns the Google Groups email address for the given registrar ID and RegistrarContact.Type.
*/
public static String getGroupEmailAddressForContactType(
String registrarId, RegistrarPocBase.Type type, String gSuiteDomainName) {
String registrarId, RegistrarPoc.Type type, String gSuiteDomainName) {
// Take the registrar's ID, make it lowercase, and remove all characters that aren't
// alphanumeric, hyphens, or underscores.
return String.format(
@@ -176,7 +175,7 @@ public final class SyncGroupMembersAction implements Runnable {
Set<RegistrarPoc> registrarPocs = registrar.getContacts();
long totalAdded = 0;
long totalRemoved = 0;
for (final RegistrarPocBase.Type type : RegistrarPocBase.Type.values()) {
for (final RegistrarPoc.Type type : RegistrarPoc.Type.values()) {
groupKey =
getGroupEmailAddressForContactType(registrar.getRegistrarId(), type, gSuiteDomainName);
Set<String> currentMembers = groupsConnection.getMembersOfGroup(groupKey);

View File

@@ -17,13 +17,13 @@ package google.registry.export.sheet;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.model.common.Cursor.CursorType.SYNC_REGISTRAR_SHEET;
import static google.registry.model.registrar.RegistrarPocBase.Type.ABUSE;
import static google.registry.model.registrar.RegistrarPocBase.Type.ADMIN;
import static google.registry.model.registrar.RegistrarPocBase.Type.BILLING;
import static google.registry.model.registrar.RegistrarPocBase.Type.LEGAL;
import static google.registry.model.registrar.RegistrarPocBase.Type.MARKETING;
import static google.registry.model.registrar.RegistrarPocBase.Type.TECH;
import static google.registry.model.registrar.RegistrarPocBase.Type.WHOIS;
import static google.registry.model.registrar.RegistrarPoc.Type.ABUSE;
import static google.registry.model.registrar.RegistrarPoc.Type.ADMIN;
import static google.registry.model.registrar.RegistrarPoc.Type.BILLING;
import static google.registry.model.registrar.RegistrarPoc.Type.LEGAL;
import static google.registry.model.registrar.RegistrarPoc.Type.MARKETING;
import static google.registry.model.registrar.RegistrarPoc.Type.TECH;
import static google.registry.model.registrar.RegistrarPoc.Type.WHOIS;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
@@ -36,7 +36,6 @@ import google.registry.model.common.Cursor;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarAddress;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.registrar.RegistrarPocBase;
import google.registry.util.Clock;
import google.registry.util.DateTimeUtils;
import jakarta.inject.Inject;
@@ -174,7 +173,7 @@ class SyncRegistrarsSheet {
return result.toString();
}
private static Predicate<RegistrarPoc> byType(final RegistrarPocBase.Type type) {
private static Predicate<RegistrarPoc> byType(final RegistrarPoc.Type type) {
return contact -> contact.getTypes().contains(type);
}

View File

@@ -185,7 +185,8 @@ public class CheckApiAction implements Runnable {
}
private boolean checkExists(String domainString, DateTime now) {
return !ForeignKeyUtils.loadCached(Domain.class, ImmutableList.of(domainString), now).isEmpty();
return !ForeignKeyUtils.loadByCache(Domain.class, ImmutableList.of(domainString), now)
.isEmpty();
}
private Optional<String> checkReserved(InternetDomainName domainName) {

View File

@@ -14,7 +14,6 @@
package google.registry.flows.domain;
import static com.google.common.base.Strings.emptyToNull;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
@@ -42,6 +41,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.InternetDomainName;
import google.registry.config.RegistryConfig;
import google.registry.config.RegistryConfig.Config;
@@ -55,14 +55,7 @@ import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.custom.DomainCheckFlowCustomLogic;
import google.registry.flows.custom.DomainCheckFlowCustomLogic.BeforeResponseParameters;
import google.registry.flows.custom.DomainCheckFlowCustomLogic.BeforeResponseReturnData;
import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException;
import google.registry.flows.domain.token.AllocationTokenDomainCheckResults;
import google.registry.flows.domain.token.AllocationTokenFlowUtils;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForCommandException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException;
import google.registry.model.EppResource;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.billing.BillingRecurrence;
@@ -87,7 +80,6 @@ import google.registry.model.tld.Tld;
import google.registry.model.tld.Tld.TldState;
import google.registry.model.tld.label.ReservationType;
import google.registry.persistence.VKey;
import google.registry.pricing.PricingEngineProxy;
import google.registry.util.Clock;
import jakarta.inject.Inject;
import java.util.Collection;
@@ -131,6 +123,8 @@ import org.joda.time.DateTime;
@ReportingSpec(ActivityReportField.DOMAIN_CHECK)
public final class DomainCheckFlow implements TransactionalFlow {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final String STANDARD_FEE_RESPONSE_CLASS = "STANDARD";
private static final String STANDARD_PROMOTION_FEE_RESPONSE_CLASS = "STANDARD PROMOTION";
@@ -146,7 +140,6 @@ public final class DomainCheckFlow implements TransactionalFlow {
@Inject @Superuser boolean isSuperuser;
@Inject Clock clock;
@Inject EppResponse.Builder responseBuilder;
@Inject AllocationTokenFlowUtils allocationTokenFlowUtils;
@Inject DomainCheckFlowCustomLogic flowCustomLogic;
@Inject DomainPricingLogic pricingLogic;
@@ -195,36 +188,15 @@ public final class DomainCheckFlow implements TransactionalFlow {
existingDomains.size() == parsedDomains.size()
? ImmutableSet.of()
: getBsaBlockedDomains(parsedDomains.values(), now);
Optional<AllocationTokenExtension> allocationTokenExtension =
eppInput.getSingleExtension(AllocationTokenExtension.class);
Optional<AllocationTokenDomainCheckResults> tokenDomainCheckResults =
allocationTokenExtension.map(
tokenExtension ->
allocationTokenFlowUtils.checkDomainsWithToken(
ImmutableList.copyOf(parsedDomains.values()),
tokenExtension.getAllocationToken(),
registrarId,
now));
ImmutableList.Builder<DomainCheck> checksBuilder = new ImmutableList.Builder<>();
ImmutableSet.Builder<String> availableDomains = new ImmutableSet.Builder<>();
ImmutableMap<String, TldState> tldStates =
Maps.toMap(seenTlds, tld -> Tld.get(tld).getTldState(now));
ImmutableMap<InternetDomainName, String> domainCheckResults =
tokenDomainCheckResults
.map(AllocationTokenDomainCheckResults::domainCheckResults)
.orElse(ImmutableMap.of());
Optional<AllocationToken> allocationToken =
tokenDomainCheckResults.flatMap(AllocationTokenDomainCheckResults::token);
for (String domainName : domainNames) {
Optional<String> message =
getMessageForCheck(
parsedDomains.get(domainName),
existingDomains,
bsaBlockedDomainNames,
domainCheckResults,
tldStates,
allocationToken);
domainName, existingDomains, bsaBlockedDomainNames, tldStates, parsedDomains, now);
boolean isAvailable = message.isEmpty();
checksBuilder.add(DomainCheck.create(isAvailable, domainName, message.orElse(null)));
if (isAvailable) {
@@ -237,11 +209,7 @@ public final class DomainCheckFlow implements TransactionalFlow {
.setDomainChecks(checksBuilder.build())
.setResponseExtensions(
getResponseExtensions(
parsedDomains,
existingDomains,
availableDomains.build(),
now,
allocationToken))
parsedDomains, existingDomains, availableDomains.build(), now))
.setAsOfDate(now)
.build());
return responseBuilder
@@ -251,10 +219,39 @@ public final class DomainCheckFlow implements TransactionalFlow {
}
private Optional<String> getMessageForCheck(
String domainName,
ImmutableMap<String, VKey<Domain>> existingDomains,
ImmutableSet<InternetDomainName> bsaBlockedDomainNames,
ImmutableMap<String, TldState> tldStates,
ImmutableMap<String, InternetDomainName> parsedDomains,
DateTime now) {
InternetDomainName idn = parsedDomains.get(domainName);
Optional<AllocationToken> token;
try {
// Which token we use may vary based on the domain -- a provided token may be invalid for
// some domains, or there may be DEFAULT PROMO tokens only applicable on some domains
token =
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
registrarId,
now,
eppInput.getSingleExtension(AllocationTokenExtension.class),
Tld.get(idn.parent().toString()),
domainName,
FeeQueryCommandExtensionItem.CommandName.CREATE);
} catch (AllocationTokenFlowUtils.NonexistentAllocationTokenException
| AllocationTokenFlowUtils.AllocationTokenInvalidException e) {
// The provided token was catastrophically invalid in some way
logger.atInfo().withCause(e).log("Cannot load/use allocation token.");
return Optional.of(e.getMessage());
}
return getMessageForCheckWithToken(
idn, existingDomains, bsaBlockedDomainNames, tldStates, token);
}
private Optional<String> getMessageForCheckWithToken(
InternetDomainName domainName,
ImmutableMap<String, VKey<Domain>> existingDomains,
ImmutableSet<InternetDomainName> bsaBlockedDomains,
ImmutableMap<InternetDomainName, String> tokenCheckResults,
ImmutableMap<String, TldState> tldStates,
Optional<AllocationToken> allocationToken) {
if (existingDomains.containsKey(domainName.toString())) {
@@ -271,11 +268,6 @@ public final class DomainCheckFlow implements TransactionalFlow {
}
}
}
Optional<String> tokenResult =
Optional.ofNullable(emptyToNull(tokenCheckResults.get(domainName)));
if (tokenResult.isPresent()) {
return tokenResult;
}
if (isRegisterBsaCreate(domainName, allocationToken)
|| !bsaBlockedDomains.contains(domainName)) {
return Optional.empty();
@@ -290,8 +282,7 @@ public final class DomainCheckFlow implements TransactionalFlow {
ImmutableMap<String, InternetDomainName> domainNames,
ImmutableMap<String, VKey<Domain>> existingDomains,
ImmutableSet<String> availableDomains,
DateTime now,
Optional<AllocationToken> allocationToken)
DateTime now)
throws EppException {
Optional<FeeCheckCommandExtension> feeCheckOpt =
eppInput.getSingleExtension(FeeCheckCommandExtension.class);
@@ -309,84 +300,24 @@ public final class DomainCheckFlow implements TransactionalFlow {
RegistryConfig.getTieredPricingPromotionRegistrarIds().contains(registrarId);
for (FeeCheckCommandExtensionItem feeCheckItem : feeCheck.getItems()) {
for (String domainName : getDomainNamesToCheckForFee(feeCheckItem, domainNames.keySet())) {
Optional<AllocationToken> defaultToken =
DomainFlowUtils.checkForDefaultToken(
Tld.get(InternetDomainName.from(domainName).parent().toString()),
domainName,
feeCheckItem.getCommandName(),
registrarId,
now);
FeeCheckResponseExtensionItem.Builder<?> builder = feeCheckItem.createResponseBuilder();
Optional<Domain> domain = Optional.ofNullable(domainObjs.get(domainName));
Tld tld = Tld.get(domainNames.get(domainName).parent().toString());
Optional<AllocationToken> token;
try {
if (allocationToken.isPresent()) {
AllocationTokenFlowUtils.validateToken(
InternetDomainName.from(domainName),
allocationToken.get(),
feeCheckItem.getCommandName(),
registrarId,
PricingEngineProxy.isDomainPremium(domainName, now),
now);
}
handleFeeRequest(
feeCheckItem,
builder,
domainNames.get(domainName),
domain,
feeCheck.getCurrency(),
now,
pricingLogic,
allocationToken.isPresent() ? allocationToken : defaultToken,
availableDomains.contains(domainName),
recurrences.getOrDefault(domainName, null));
// In the case of a registrar that is running a tiered pricing promotion, we issue two
// responses for the CREATE fee check command: one (the default response) with the
// non-promotional price, and one (an extra STANDARD PROMO response) with the actual
// promotional price.
if (defaultToken.isPresent()
&& shouldUseTieredPricingPromotion
&& feeCheckItem
.getCommandName()
.equals(FeeQueryCommandExtensionItem.CommandName.CREATE)) {
// First, set the promotional (real) price under the STANDARD PROMO class
builder
.setClass(STANDARD_PROMOTION_FEE_RESPONSE_CLASS)
.setCommand(
FeeQueryCommandExtensionItem.CommandName.CUSTOM,
feeCheckItem.getPhase(),
feeCheckItem.getSubphase());
// Next, get the non-promotional price and set it as the standard response to the CREATE
// fee check command
FeeCheckResponseExtensionItem.Builder<?> nonPromotionalBuilder =
feeCheckItem.createResponseBuilder();
handleFeeRequest(
feeCheckItem,
nonPromotionalBuilder,
domainNames.get(domainName),
domain,
feeCheck.getCurrency(),
now,
pricingLogic,
allocationToken,
availableDomains.contains(domainName),
recurrences.getOrDefault(domainName, null));
responseItems.add(
nonPromotionalBuilder
.setClass(STANDARD_FEE_RESPONSE_CLASS)
.setDomainNameIfSupported(domainName)
.build());
}
responseItems.add(builder.setDomainNameIfSupported(domainName).build());
} catch (AllocationTokenInvalidForPremiumNameException
| AllocationTokenNotValidForCommandException
| AllocationTokenNotValidForDomainException
| AllocationTokenNotValidForRegistrarException
| AllocationTokenNotValidForTldException
| AllocationTokenNotInPromotionException e) {
// Allocation token is either not an active token or it is not valid for the EPP command,
// registrar, domain, or TLD.
Tld tld = Tld.get(InternetDomainName.from(domainName).parent().toString());
// The precise token to use for this fee request may vary based on the domain or even the
// precise command issued (some tokens may be valid only for certain actions)
token =
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
registrarId,
now,
eppInput.getSingleExtension(AllocationTokenExtension.class),
tld,
domainName,
feeCheckItem.getCommandName());
} catch (AllocationTokenFlowUtils.NonexistentAllocationTokenException
| AllocationTokenFlowUtils.AllocationTokenInvalidException e) {
// The provided token was catastrophically invalid in some way
responseItems.add(
builder
.setDomainNameIfSupported(domainName)
@@ -398,7 +329,60 @@ public final class DomainCheckFlow implements TransactionalFlow {
.setCurrencyIfSupported(tld.getCurrency())
.setClass("token-not-supported")
.build());
continue;
}
handleFeeRequest(
feeCheckItem,
builder,
domainNames.get(domainName),
domain,
feeCheck.getCurrency(),
now,
pricingLogic,
token,
availableDomains.contains(domainName),
recurrences.getOrDefault(domainName, null));
// In the case of a registrar that is running a tiered pricing promotion, we issue two
// responses for the CREATE fee check command: one (the default response) with the
// non-promotional price, and one (an extra STANDARD PROMO response) with the actual
// promotional price.
if (token
.map(t -> t.getTokenType().equals(AllocationToken.TokenType.DEFAULT_PROMO))
.orElse(false)
&& shouldUseTieredPricingPromotion
&& feeCheckItem
.getCommandName()
.equals(FeeQueryCommandExtensionItem.CommandName.CREATE)) {
// First, set the promotional (real) price under the STANDARD PROMO class
builder
.setClass(STANDARD_PROMOTION_FEE_RESPONSE_CLASS)
.setCommand(
FeeQueryCommandExtensionItem.CommandName.CUSTOM,
feeCheckItem.getPhase(),
feeCheckItem.getSubphase());
// Next, get the non-promotional price and set it as the standard response to the CREATE
// fee check command
FeeCheckResponseExtensionItem.Builder<?> nonPromotionalBuilder =
feeCheckItem.createResponseBuilder();
handleFeeRequest(
feeCheckItem,
nonPromotionalBuilder,
domainNames.get(domainName),
domain,
feeCheck.getCurrency(),
now,
pricingLogic,
Optional.empty(),
availableDomains.contains(domainName),
recurrences.getOrDefault(domainName, null));
responseItems.add(
nonPromotionalBuilder
.setClass(STANDARD_FEE_RESPONSE_CLASS)
.setDomainNameIfSupported(domainName)
.build());
}
responseItems.add(builder.setDomainNameIfSupported(domainName).build());
}
}
return ImmutableList.of(feeCheck.createResponse(responseItems.build()));
@@ -448,7 +432,7 @@ public final class DomainCheckFlow implements TransactionalFlow {
.filter(existingDomains::containsKey)
.collect(toImmutableMap(d -> d, existingDomains::get));
ImmutableMap<VKey<? extends EppResource>, EppResource> loadedDomains =
EppResource.loadCached(ImmutableList.copyOf(existingDomainsToLoad.values()));
EppResource.loadByCacheIfEnabled(ImmutableList.copyOf(existingDomainsToLoad.values()));
return ImmutableMap.copyOf(
Maps.transformEntries(existingDomainsToLoad, (k, v) -> (Domain) loadedDomains.get(v)));
}

View File

@@ -133,11 +133,8 @@ import org.joda.time.Duration;
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException}
* @error {@link AllocationTokenFlowUtils.NonexistentAllocationTokenException}
* @error {@link google.registry.flows.exceptions.OnlyToolCanPassMetadataException}
* @error {@link ResourceAlreadyExistsForThisClientException}
* @error {@link ResourceCreateContentionException}
@@ -205,7 +202,6 @@ import org.joda.time.Duration;
* @error {@link DomainFlowUtils.UnexpectedClaimsNoticeException}
* @error {@link DomainFlowUtils.UnsupportedFeeAttributeException}
* @error {@link DomainFlowUtils.UnsupportedMarkTypeException}
* @error {@link DomainPricingLogic.AllocationTokenInvalidForPremiumNameException}
*/
@ReportingSpec(ActivityReportField.DOMAIN_CREATE)
public final class DomainCreateFlow implements MutatingFlow {
@@ -221,7 +217,6 @@ public final class DomainCreateFlow implements MutatingFlow {
@Inject @Superuser boolean isSuperuser;
@Inject DomainHistory.Builder historyBuilder;
@Inject EppResponse.Builder responseBuilder;
@Inject AllocationTokenFlowUtils allocationTokenFlowUtils;
@Inject DomainCreateFlowCustomLogic flowCustomLogic;
@Inject DomainFlowTmchUtils tmchUtils;
@Inject DomainPricingLogic pricingLogic;
@@ -264,25 +259,20 @@ public final class DomainCreateFlow implements MutatingFlow {
}
boolean isSunriseCreate = hasSignedMarks && (tldState == START_DATE_SUNRISE);
Optional<AllocationToken> allocationToken =
allocationTokenFlowUtils.verifyAllocationTokenCreateIfPresent(
command,
tld,
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
registrarId,
now,
eppInput.getSingleExtension(AllocationTokenExtension.class));
boolean defaultTokenUsed = false;
if (allocationToken.isEmpty()) {
allocationToken =
DomainFlowUtils.checkForDefaultToken(
tld, command.getDomainName(), CommandName.CREATE, registrarId, now);
if (allocationToken.isPresent()) {
defaultTokenUsed = true;
}
}
eppInput.getSingleExtension(AllocationTokenExtension.class),
tld,
command.getDomainName(),
CommandName.CREATE);
boolean defaultTokenUsed =
allocationToken.map(t -> t.getTokenType().equals(TokenType.DEFAULT_PROMO)).orElse(false);
boolean isAnchorTenant =
isAnchorTenant(
domainName, allocationToken, eppInput.getSingleExtension(MetadataExtension.class));
verifyAnchorTenantValidPeriod(isAnchorTenant, years);
// Superusers can create reserved domains, force creations on domains that require a claims
// notice without specifying a claims key, ignore the registry phase, and override blocks on
// registering premium domains.
@@ -416,7 +406,7 @@ public final class DomainCreateFlow implements MutatingFlow {
entitiesToSave.add(domain, domainHistory);
if (allocationToken.isPresent() && allocationToken.get().getTokenType().isOneTimeUse()) {
entitiesToSave.add(
allocationTokenFlowUtils.redeemToken(
AllocationTokenFlowUtils.redeemToken(
allocationToken.get(), domainHistory.getHistoryEntryId()));
}
if (domain.shouldPublishToDns()) {

View File

@@ -42,7 +42,6 @@ import static google.registry.model.tld.label.ReservationType.RESERVED_FOR_ANCHO
import static google.registry.model.tld.label.ReservationType.RESERVED_FOR_SPECIFIC_USE;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.pricing.PricingEngineProxy.isDomainPremium;
import static google.registry.util.CollectionUtils.isNullOrEmpty;
import static google.registry.util.CollectionUtils.nullToEmpty;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.DateTimeUtils.isAtOrAfter;
@@ -67,7 +66,6 @@ import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import com.google.common.net.InternetDomainName;
import google.registry.flows.EppException;
import google.registry.flows.EppException.AssociationProhibitsOperationException;
import google.registry.flows.EppException.AuthorizationErrorException;
import google.registry.flows.EppException.CommandUseErrorException;
import google.registry.flows.EppException.ObjectDoesNotExistException;
@@ -77,8 +75,6 @@ import google.registry.flows.EppException.ParameterValueSyntaxErrorException;
import google.registry.flows.EppException.RequiredParameterMissingException;
import google.registry.flows.EppException.StatusProhibitsOperationException;
import google.registry.flows.EppException.UnimplementedOptionException;
import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils;
import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException;
import google.registry.model.EppResource;
import google.registry.model.billing.BillingBase.Flag;
@@ -101,7 +97,6 @@ import google.registry.model.domain.fee.BaseFee.FeeType;
import google.registry.model.domain.fee.Credit;
import google.registry.model.domain.fee.Fee;
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem;
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName;
import google.registry.model.domain.fee.FeeQueryResponseExtensionItem;
import google.registry.model.domain.fee.FeeTransformCommandExtension;
import google.registry.model.domain.fee.FeeTransformResponseExtension;
@@ -124,7 +119,7 @@ import google.registry.model.eppoutput.EppResponse.ResponseExtension;
import google.registry.model.host.Host;
import google.registry.model.poll.PollMessage.Autorenew;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarBase.State;
import google.registry.model.registrar.Registrar.State;
import google.registry.model.reporting.DomainTransactionRecord;
import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField;
import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
@@ -422,7 +417,7 @@ public class DomainFlowUtils {
contacts.stream().map(DesignatedContact::getContactKey).forEach(keysToLoad::add);
registrant.ifPresent(keysToLoad::add);
keysToLoad.addAll(nameservers);
verifyNotInPendingDelete(EppResource.loadCached(keysToLoad.build()).values());
verifyNotInPendingDelete(EppResource.loadByCacheIfEnabled(keysToLoad.build()).values());
}
private static void verifyNotInPendingDelete(Iterable<EppResource> resources)
@@ -1233,52 +1228,6 @@ public class DomainFlowUtils {
.getResultList();
}
/**
* Checks if there is a valid default token to be used for a domain create command.
*
* <p>If there is more than one valid default token for the registration, only the first valid
* token found on the TLD's default token list will be returned.
*/
public static Optional<AllocationToken> checkForDefaultToken(
Tld tld, String domainName, CommandName commandName, String registrarId, DateTime now)
throws EppException {
if (isNullOrEmpty(tld.getDefaultPromoTokens())) {
return Optional.empty();
}
Map<VKey<AllocationToken>, Optional<AllocationToken>> tokens =
AllocationToken.getAll(tld.getDefaultPromoTokens());
ImmutableList<Optional<AllocationToken>> tokenList =
tld.getDefaultPromoTokens().stream()
.map(tokens::get)
.filter(Optional::isPresent)
.collect(toImmutableList());
checkState(
!isNullOrEmpty(tokenList),
"Failure while loading default TLD promotions from the database");
// Check if any of the tokens are valid for this domain registration
for (Optional<AllocationToken> token : tokenList) {
try {
AllocationTokenFlowUtils.validateToken(
InternetDomainName.from(domainName),
token.get(),
commandName,
registrarId,
isDomainPremium(domainName, now),
now);
} catch (AssociationProhibitsOperationException
| StatusProhibitsOperationException
| AllocationTokenInvalidForPremiumNameException e) {
// Allocation token was not valid for this registration, continue to check the next token in
// the list
continue;
}
// Only use the first valid token in the list
return token;
}
// No valid default token found
return Optional.empty();
}
/** Resource linked to this domain does not exist. */
static class LinkedResourcesDoNotExistException extends ObjectDoesNotExistException {
public LinkedResourcesDoNotExistException(Class<?> type, ImmutableSet<String> resourceIds) {

View File

@@ -31,6 +31,7 @@ import google.registry.flows.ExtensionManager;
import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.MutatingFlow;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.custom.DomainInfoFlowCustomLogic;
@@ -53,6 +54,8 @@ import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.eppoutput.EppResponse.ResponseExtension;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.persistence.IsolationLevel;
import google.registry.persistence.PersistenceModule;
import google.registry.util.Clock;
import jakarta.inject.Inject;
import java.util.Optional;
@@ -62,8 +65,12 @@ import org.joda.time.DateTime;
* An EPP flow that returns information about a domain.
*
* <p>The registrar that owns the domain, and any registrar presenting a valid authInfo for the
* domain, will get a rich result with all of the domain's fields. All other requests will be
* answered with a minimal result containing only basic information about the domain.
* domain, will get a rich result with all the domain's fields. All other requests will be answered
* with a minimal result containing only basic information about the domain.
*
* <p>This implements {@link MutatingFlow} instead of {@link TransactionalFlow} as a workaround so
* that the common workflow of "create domain, then immediately get domain info" does not run into
* replication lag issues where the info command claims the domain does not exist.
*
* @error {@link google.registry.flows.FlowUtils.NotLoggedInException}
* @error {@link google.registry.flows.FlowUtils.UnknownCurrencyEppException}
@@ -76,7 +83,8 @@ import org.joda.time.DateTime;
* @error {@link DomainFlowUtils.TransfersAreAlwaysForOneYearException}
*/
@ReportingSpec(ActivityReportField.DOMAIN_INFO)
public final class DomainInfoFlow implements TransactionalFlow {
@IsolationLevel(PersistenceModule.TransactionIsolationLevel.TRANSACTION_REPEATABLE_READ)
public final class DomainInfoFlow implements MutatingFlow {
@Inject ExtensionManager extensionManager;
@Inject ResourceCommand resourceCommand;

View File

@@ -16,14 +16,13 @@ package google.registry.flows.domain;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.flows.domain.DomainFlowUtils.zeroInCurrency;
import static google.registry.flows.domain.token.AllocationTokenFlowUtils.validateTokenForPossiblePremiumName;
import static google.registry.flows.domain.token.AllocationTokenFlowUtils.discountTokenInvalidForPremiumName;
import static google.registry.pricing.PricingEngineProxy.getPricesForDomainName;
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
import com.google.common.net.InternetDomainName;
import google.registry.config.RegistryConfig;
import google.registry.flows.EppException;
import google.registry.flows.EppException.CommandUseErrorException;
import google.registry.flows.custom.DomainPricingCustomLogic;
import google.registry.flows.custom.DomainPricingCustomLogic.CreatePriceParameters;
import google.registry.flows.custom.DomainPricingCustomLogic.RenewPriceParameters;
@@ -129,9 +128,7 @@ public final class DomainPricingLogic {
DateTime dateTime,
int years,
@Nullable BillingRecurrence billingRecurrence,
Optional<AllocationToken> allocationToken)
throws AllocationTokenInvalidForCurrencyException,
AllocationTokenInvalidForPremiumNameException {
Optional<AllocationToken> allocationToken) {
checkArgument(years > 0, "Number of years must be positive");
Money renewCost;
DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime);
@@ -260,8 +257,7 @@ public final class DomainPricingLogic {
/** Returns the domain create cost with allocation-token-related discounts applied. */
private Money getDomainCreateCostWithDiscount(
DomainPrices domainPrices, int years, Optional<AllocationToken> allocationToken, Tld tld)
throws EppException {
DomainPrices domainPrices, int years, Optional<AllocationToken> allocationToken, Tld tld) {
return getDomainCostWithDiscount(
domainPrices.isPremium(),
years,
@@ -277,9 +273,7 @@ public final class DomainPricingLogic {
DomainPrices domainPrices,
DateTime dateTime,
int years,
Optional<AllocationToken> allocationToken)
throws AllocationTokenInvalidForCurrencyException,
AllocationTokenInvalidForPremiumNameException {
Optional<AllocationToken> allocationToken) {
// Short-circuit if the user sent an anchor-tenant or otherwise NONPREMIUM-renewal token
if (allocationToken.isPresent()) {
AllocationToken token = allocationToken.get();
@@ -315,44 +309,41 @@ public final class DomainPricingLogic {
Optional<AllocationToken> allocationToken,
Money firstYearCost,
Optional<Money> subsequentYearCost,
Tld tld)
throws AllocationTokenInvalidForCurrencyException,
AllocationTokenInvalidForPremiumNameException {
Tld tld) {
checkArgument(years > 0, "Registration years to get cost for must be positive.");
validateTokenForPossiblePremiumName(allocationToken, isPremium);
Money totalDomainFlowCost =
firstYearCost.plus(subsequentYearCost.orElse(firstYearCost).multipliedBy(years - 1));
if (allocationToken.isEmpty()) {
return totalDomainFlowCost;
}
AllocationToken token = allocationToken.get();
if (discountTokenInvalidForPremiumName(token, isPremium)) {
return totalDomainFlowCost;
}
if (!token.getTokenBehavior().equals(TokenBehavior.DEFAULT)) {
return totalDomainFlowCost;
}
// Apply the allocation token discount, if applicable.
if (allocationToken.isPresent()
&& allocationToken.get().getTokenBehavior().equals(TokenBehavior.DEFAULT)) {
if (allocationToken.get().getDiscountPrice().isPresent()) {
if (!tld.getCurrency()
.equals(allocationToken.get().getDiscountPrice().get().getCurrencyUnit())) {
throw new AllocationTokenInvalidForCurrencyException();
}
int nonDiscountedYears = Math.max(0, years - allocationToken.get().getDiscountYears());
totalDomainFlowCost =
allocationToken
.get()
.getDiscountPrice()
.get()
.multipliedBy(allocationToken.get().getDiscountYears())
.plus(subsequentYearCost.orElse(firstYearCost).multipliedBy(nonDiscountedYears));
} else {
// Assumes token has discount fraction set.
int discountedYears = Math.min(years, allocationToken.get().getDiscountYears());
if (token.getDiscountPrice().isPresent()
&& tld.getCurrency().equals(token.getDiscountPrice().get().getCurrencyUnit())) {
int nonDiscountedYears = Math.max(0, years - token.getDiscountYears());
totalDomainFlowCost =
token
.getDiscountPrice()
.get()
.multipliedBy(token.getDiscountYears())
.plus(subsequentYearCost.orElse(firstYearCost).multipliedBy(nonDiscountedYears));
} else if (token.getDiscountFraction() > 0) {
int discountedYears = Math.min(years, token.getDiscountYears());
if (discountedYears > 0) {
var discount =
firstYearCost
.plus(subsequentYearCost.orElse(firstYearCost).multipliedBy(discountedYears - 1))
.multipliedBy(
allocationToken.get().getDiscountFraction(), RoundingMode.HALF_EVEN);
var discount =
firstYearCost
.plus(subsequentYearCost.orElse(firstYearCost).multipliedBy(discountedYears - 1))
.multipliedBy(token.getDiscountFraction(), RoundingMode.HALF_EVEN);
totalDomainFlowCost = totalDomainFlowCost.minus(discount);
}
}
}
return totalDomainFlowCost;
}
@@ -376,18 +367,4 @@ public final class DomainPricingLogic {
: 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 {
public AllocationTokenInvalidForPremiumNameException() {
super("Token not valid for premium name");
}
}
public static class AllocationTokenInvalidForCurrencyException extends CommandUseErrorException {
public AllocationTokenInvalidForCurrencyException() {
super("Token and domain currencies do not match.");
}
}
}

View File

@@ -31,7 +31,7 @@ import static google.registry.flows.domain.DomainFlowUtils.validateRegistrationP
import static google.registry.flows.domain.DomainFlowUtils.verifyRegistrarIsActive;
import static google.registry.flows.domain.DomainFlowUtils.verifyUnitIsYears;
import static google.registry.flows.domain.token.AllocationTokenFlowUtils.maybeApplyBulkPricingRemovalToken;
import static google.registry.flows.domain.token.AllocationTokenFlowUtils.verifyTokenAllowedOnDomain;
import static google.registry.flows.domain.token.AllocationTokenFlowUtils.verifyBulkTokenAllowedOnDomain;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_RENEW;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.leapSafeAddYears;
@@ -124,15 +124,12 @@ import org.joda.time.Duration;
* @error {@link RemoveBulkPricingTokenOnNonBulkPricingDomainException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException}
* @error {@link AllocationTokenFlowUtils.NonexistentAllocationTokenException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException}
*/
@ReportingSpec(ActivityReportField.DOMAIN_RENEW)
@@ -154,7 +151,6 @@ public final class DomainRenewFlow implements MutatingFlow {
@Inject @Superuser boolean isSuperuser;
@Inject DomainHistory.Builder historyBuilder;
@Inject EppResponse.Builder responseBuilder;
@Inject AllocationTokenFlowUtils allocationTokenFlowUtils;
@Inject DomainRenewFlowCustomLogic flowCustomLogic;
@Inject DomainPricingLogic pricingLogic;
@Inject DomainRenewFlow() {}
@@ -174,22 +170,17 @@ public final class DomainRenewFlow implements MutatingFlow {
String tldStr = existingDomain.getTld();
Tld tld = Tld.get(tldStr);
Optional<AllocationToken> allocationToken =
allocationTokenFlowUtils.verifyAllocationTokenIfPresent(
existingDomain,
tld,
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
registrarId,
now,
CommandName.RENEW,
eppInput.getSingleExtension(AllocationTokenExtension.class));
boolean defaultTokenUsed = false;
if (allocationToken.isEmpty()) {
allocationToken =
DomainFlowUtils.checkForDefaultToken(
tld, existingDomain.getDomainName(), CommandName.RENEW, registrarId, now);
if (allocationToken.isPresent()) {
defaultTokenUsed = true;
}
}
eppInput.getSingleExtension(AllocationTokenExtension.class),
tld,
existingDomain.getDomainName(),
CommandName.RENEW);
boolean defaultTokenUsed =
allocationToken
.map(t -> t.getTokenType().equals(AllocationToken.TokenType.DEFAULT_PROMO))
.orElse(false);
verifyRenewAllowed(authInfo, existingDomain, command, allocationToken);
// If client passed an applicable static token this updates the domain
@@ -259,7 +250,7 @@ public final class DomainRenewFlow implements MutatingFlow {
newDomain, domainHistory, explicitRenewEvent, newAutorenewEvent, newAutorenewPollMessage);
if (allocationToken.isPresent() && allocationToken.get().getTokenType().isOneTimeUse()) {
entitiesToSave.add(
allocationTokenFlowUtils.redeemToken(
AllocationTokenFlowUtils.redeemToken(
allocationToken.get(), domainHistory.getHistoryEntryId()));
}
EntityChanges entityChanges =
@@ -327,7 +318,7 @@ public final class DomainRenewFlow implements MutatingFlow {
}
verifyUnitIsYears(command.getPeriod());
// We only allow __REMOVE_BULK_PRICING__ token on bulk pricing domains for now
verifyTokenAllowedOnDomain(existingDomain, allocationToken);
verifyBulkTokenAllowedOnDomain(existingDomain, allocationToken);
// If the date they specify doesn't match the expiration, fail. (This is an idempotence check).
if (!command.getCurrentExpirationDate().equals(
existingDomain.getRegistrationExpirationTime().toLocalDate())) {

View File

@@ -54,7 +54,6 @@ import google.registry.model.billing.BillingRecurrence;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.domain.token.AllocationTokenExtension;
@@ -94,15 +93,12 @@ import org.joda.time.DateTime;
* @error {@link DomainFlowUtils.NotAuthorizedForTldException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException}
* @error {@link AllocationTokenFlowUtils.NonexistentAllocationTokenException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException}
*/
@ReportingSpec(ActivityReportField.DOMAIN_TRANSFER_APPROVE)
@@ -116,7 +112,6 @@ public final class DomainTransferApproveFlow implements MutatingFlow {
@Inject DomainHistory.Builder historyBuilder;
@Inject EppResponse.Builder responseBuilder;
@Inject DomainPricingLogic pricingLogic;
@Inject AllocationTokenFlowUtils allocationTokenFlowUtils;
@Inject EppInput eppInput;
@Inject DomainTransferApproveFlow() {}
@@ -132,13 +127,8 @@ public final class DomainTransferApproveFlow implements MutatingFlow {
extensionManager.validate();
DateTime now = tm().getTransactionTime();
Domain existingDomain = loadAndVerifyExistence(Domain.class, targetId, now);
allocationTokenFlowUtils.verifyAllocationTokenIfPresent(
existingDomain,
Tld.get(existingDomain.getTld()),
registrarId,
now,
CommandName.TRANSFER,
eppInput.getSingleExtension(AllocationTokenExtension.class));
AllocationTokenFlowUtils.loadAllocationTokenFromExtension(
registrarId, targetId, now, eppInput.getSingleExtension(AllocationTokenExtension.class));
verifyOptionalAuthInfo(authInfo, existingDomain);
verifyHasPendingTransfer(existingDomain);
verifyResourceOwnership(registrarId, existingDomain);

View File

@@ -58,7 +58,6 @@ import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainCommand.Transfer;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.Period;
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName;
import google.registry.model.domain.fee.FeeTransferCommandExtension;
import google.registry.model.domain.fee.FeeTransformResponseExtension;
import google.registry.model.domain.metadata.MetadataExtension;
@@ -123,15 +122,12 @@ import org.joda.time.DateTime;
* @error {@link DomainFlowUtils.UnsupportedFeeAttributeException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException}
* @error {@link AllocationTokenFlowUtils.NonexistentAllocationTokenException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException}
*/
@ReportingSpec(ActivityReportField.DOMAIN_TRANSFER_REQUEST)
@@ -154,7 +150,6 @@ public final class DomainTransferRequestFlow implements MutatingFlow {
@Inject AsyncTaskEnqueuer asyncTaskEnqueuer;
@Inject EppResponse.Builder responseBuilder;
@Inject DomainPricingLogic pricingLogic;
@Inject AllocationTokenFlowUtils allocationTokenFlowUtils;
@Inject DomainTransferRequestFlow() {}
@@ -170,12 +165,10 @@ public final class DomainTransferRequestFlow implements MutatingFlow {
extensionManager.validate();
DateTime now = tm().getTransactionTime();
Domain existingDomain = loadAndVerifyExistence(Domain.class, targetId, now);
allocationTokenFlowUtils.verifyAllocationTokenIfPresent(
existingDomain,
Tld.get(existingDomain.getTld()),
AllocationTokenFlowUtils.loadAllocationTokenFromExtension(
gainingClientId,
targetId,
now,
CommandName.TRANSFER,
eppInput.getSingleExtension(AllocationTokenExtension.class));
Optional<DomainTransferRequestSuperuserExtension> superuserExtension =
eppInput.getSingleExtension(DomainTransferRequestSuperuserExtension.class);

View File

@@ -15,218 +15,97 @@
package google.registry.flows.domain.token;
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.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.pricing.PricingEngineProxy.isDomainPremium;
import static google.registry.util.CollectionUtils.isNullOrEmpty;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.collect.ImmutableList;
import com.google.common.net.InternetDomainName;
import google.registry.flows.EppException;
import google.registry.flows.EppException.AssociationProhibitsOperationException;
import google.registry.flows.EppException.AuthorizationErrorException;
import google.registry.flows.EppException.StatusProhibitsOperationException;
import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException;
import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
import google.registry.model.billing.BillingBase;
import google.registry.model.billing.BillingRecurrence;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainCommand;
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationToken.TokenBehavior;
import google.registry.model.domain.token.AllocationToken.TokenStatus;
import google.registry.model.domain.token.AllocationTokenExtension;
import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
import google.registry.model.tld.Tld;
import google.registry.persistence.VKey;
import jakarta.inject.Inject;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.joda.time.DateTime;
/** Utility functions for dealing with {@link AllocationToken}s in domain flows. */
public class AllocationTokenFlowUtils {
@Inject
public AllocationTokenFlowUtils() {}
/**
* Checks if the allocation token applies to the given domain names, used for domain checks.
*
* @return A map of domain names to domain check error response messages. If a message is present
* for a a given domain then it does not validate with this allocation token; domains that do
* validate have blank messages (i.e. no error).
*/
public AllocationTokenDomainCheckResults checkDomainsWithToken(
List<InternetDomainName> domainNames, String token, String registrarId, DateTime now) {
// If the token is completely invalid, return the error message for all domain names
AllocationToken tokenEntity;
try {
tokenEntity = loadToken(token);
} catch (EppException e) {
return new AllocationTokenDomainCheckResults(
Optional.empty(), Maps.toMap(domainNames, ignored -> e.getMessage()));
}
// If the token is only invalid for some domain names (e.g. an invalid TLD), include those error
// results for only those domain names
ImmutableMap.Builder<InternetDomainName, String> resultsBuilder = new ImmutableMap.Builder<>();
for (InternetDomainName domainName : domainNames) {
try {
validateToken(
domainName,
tokenEntity,
CommandName.CREATE,
registrarId,
isDomainPremium(domainName.toString(), now),
now);
resultsBuilder.put(domainName, "");
} catch (EppException e) {
resultsBuilder.put(domainName, e.getMessage());
}
}
return new AllocationTokenDomainCheckResults(Optional.of(tokenEntity), resultsBuilder.build());
}
private AllocationTokenFlowUtils() {}
/** Redeems a SINGLE_USE {@link AllocationToken}, returning the redeemed copy. */
public AllocationToken redeemToken(AllocationToken token, HistoryEntryId redemptionHistoryId) {
public static AllocationToken redeemToken(
AllocationToken token, HistoryEntryId redemptionHistoryId) {
checkArgument(
token.getTokenType().isOneTimeUse(), "Only SINGLE_USE tokens can be marked as redeemed");
return token.asBuilder().setRedemptionHistoryId(redemptionHistoryId).build();
}
/**
* Validates a given token. The token could be invalid if it has allowed client IDs or TLDs that
* do not include this client ID / TLD, or if the token has a promotion that is not currently
* running, or the token is not valid for a premium name when necessary.
*
* @throws EppException if the token is invalid in any way
*/
public static void validateToken(
InternetDomainName domainName,
AllocationToken token,
CommandName commandName,
String registrarId,
boolean isPremium,
DateTime now)
throws EppException {
// Only tokens with default behavior require validation
if (!TokenBehavior.DEFAULT.equals(token.getTokenBehavior())) {
return;
}
validateTokenForPossiblePremiumName(Optional.of(token), isPremium);
if (!token.getAllowedEppActions().isEmpty()
&& !token.getAllowedEppActions().contains(commandName)) {
throw new AllocationTokenNotValidForCommandException();
}
if (!token.getAllowedRegistrarIds().isEmpty()
&& !token.getAllowedRegistrarIds().contains(registrarId)) {
throw new AllocationTokenNotValidForRegistrarException();
}
if (!token.getAllowedTlds().isEmpty()
&& !token.getAllowedTlds().contains(domainName.parent().toString())) {
throw new AllocationTokenNotValidForTldException();
}
if (token.getDomainName().isPresent()
&& !token.getDomainName().get().equals(domainName.toString())) {
throw new AllocationTokenNotValidForDomainException();
}
// Tokens without status transitions will just have a single-entry NOT_STARTED map, so only
// check the status transitions map if it's non-trivial.
if (token.getTokenStatusTransitions().size() > 1
&& !TokenStatus.VALID.equals(token.getTokenStatusTransitions().getValueAtTime(now))) {
throw new AllocationTokenNotInPromotionException();
}
}
/** Validates that the given token is valid for a premium name if the name is premium. */
public static void validateTokenForPossiblePremiumName(
Optional<AllocationToken> token, boolean isPremium)
throws AllocationTokenInvalidForPremiumNameException {
if (token.isPresent()
&& (token.get().getDiscountFraction() != 0.0 || token.get().getDiscountPrice().isPresent())
/** Don't apply discounts on premium domains if the token isn't configured that way. */
public static boolean discountTokenInvalidForPremiumName(
AllocationToken token, boolean isPremium) {
return (token.getDiscountFraction() != 0.0 || token.getDiscountPrice().isPresent())
&& isPremium
&& !token.get().shouldDiscountPremiums()) {
throw new AllocationTokenInvalidForPremiumNameException();
}
&& !token.shouldDiscountPremiums();
}
/** Loads a given token and validates that it is not redeemed */
private static AllocationToken loadToken(String token) throws EppException {
if (Strings.isNullOrEmpty(token)) {
// We load the token directly from the input XML. If it's null or empty we should throw
// an InvalidAllocationTokenException before the database load attempt fails.
// See https://tools.ietf.org/html/draft-ietf-regext-allocation-token-04#section-2.1
throw new InvalidAllocationTokenException();
}
Optional<AllocationToken> maybeTokenEntity = AllocationToken.maybeGetStaticTokenInstance(token);
if (maybeTokenEntity.isPresent()) {
return maybeTokenEntity.get();
}
// TODO(b/368069206): `reTransact` needed by tests only.
maybeTokenEntity =
tm().reTransact(() -> tm().loadByKeyIfPresent(VKey.create(AllocationToken.class, token)));
if (maybeTokenEntity.isEmpty()) {
throw new InvalidAllocationTokenException();
}
if (maybeTokenEntity.get().isRedeemed()) {
throw new AlreadyRedeemedAllocationTokenException();
}
return maybeTokenEntity.get();
}
/** Verifies and returns the allocation token if one is specified, otherwise does nothing. */
public Optional<AllocationToken> verifyAllocationTokenCreateIfPresent(
DomainCommand.Create command,
Tld tld,
/** Loads and verifies the allocation token if one is specified, otherwise does nothing. */
public static Optional<AllocationToken> loadAllocationTokenFromExtension(
String registrarId,
String domainName,
DateTime now,
Optional<AllocationTokenExtension> extension)
throws EppException {
throws NonexistentAllocationTokenException, AllocationTokenInvalidException {
if (extension.isEmpty()) {
return Optional.empty();
}
AllocationToken tokenEntity = loadToken(extension.get().getAllocationToken());
validateToken(
InternetDomainName.from(command.getDomainName()),
tokenEntity,
CommandName.CREATE,
registrarId,
isDomainPremium(command.getDomainName(), now),
now);
return Optional.of(tokenEntity);
return Optional.of(
loadAndValidateToken(extension.get().getAllocationToken(), registrarId, domainName, now));
}
/** Verifies and returns the allocation token if one is specified, otherwise does nothing. */
public Optional<AllocationToken> verifyAllocationTokenIfPresent(
Domain existingDomain,
Tld tld,
/**
* Loads the relevant token, if present, for the given extension + request.
*
* <p>This may be the allocation token provided in the request, if it is present and valid for the
* request. Otherwise, it may be a default allocation token if one is present and valid for the
* request.
*/
public static Optional<AllocationToken> loadTokenFromExtensionOrGetDefault(
String registrarId,
DateTime now,
CommandName commandName,
Optional<AllocationTokenExtension> extension)
throws EppException {
if (extension.isEmpty()) {
return Optional.empty();
Optional<AllocationTokenExtension> extension,
Tld tld,
String domainName,
CommandName commandName)
throws NonexistentAllocationTokenException, AllocationTokenInvalidException {
Optional<AllocationToken> fromExtension =
loadAllocationTokenFromExtension(registrarId, domainName, now, extension);
if (fromExtension.isPresent()
&& tokenIsValidAgainstDomain(
InternetDomainName.from(domainName), fromExtension.get(), commandName, now)) {
return fromExtension;
}
AllocationToken tokenEntity = loadToken(extension.get().getAllocationToken());
validateToken(
InternetDomainName.from(existingDomain.getDomainName()),
tokenEntity,
commandName,
registrarId,
isDomainPremium(existingDomain.getDomainName(), now),
now);
return Optional.of(tokenEntity);
return checkForDefaultToken(tld, domainName, commandName, registrarId, now);
}
public static void verifyTokenAllowedOnDomain(
/** Verifies that the given domain can have a bulk pricing token removed from it. */
public static void verifyBulkTokenAllowedOnDomain(
Domain domain, Optional<AllocationToken> allocationToken) throws EppException {
boolean domainHasBulkToken = domain.getCurrentBulkToken().isPresent();
boolean hasRemoveBulkPricingToken =
allocationToken.isPresent()
@@ -239,6 +118,11 @@ public class AllocationTokenFlowUtils {
}
}
/**
* Removes the bulk pricing token from the provided domain, if applicable.
*
* @param allocationToken the (possibly) REMOVE_BULK_PRICING token provided by the client.
*/
public static Domain maybeApplyBulkPricingRemovalToken(
Domain domain, Optional<AllocationToken> allocationToken) {
if (allocationToken.isEmpty()
@@ -249,7 +133,7 @@ public class AllocationTokenFlowUtils {
BillingRecurrence newBillingRecurrence =
tm().loadByKey(domain.getAutorenewBillingEvent())
.asBuilder()
.setRenewalPriceBehavior(RenewalPriceBehavior.DEFAULT)
.setRenewalPriceBehavior(BillingBase.RenewalPriceBehavior.DEFAULT)
.setRenewalPrice(null)
.build();
@@ -267,35 +151,139 @@ public class AllocationTokenFlowUtils {
.build();
}
/**
* Checks if the given token is valid for the given request.
*
* <p>Note that if the token is not valid, that is not a catastrophic error -- we may move on to
* trying a different token or skip token usage entirely.
*/
@VisibleForTesting
static boolean tokenIsValidAgainstDomain(
InternetDomainName domainName, AllocationToken token, CommandName commandName, DateTime now) {
if (discountTokenInvalidForPremiumName(token, isDomainPremium(domainName.toString(), now))) {
return false;
}
if (!token.getAllowedEppActions().isEmpty()
&& !token.getAllowedEppActions().contains(commandName)) {
return false;
}
if (!token.getAllowedTlds().isEmpty()
&& !token.getAllowedTlds().contains(domainName.parent().toString())) {
return false;
}
return token.getDomainName().isEmpty()
|| token.getDomainName().get().equals(domainName.toString());
}
/**
* Checks if there is a valid default token to be used for a domain create command.
*
* <p>If there is more than one valid default token for the registration, only the first valid
* token found on the TLD's default token list will be returned.
*/
private static Optional<AllocationToken> checkForDefaultToken(
Tld tld, String domainName, CommandName commandName, String registrarId, DateTime now) {
ImmutableList<VKey<AllocationToken>> tokensFromTld = tld.getDefaultPromoTokens();
if (isNullOrEmpty(tokensFromTld)) {
return Optional.empty();
}
Map<VKey<AllocationToken>, Optional<AllocationToken>> tokens =
AllocationToken.getAll(tokensFromTld);
checkState(
!isNullOrEmpty(tokens), "Failure while loading default TLD tokens from the database");
// Iterate over the list to maintain token ordering (since we return the first valid token)
ImmutableList<AllocationToken> tokenList =
tokensFromTld.stream()
.map(tokens::get)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(toImmutableList());
// Check if any of the tokens are valid for this domain registration
for (AllocationToken token : tokenList) {
try {
validateTokenEntity(token, registrarId, domainName, now);
} catch (AllocationTokenInvalidException e) {
// Token is not valid for this registrar, etc. -- continue trying tokens
continue;
}
if (tokenIsValidAgainstDomain(InternetDomainName.from(domainName), token, commandName, now)) {
return Optional.of(token);
}
}
// No valid default token found
return Optional.empty();
}
/** Loads a given token and validates it against the registrar, time, etc */
private static AllocationToken loadAndValidateToken(
String token, String registrarId, String domainName, DateTime now)
throws NonexistentAllocationTokenException, AllocationTokenInvalidException {
if (Strings.isNullOrEmpty(token)) {
// We load the token directly from the input XML. If it's null or empty we should throw
// an NonexistentAllocationTokenException before the database load attempt fails.
// See https://tools.ietf.org/html/draft-ietf-regext-allocation-token-04#section-2.1
throw new NonexistentAllocationTokenException();
}
Optional<AllocationToken> maybeTokenEntity = AllocationToken.maybeGetStaticTokenInstance(token);
if (maybeTokenEntity.isPresent()) {
return maybeTokenEntity.get();
}
maybeTokenEntity = AllocationToken.get(VKey.create(AllocationToken.class, token));
if (maybeTokenEntity.isEmpty()) {
throw new NonexistentAllocationTokenException();
}
AllocationToken tokenEntity = maybeTokenEntity.get();
validateTokenEntity(tokenEntity, registrarId, domainName, now);
return tokenEntity;
}
private static void validateTokenEntity(
AllocationToken token, String registrarId, String domainName, DateTime now)
throws AllocationTokenInvalidException {
if (token.isRedeemed()) {
throw new AlreadyRedeemedAllocationTokenException();
}
if (!token.getAllowedRegistrarIds().isEmpty()
&& !token.getAllowedRegistrarIds().contains(registrarId)) {
throw new AllocationTokenNotValidForRegistrarException();
}
// Tokens without status transitions will just have a single-entry NOT_STARTED map, so only
// check the status transitions map if it's non-trivial.
if (token.getTokenStatusTransitions().size() > 1
&& !AllocationToken.TokenStatus.VALID.equals(
token.getTokenStatusTransitions().getValueAtTime(now))) {
throw new AllocationTokenNotInPromotionException();
}
if (token.getDomainName().isPresent() && !token.getDomainName().get().equals(domainName)) {
throw new AllocationTokenNotValidForDomainException();
}
}
// Note: exception messages should be <= 32 characters long for domain check results
/** The allocation token exists but is not valid, e.g. the wrong registrar. */
public abstract static class AllocationTokenInvalidException
extends StatusProhibitsOperationException {
AllocationTokenInvalidException(String message) {
super(message);
}
}
/** The allocation token is not currently valid. */
public static class AllocationTokenNotInPromotionException
extends StatusProhibitsOperationException {
extends AllocationTokenInvalidException {
AllocationTokenNotInPromotionException() {
super("Alloc token not in promo period");
}
}
/** The allocation token is not valid for this TLD. */
public static class AllocationTokenNotValidForTldException
extends AssociationProhibitsOperationException {
AllocationTokenNotValidForTldException() {
super("Alloc token invalid for TLD");
}
}
/** The allocation token is not valid for this domain. */
public static class AllocationTokenNotValidForDomainException
extends AssociationProhibitsOperationException {
AllocationTokenNotValidForDomainException() {
super("Alloc token invalid for domain");
}
}
/** The allocation token is not valid for this registrar. */
public static class AllocationTokenNotValidForRegistrarException
extends AssociationProhibitsOperationException {
extends AllocationTokenInvalidException {
AllocationTokenNotValidForRegistrarException() {
super("Alloc token invalid for client");
}
@@ -303,23 +291,23 @@ public class AllocationTokenFlowUtils {
/** The allocation token was already redeemed. */
public static class AlreadyRedeemedAllocationTokenException
extends AssociationProhibitsOperationException {
extends AllocationTokenInvalidException {
AlreadyRedeemedAllocationTokenException() {
super("Alloc token was already redeemed");
}
}
/** The allocation token is not valid for this EPP command. */
public static class AllocationTokenNotValidForCommandException
extends AssociationProhibitsOperationException {
AllocationTokenNotValidForCommandException() {
super("Allocation token not valid for the EPP command");
/** The allocation token is not valid for this domain. */
public static class AllocationTokenNotValidForDomainException
extends AllocationTokenInvalidException {
AllocationTokenNotValidForDomainException() {
super("Alloc token invalid for domain");
}
}
}
/** The allocation token is invalid. */
public static class InvalidAllocationTokenException extends AuthorizationErrorException {
InvalidAllocationTokenException() {
public static class NonexistentAllocationTokenException extends AuthorizationErrorException {
NonexistentAllocationTokenException() {
super("The allocation token is invalid");
}
}

View File

@@ -404,7 +404,7 @@ public abstract class EppResource extends UpdateAutoTimestampEntity implements B
* <p>Don't use this unless you really need it for performance reasons, and be sure that you are
* OK with the trade-offs in loss of transactional consistency.
*/
public static ImmutableMap<VKey<? extends EppResource>, EppResource> loadCached(
public static ImmutableMap<VKey<? extends EppResource>, EppResource> loadByCacheIfEnabled(
Iterable<VKey<? extends EppResource>> keys) {
if (!RegistryConfig.isEppResourceCachingEnabled()) {
return tm().reTransact(() -> tm().loadByKeys(keys));
@@ -413,15 +413,12 @@ public abstract class EppResource extends UpdateAutoTimestampEntity implements B
}
/**
* Loads a given EppResource by its key using the cache (if enabled).
* Loads a given EppResource by its key using the cache.
*
* <p>Don't use this unless you really need it for performance reasons, and be sure that you are
* OK with the trade-offs in loss of transactional consistency.
* <p>This method ignores the `isEppResourceCachingEnabled` config setting. It is reserved for use
* cases that can tolerate slightly stale data, e.g., RDAP queries.
*/
public static <T extends EppResource> T loadCached(VKey<T> key) {
if (!RegistryConfig.isEppResourceCachingEnabled()) {
return tm().reTransact(() -> tm().loadByKey(key));
}
public static <T extends EppResource> T loadByCache(VKey<T> key) {
// Safe to cast because loading a Key<T> returns an entity of type T.
@SuppressWarnings("unchecked")
T resource = (T) cacheEppResources.get(key);

View File

@@ -16,6 +16,7 @@ package google.registry.model;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static google.registry.util.DateTimeUtils.isAtOrAfter;
@@ -40,6 +41,7 @@ import google.registry.model.transfer.DomainTransferData;
import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferStatus;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.TransactionManager;
import jakarta.persistence.Query;
import java.util.Collection;
import java.util.Comparator;
@@ -109,12 +111,12 @@ public final class EppResourceUtils {
*/
public static <T extends EppResource> Optional<T> loadByForeignKey(
Class<T> clazz, String foreignKey, DateTime now) {
return loadByForeignKeyHelper(clazz, foreignKey, now, false);
return loadByForeignKeyHelper(tm(), clazz, foreignKey, now, false);
}
/**
* Loads the last created version of an {@link EppResource} from the database by foreign key,
* using a cache.
* using a cache, if caching is enabled in config settings.
*
* <p>Returns null if no resource with this foreign key was ever created, or if the most recently
* created resource was deleted before time "now".
@@ -134,20 +136,36 @@ public final class EppResourceUtils {
* @param foreignKey id to match
* @param now the current logical time to project resources at
*/
public static <T extends EppResource> Optional<T> loadByForeignKeyCached(
public static <T extends EppResource> Optional<T> loadByForeignKeyByCacheIfEnabled(
Class<T> clazz, String foreignKey, DateTime now) {
return loadByForeignKeyHelper(
clazz, foreignKey, now, RegistryConfig.isEppResourceCachingEnabled());
tm(), clazz, foreignKey, now, RegistryConfig.isEppResourceCachingEnabled());
}
/**
* Loads the last created version of an {@link EppResource} from the replica database by foreign
* key, using a cache.
*
* <p>This method ignores the config setting for caching, and is reserved for use cases that can
* tolerate slightly stale data.
*/
public static <T extends EppResource> Optional<T> loadByForeignKeyByCache(
Class<T> clazz, String foreignKey, DateTime now) {
return loadByForeignKeyHelper(replicaTm(), clazz, foreignKey, now, true);
}
private static <T extends EppResource> Optional<T> loadByForeignKeyHelper(
Class<T> clazz, String foreignKey, DateTime now, boolean useCache) {
TransactionManager txnManager,
Class<T> clazz,
String foreignKey,
DateTime now,
boolean useCache) {
checkArgument(
ForeignKeyedEppResource.class.isAssignableFrom(clazz),
"loadByForeignKey may only be called for foreign keyed EPP resources");
VKey<T> key =
useCache
? ForeignKeyUtils.loadCached(clazz, ImmutableList.of(foreignKey), now).get(foreignKey)
? ForeignKeyUtils.loadByCache(clazz, ImmutableList.of(foreignKey), now).get(foreignKey)
: ForeignKeyUtils.load(clazz, foreignKey, now);
// The returned key is null if the resource is hard deleted or soft deleted by the given time.
if (key == null) {
@@ -155,10 +173,10 @@ public final class EppResourceUtils {
}
T resource =
useCache
? EppResource.loadCached(key)
? EppResource.loadByCache(key)
// This transaction is buried very deeply inside many outer nested calls, hence merits
// the use of reTransact() for now pending a substantial refactoring.
: tm().reTransact(() -> tm().loadByKeyIfPresent(key).orElse(null));
: txnManager.reTransact(() -> txnManager.loadByKeyIfPresent(key).orElse(null));
if (resource == null || isAtOrAfter(now, resource.getDeletionTime())) {
return Optional.empty();
}

View File

@@ -204,11 +204,25 @@ public final class ForeignKeyUtils {
* <p>Don't use the cached version of this method unless you really need it for performance
* reasons, and are OK with the trade-offs in loss of transactional consistency.
*/
public static <E extends EppResource> ImmutableMap<String, VKey<E>> loadCached(
public static <E extends EppResource> ImmutableMap<String, VKey<E>> loadByCacheIfEnabled(
Class<E> clazz, Collection<String> foreignKeys, final DateTime now) {
if (!RegistryConfig.isEppResourceCachingEnabled()) {
return load(clazz, foreignKeys, now);
}
return loadByCache(clazz, foreignKeys, now);
}
/**
* Load a list of {@link VKey} to {@link EppResource} instances by class and foreign key strings
* that are active at or after the specified moment in time, using the cache.
*
* <p>The returned map will omit any keys for which the {@link EppResource} doesn't exist or has
* been soft-deleted.
*
* <p>This method is reserved for use cases that can tolerate slightly stale data.
*/
public static <E extends EppResource> ImmutableMap<String, VKey<E>> loadByCache(
Class<E> clazz, Collection<String> foreignKeys, final DateTime now) {
return foreignKeyCache
.getAll(foreignKeys.stream().map(fk -> VKey.create(clazz, fk)).collect(toImmutableList()))
.entrySet()

View File

@@ -1,99 +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.model.console;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
import google.registry.persistence.VKey;
import jakarta.persistence.Access;
import jakarta.persistence.AccessType;
import jakarta.persistence.AttributeOverride;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Index;
import jakarta.persistence.Table;
/**
* A persisted history object representing an EPP action via the console.
*
* <p>In addition to the generic history fields (time, URL, etc.) we also persist a reference to the
* history entry so that we can refer to it if necessary.
*/
@Access(AccessType.FIELD)
@Entity
@Table(
indexes = {
@Index(columnList = "historyActingUser"),
@Index(columnList = "repoId"),
@Index(columnList = "revisionId")
})
public class ConsoleEppActionHistory extends ConsoleUpdateHistory {
@AttributeOverride(name = "repoId", column = @Column(nullable = false))
HistoryEntryId historyEntryId;
@Column(nullable = false)
Class<? extends HistoryEntry> historyEntryClass;
public HistoryEntryId getHistoryEntryId() {
return historyEntryId;
}
public Class<? extends HistoryEntry> getHistoryEntryClass() {
return historyEntryClass;
}
/** Creates a {@link VKey} instance for this entity. */
@Override
public VKey<ConsoleEppActionHistory> createVKey() {
return VKey.create(ConsoleEppActionHistory.class, getRevisionId());
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
}
/** Builder for the immutable UserUpdateHistory. */
public static class Builder
extends ConsoleUpdateHistory.Builder<ConsoleEppActionHistory, Builder> {
public Builder() {}
public Builder(ConsoleEppActionHistory instance) {
super(instance);
}
@Override
public ConsoleEppActionHistory build() {
checkArgumentNotNull(getInstance().historyEntryId, "History entry ID must be specified");
checkArgumentNotNull(
getInstance().historyEntryClass, "History entry class must be specified");
return super.build();
}
public Builder setHistoryEntryId(HistoryEntryId historyEntryId) {
getInstance().historyEntryId = historyEntryId;
return this;
}
public Builder setHistoryEntryClass(Class<? extends HistoryEntry> historyEntryClass) {
getInstance().historyEntryClass = historyEntryClass;
return this;
}
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
// 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.
@@ -19,26 +19,87 @@ import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.model.annotations.IdAllocation;
import jakarta.persistence.Access;
import jakarta.persistence.AccessType;
import google.registry.persistence.WithVKey;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.Table;
import java.util.Optional;
import org.joda.time.DateTime;
/**
* A record of a resource that was updated through the console.
*
* <p>This abstract class has several subclasses that (mostly) include the modified resource itself
* so that the entire object history is persisted to SQL.
*/
@Access(AccessType.FIELD)
@MappedSuperclass
public abstract class ConsoleUpdateHistory extends ImmutableObject implements Buildable {
@Entity
@WithVKey(Long.class)
@Table(
indexes = {
@Index(columnList = "actingUser", name = "idx_console_update_history_acting_user"),
@Index(columnList = "type", name = "idx_console_update_history_type"),
@Index(columnList = "modificationTime", name = "idx_console_update_history_modification_time")
})
public class ConsoleUpdateHistory extends ImmutableObject implements Buildable {
@Id @IdAllocation @Column Long revisionId;
@Column(nullable = false)
DateTime modificationTime;
/** The HTTP method (e.g. POST, PUT) used to make this modification. */
@Column(nullable = false)
String method;
/** The type of modification. */
@Column(nullable = false)
@Enumerated(EnumType.STRING)
Type type;
/** The URL of the action that was used to make the modification. */
@Column(nullable = false)
String url;
/** An optional further description of the action. */
String description;
/** The user that performed the modification. */
@JoinColumn(name = "actingUser", referencedColumnName = "emailAddress", nullable = false)
@ManyToOne
User actingUser;
public Long getRevisionId() {
return revisionId;
}
public DateTime getModificationTime() {
return modificationTime;
}
public Optional<String> getDescription() {
return Optional.ofNullable(description);
}
public String getMethod() {
return method;
}
public Type getType() {
return type;
}
public String getUrl() {
return url;
}
public User getActingUser() {
return actingUser;
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
}
public enum Type {
DOMAIN_DELETE,
@@ -53,118 +114,51 @@ public abstract class ConsoleUpdateHistory extends ImmutableObject implements Bu
USER_UPDATE
}
/** Autogenerated ID of this event. */
@Id
@IdAllocation
@Column(nullable = false, name = "historyRevisionId")
protected Long revisionId;
public static class Builder extends Buildable.Builder<ConsoleUpdateHistory> {
public Builder() {}
/** The user that performed the modification. */
@JoinColumn(name = "historyActingUser", referencedColumnName = "emailAddress", nullable = false)
@ManyToOne
User actingUser;
/** The URL of the action that was used to make the modification. */
@Column(nullable = false, name = "historyUrl")
String url;
/** The HTTP method (e.g. POST, PUT) used to make this modification. */
@Column(nullable = false, name = "historyMethod")
String method;
/** The raw body of the request that was used to make this modification. */
@Column(name = "historyRequestBody")
String requestBody;
/** The time at which the modification was mode. */
@Column(nullable = false, name = "historyModificationTime")
DateTime modificationTime;
/** The type of modification. */
@Column(nullable = false, name = "historyType")
@Enumerated(EnumType.STRING)
Type type;
public long getRevisionId() {
return revisionId;
}
public User getActingUser() {
return actingUser;
}
public String getUrl() {
return url;
}
public String getMethod() {
return method;
}
public String getRequestBody() {
return requestBody;
}
public DateTime getModificationTime() {
return modificationTime;
}
public Type getType() {
return type;
}
@Override
public abstract Builder<? extends ConsoleUpdateHistory, ?> asBuilder();
/** Builder for the immutable ConsoleUpdateHistory. */
public abstract static class Builder<
T extends ConsoleUpdateHistory, B extends ConsoleUpdateHistory.Builder<?, ?>>
extends GenericBuilder<T, B> {
protected Builder() {}
protected Builder(T instance) {
private Builder(ConsoleUpdateHistory instance) {
super(instance);
}
@Override
public T build() {
public ConsoleUpdateHistory build() {
checkArgumentNotNull(getInstance().modificationTime, "Modification time must be specified");
checkArgumentNotNull(getInstance().actingUser, "Acting user must be specified");
checkArgumentNotNull(getInstance().url, "URL must be specified");
checkArgumentNotNull(getInstance().method, "HTTP method must be specified");
checkArgumentNotNull(getInstance().modificationTime, "modificationTime must be specified");
checkArgumentNotNull(getInstance().type, "Console History type must be specified");
checkArgumentNotNull(getInstance().type, "ConsoleUpdateHistory type must be specified");
return super.build();
}
public B setActingUser(User actingUser) {
getInstance().actingUser = actingUser;
return thisCastToDerived();
}
public B setUrl(String url) {
getInstance().url = url;
return thisCastToDerived();
}
public B setMethod(String method) {
getInstance().method = method;
return thisCastToDerived();
}
public B setRequestBody(String requestBody) {
getInstance().requestBody = requestBody;
return thisCastToDerived();
}
public B setModificationTime(DateTime modificationTime) {
public Builder setModificationTime(DateTime modificationTime) {
getInstance().modificationTime = modificationTime;
return thisCastToDerived();
return this;
}
public B setType(Type type) {
public Builder setActingUser(User actingUser) {
getInstance().actingUser = actingUser;
return this;
}
public Builder setUrl(String url) {
getInstance().url = url;
return this;
}
public Builder setMethod(String method) {
getInstance().method = method;
return this;
}
public Builder setDescription(String description) {
getInstance().description = description;
return this;
}
public Builder setType(Type type) {
getInstance().type = type;
return thisCastToDerived();
return this;
}
}
}

View File

@@ -0,0 +1,150 @@
// 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.model.console;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import google.registry.model.Buildable;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.ImmutableObject;
import google.registry.persistence.WithVKey;
import jakarta.persistence.AttributeOverride;
import jakarta.persistence.AttributeOverrides;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import java.util.Optional;
import java.util.UUID;
import org.joda.time.DateTime;
/**
* Represents a password reset request of some type.
*
* <p>Password reset requests must be performed within an hour of the time that they were requested,
* as well as requiring that the requester and the fulfiller have the proper respective permissions.
*/
@Entity
@WithVKey(String.class)
public class PasswordResetRequest extends ImmutableObject implements Buildable {
public enum Type {
EPP,
REGISTRY_LOCK
}
@Id private String verificationCode;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
Type type;
@AttributeOverrides({
@AttributeOverride(
name = "creationTime",
column = @Column(name = "requestTime", nullable = false))
})
CreateAutoTimestamp requestTime = CreateAutoTimestamp.create(null);
@Column(nullable = false)
String requester;
@Column DateTime fulfillmentTime;
@Column(nullable = false)
String destinationEmail;
@Column(nullable = false)
String registrarId;
public String getVerificationCode() {
return verificationCode;
}
public Type getType() {
return type;
}
public DateTime getRequestTime() {
return requestTime.getTimestamp();
}
public String getRequester() {
return requester;
}
public Optional<DateTime> getFulfillmentTime() {
return Optional.ofNullable(fulfillmentTime);
}
public String getDestinationEmail() {
return destinationEmail;
}
public String getRegistrarId() {
return registrarId;
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
}
/** Builder for constructing immutable {@link PasswordResetRequest} objects. */
public static class Builder extends Buildable.Builder<PasswordResetRequest> {
public Builder() {}
private Builder(PasswordResetRequest instance) {
super(instance);
}
@Override
public PasswordResetRequest build() {
checkArgumentNotNull(getInstance().type, "Type must be specified");
checkArgumentNotNull(getInstance().requester, "Requester must be specified");
checkArgumentNotNull(getInstance().destinationEmail, "Destination email must be specified");
checkArgumentNotNull(getInstance().registrarId, "Registrar ID must be specified");
getInstance().verificationCode = UUID.randomUUID().toString();
return super.build();
}
public Builder setType(Type type) {
getInstance().type = type;
return this;
}
public Builder setRequester(String requester) {
getInstance().requester = requester;
return this;
}
public Builder setDestinationEmail(String destinationEmail) {
getInstance().destinationEmail = destinationEmail;
return this;
}
public Builder setRegistrarId(String registrarId) {
getInstance().registrarId = registrarId;
return this;
}
public Builder setFulfillmentTime(DateTime fulfillmentTime) {
getInstance().fulfillmentTime = fulfillmentTime;
return this;
}
}
}

View File

@@ -1,99 +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.model.console;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.registrar.RegistrarPocBase;
import google.registry.persistence.VKey;
import jakarta.persistence.Access;
import jakarta.persistence.AccessType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Index;
import jakarta.persistence.PostLoad;
import jakarta.persistence.Table;
/**
* A persisted history object representing an update to a RegistrarPoc.
*
* <p>In addition to the generic history fields (time, URL, etc.) we also persist a copy of the
* modified RegistrarPoc object at this point in time.
*/
@Access(AccessType.FIELD)
@Entity
@Table(
indexes = {
@Index(columnList = "historyActingUser"),
@Index(columnList = "emailAddress"),
@Index(columnList = "registrarId")
})
public class RegistrarPocUpdateHistory extends ConsoleUpdateHistory {
RegistrarPocBase registrarPoc;
// These fields exist so that they can be populated in the SQL table
@Column(nullable = false)
String emailAddress;
@Column(nullable = false)
String registrarId;
public RegistrarPocBase getRegistrarPoc() {
return registrarPoc;
}
@PostLoad
void postLoad() {
registrarPoc.setEmailAddress(emailAddress);
registrarPoc.setRegistrarId(registrarId);
}
/** Creates a {@link VKey} instance for this entity. */
@Override
public VKey<RegistrarPocUpdateHistory> createVKey() {
return VKey.create(RegistrarPocUpdateHistory.class, getRevisionId());
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
}
/** Builder for the immutable UserUpdateHistory. */
public static class Builder
extends ConsoleUpdateHistory.Builder<RegistrarPocUpdateHistory, Builder> {
public Builder() {}
public Builder(RegistrarPocUpdateHistory instance) {
super(instance);
}
@Override
public RegistrarPocUpdateHistory build() {
checkArgumentNotNull(getInstance().registrarPoc, "Registrar POC must be specified");
return super.build();
}
public Builder setRegistrarPoc(RegistrarPoc registrarPoc) {
getInstance().registrarPoc = registrarPoc;
getInstance().registrarId = registrarPoc.getRegistrarId();
getInstance().emailAddress = registrarPoc.getEmailAddress();
return this;
}
}
}

View File

@@ -1,89 +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.model.console;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import google.registry.model.registrar.RegistrarBase;
import google.registry.persistence.VKey;
import jakarta.persistence.Access;
import jakarta.persistence.AccessType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Index;
import jakarta.persistence.PostLoad;
import jakarta.persistence.Table;
/**
* A persisted history object representing an update to a Registrar.
*
* <p>In addition to the generic history fields (time, URL, etc.) we also persist a copy of the
* modified Registrar object at this point in time.
*/
@Access(AccessType.FIELD)
@Entity
@Table(indexes = {@Index(columnList = "historyActingUser"), @Index(columnList = "registrarId")})
public class RegistrarUpdateHistory extends ConsoleUpdateHistory {
RegistrarBase registrar;
// This field exists so that it exists in the SQL table
@Column(nullable = false)
@SuppressWarnings("unused")
private String registrarId;
public RegistrarBase getRegistrar() {
return registrar;
}
@PostLoad
void postLoad() {
registrar.setRegistrarId(registrarId);
}
/** Creates a {@link VKey} instance for this entity. */
@Override
public VKey<RegistrarUpdateHistory> createVKey() {
return VKey.create(RegistrarUpdateHistory.class, getRevisionId());
}
@Override
public Builder asBuilder() {
return new RegistrarUpdateHistory.Builder(clone(this));
}
/** Builder for the immutable UserUpdateHistory. */
public static class Builder
extends ConsoleUpdateHistory.Builder<RegistrarUpdateHistory, Builder> {
public Builder() {}
public Builder(RegistrarUpdateHistory instance) {
super(instance);
}
@Override
public RegistrarUpdateHistory build() {
checkArgumentNotNull(getInstance().registrar, "Registrar must be specified");
return super.build();
}
public Builder setRegistrar(RegistrarBase registrar) {
getInstance().registrar = registrar;
getInstance().registrarId = registrar.getRegistrarId();
return this;
}
}
}

View File

@@ -1,153 +0,0 @@
// 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.model.console;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.model.annotations.IdAllocation;
import google.registry.persistence.WithVKey;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.util.Optional;
import org.joda.time.DateTime;
@Entity
@WithVKey(Long.class)
@Table(
name = "ConsoleUpdateHistory",
indexes = {
@Index(columnList = "actingUser", name = "idx_console_update_history_acting_user"),
@Index(columnList = "type", name = "idx_console_update_history_type"),
@Index(columnList = "modificationTime", name = "idx_console_update_history_modification_time")
})
// TODO: rename this to ConsoleUpdateHistory when that class is removed
public class SimpleConsoleUpdateHistory extends ImmutableObject implements Buildable {
@Id @IdAllocation @Column Long revisionId;
@Column(nullable = false)
DateTime modificationTime;
/** The HTTP method (e.g. POST, PUT) used to make this modification. */
@Column(nullable = false)
String method;
/** The type of modification. */
@Column(nullable = false)
@Enumerated(EnumType.STRING)
ConsoleUpdateHistory.Type type;
/** The URL of the action that was used to make the modification. */
@Column(nullable = false)
String url;
/** An optional further description of the action. */
String description;
/** The user that performed the modification. */
@JoinColumn(name = "actingUser", referencedColumnName = "emailAddress", nullable = false)
@ManyToOne
User actingUser;
public Long getRevisionId() {
return revisionId;
}
public DateTime getModificationTime() {
return modificationTime;
}
public Optional<String> getDescription() {
return Optional.ofNullable(description);
}
public String getMethod() {
return method;
}
public ConsoleUpdateHistory.Type getType() {
return type;
}
public String getUrl() {
return url;
}
public User getActingUser() {
return actingUser;
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
}
public static class Builder extends Buildable.Builder<SimpleConsoleUpdateHistory> {
public Builder() {}
private Builder(SimpleConsoleUpdateHistory instance) {
super(instance);
}
@Override
public SimpleConsoleUpdateHistory build() {
checkArgumentNotNull(getInstance().modificationTime, "Modification time must be specified");
checkArgumentNotNull(getInstance().actingUser, "Acting user must be specified");
checkArgumentNotNull(getInstance().url, "URL must be specified");
checkArgumentNotNull(getInstance().method, "HTTP method must be specified");
checkArgumentNotNull(getInstance().type, "ConsoleUpdateHistory type must be specified");
return super.build();
}
public Builder setModificationTime(DateTime modificationTime) {
getInstance().modificationTime = modificationTime;
return this;
}
public Builder setActingUser(User actingUser) {
getInstance().actingUser = actingUser;
return this;
}
public Builder setUrl(String url) {
getInstance().url = url;
return this;
}
public Builder setMethod(String method) {
getInstance().method = method;
return this;
}
public Builder setDescription(String description) {
getInstance().description = description;
return this;
}
public Builder setType(ConsoleUpdateHistory.Type type) {
getInstance().type = type;
return this;
}
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
// 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.
@@ -15,23 +15,30 @@
package google.registry.model.console;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.io.BaseEncoding.base64;
import static google.registry.model.registrar.Registrar.checkValidEmail;
import static google.registry.tools.server.UpdateUserGroupAction.GROUP_UPDATE_QUEUE;
import static google.registry.util.PasswordUtils.SALT_SUPPLIER;
import static google.registry.util.PasswordUtils.hashPassword;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.cloud.tasks.v2.Task;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import com.google.gson.annotations.Expose;
import google.registry.batch.CloudTasksUtils;
import google.registry.persistence.VKey;
import google.registry.model.Buildable;
import google.registry.model.UpdateAutoTimestampEntity;
import google.registry.request.Action;
import google.registry.tools.IamClient;
import google.registry.tools.ServiceConnection;
import google.registry.tools.server.UpdateUserGroupAction;
import google.registry.tools.server.UpdateUserGroupAction.Mode;
import google.registry.util.PasswordUtils;
import google.registry.util.RegistryEnvironment;
import jakarta.persistence.Access;
import jakarta.persistence.AccessType;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@@ -44,11 +51,33 @@ import javax.annotation.Nullable;
@Embeddable
@Entity
@Table
public class User extends UserBase {
public class User extends UpdateAutoTimestampEntity implements Buildable {
public static final String IAP_SECURED_WEB_APP_USER_ROLE = "roles/iap.httpsResourceAccessor";
private static final long serialVersionUID = 6936728603828566721L;
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/** Email address of the user in question. */
@Id @Expose String emailAddress;
/** Optional external email address to use for registry lock confirmation emails. */
@Column String registryLockEmailAddress;
/** Roles (which grant permissions) associated with this user. */
@Expose
@Column(nullable = false)
UserRoles userRoles;
/**
* A hashed password that exists iff this contact is registry-lock-enabled. The hash is a base64
* encoded SHA256 string.
*/
String registryLockPasswordHash;
/** Randomly generated hash salt. */
String registryLockPasswordSalt;
/**
* Grants the user permission to pass IAP.
*
@@ -113,9 +142,13 @@ public class User extends UserBase {
logger.atInfo().log("Removing %s from group %s", emailAddress, groupEmailAddress.get());
if (cloudTasksUtils != null) {
modifyGroupMembershipAsync(
emailAddress, groupEmailAddress.get(), cloudTasksUtils, Mode.REMOVE);
emailAddress,
groupEmailAddress.get(),
cloudTasksUtils,
UpdateUserGroupAction.Mode.REMOVE);
} else {
modifyGroupMembershipSync(emailAddress, groupEmailAddress.get(), connection, Mode.REMOVE);
modifyGroupMembershipSync(
emailAddress, groupEmailAddress.get(), connection, UpdateUserGroupAction.Mode.REMOVE);
}
}
}
@@ -124,7 +157,7 @@ public class User extends UserBase {
String userEmailAddress,
String groupEmailAddress,
CloudTasksUtils cloudTasksUtils,
Mode mode) {
UpdateUserGroupAction.Mode mode) {
Task task =
cloudTasksUtils.createTask(
UpdateUserGroupAction.class,
@@ -140,7 +173,10 @@ public class User extends UserBase {
}
private static void modifyGroupMembershipSync(
String userEmailAddress, String groupEmailAddress, ServiceConnection connection, Mode mode) {
String userEmailAddress,
String groupEmailAddress,
ServiceConnection connection,
UpdateUserGroupAction.Mode mode) {
try {
connection.sendPostRequest(
UpdateUserGroupAction.PATH,
@@ -158,30 +194,117 @@ public class User extends UserBase {
}
}
@Id
@Override
@Access(AccessType.PROPERTY)
/**
* Sets the user email address.
*
* <p>This should only be used for restoring an object being loaded in a PostLoad method
* (effectively, when it is still under construction by Hibernate). In all other cases, the object
* should be regarded as immutable and changes should go through a Builder.
*
* <p>In addition to this special case use, this method must exist to satisfy Hibernate.
*/
void setEmailAddress(String emailAddress) {
this.emailAddress = emailAddress;
}
public String getEmailAddress() {
return super.getEmailAddress();
return emailAddress;
}
public Optional<String> getRegistryLockEmailAddress() {
return Optional.ofNullable(registryLockEmailAddress);
}
public UserRoles getUserRoles() {
return userRoles;
}
public boolean hasRegistryLockPassword() {
return !isNullOrEmpty(registryLockPasswordHash) && !isNullOrEmpty(registryLockPasswordSalt);
}
public boolean verifyRegistryLockPassword(String registryLockPassword) {
if (isNullOrEmpty(registryLockPassword)
|| isNullOrEmpty(registryLockPasswordSalt)
|| isNullOrEmpty(registryLockPasswordHash)) {
return false;
}
return PasswordUtils.verifyPassword(
registryLockPassword, registryLockPasswordHash, registryLockPasswordSalt);
}
/**
* Whether the user has the registry lock permission on any registrar or globally.
*
* <p>If so, they should be allowed to (re)set their registry lock password.
*/
public boolean hasAnyRegistryLockPermission() {
if (userRoles == null) {
return false;
}
if (userRoles.isAdmin() || userRoles.hasGlobalPermission(ConsolePermission.REGISTRY_LOCK)) {
return true;
}
return userRoles.getRegistrarRoles().values().stream()
.anyMatch(role -> role.hasPermission(ConsolePermission.REGISTRY_LOCK));
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
}
@Override
public VKey<User> createVKey() {
return VKey.create(User.class, getEmailAddress());
public Builder<? extends User, ?> asBuilder() {
return new Builder<>(clone(this));
}
/** Builder for constructing immutable {@link User} objects. */
public static class Builder extends UserBase.Builder<User, Builder> {
public static class Builder<T extends User, B extends Builder<T, B>>
extends GenericBuilder<T, B> {
public Builder() {}
public Builder(User user) {
super(user);
public Builder(T abstractUser) {
super(abstractUser);
}
@Override
public T build() {
checkArgumentNotNull(getInstance().emailAddress, "Email address cannot be null");
checkArgumentNotNull(getInstance().userRoles, "User roles cannot be null");
return super.build();
}
public B setEmailAddress(String emailAddress) {
getInstance().emailAddress = checkValidEmail(emailAddress);
return thisCastToDerived();
}
public B setRegistryLockEmailAddress(@Nullable String registryLockEmailAddress) {
getInstance().registryLockEmailAddress =
registryLockEmailAddress == null ? null : checkValidEmail(registryLockEmailAddress);
return thisCastToDerived();
}
public B setUserRoles(UserRoles userRoles) {
checkArgumentNotNull(userRoles, "User roles cannot be null");
getInstance().userRoles = userRoles;
return thisCastToDerived();
}
public B removeRegistryLockPassword() {
getInstance().registryLockPasswordHash = null;
getInstance().registryLockPasswordSalt = null;
return thisCastToDerived();
}
public B setRegistryLockPassword(String registryLockPassword) {
checkArgument(
getInstance().hasAnyRegistryLockPermission(), "User has no registry lock permission");
checkArgument(
!getInstance().hasRegistryLockPassword(), "User already has a password, remove it first");
checkArgument(
!isNullOrEmpty(registryLockPassword), "Registry lock password was null or empty");
byte[] salt = SALT_SUPPLIER.get();
getInstance().registryLockPasswordSalt = base64().encode(salt);
getInstance().registryLockPasswordHash = hashPassword(registryLockPassword, salt);
return thisCastToDerived();
}
}
}

View File

@@ -1,186 +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.model.console;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.io.BaseEncoding.base64;
import static google.registry.model.registrar.Registrar.checkValidEmail;
import static google.registry.util.PasswordUtils.SALT_SUPPLIER;
import static google.registry.util.PasswordUtils.hashPassword;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.gson.annotations.Expose;
import google.registry.model.Buildable;
import google.registry.model.UpdateAutoTimestampEntity;
import google.registry.util.PasswordUtils;
import jakarta.persistence.Access;
import jakarta.persistence.AccessType;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.Transient;
import java.util.Optional;
import javax.annotation.Nullable;
/**
* A console user, either a registry employee or a registrar partner.
*
* <p>This class deliberately does not include an {@link Id} so that any foreign-keyed fields can
* refer to the proper parent entity's ID, whether we're storing this in the DB itself or as part of
* another entity.
*/
@Access(AccessType.FIELD)
@Embeddable
@MappedSuperclass
public class UserBase extends UpdateAutoTimestampEntity implements Buildable {
private static final long serialVersionUID = 6936728603828566721L;
/** Email address of the user in question. */
@Transient @Expose String emailAddress;
/** Optional external email address to use for registry lock confirmation emails. */
@Column String registryLockEmailAddress;
/** Roles (which grant permissions) associated with this user. */
@Expose
@Column(nullable = false)
UserRoles userRoles;
/**
* A hashed password that exists iff this contact is registry-lock-enabled. The hash is a base64
* encoded SHA256 string.
*/
String registryLockPasswordHash;
/** Randomly generated hash salt. */
String registryLockPasswordSalt;
/**
* Sets the user email address.
*
* <p>This should only be used for restoring an object being loaded in a PostLoad method
* (effectively, when it is still under construction by Hibernate). In all other cases, the object
* should be regarded as immutable and changes should go through a Builder.
*
* <p>In addition to this special case use, this method must exist to satisfy Hibernate.
*/
void setEmailAddress(String emailAddress) {
this.emailAddress = emailAddress;
}
public String getEmailAddress() {
return emailAddress;
}
public Optional<String> getRegistryLockEmailAddress() {
return Optional.ofNullable(registryLockEmailAddress);
}
public UserRoles getUserRoles() {
return userRoles;
}
public boolean hasRegistryLockPassword() {
return !isNullOrEmpty(registryLockPasswordHash) && !isNullOrEmpty(registryLockPasswordSalt);
}
public boolean verifyRegistryLockPassword(String registryLockPassword) {
if (isNullOrEmpty(registryLockPassword)
|| isNullOrEmpty(registryLockPasswordSalt)
|| isNullOrEmpty(registryLockPasswordHash)) {
return false;
}
return PasswordUtils.verifyPassword(
registryLockPassword, registryLockPasswordHash, registryLockPasswordSalt);
}
/**
* Whether the user has the registry lock permission on any registrar or globally.
*
* <p>If so, they should be allowed to (re)set their registry lock password.
*/
public boolean hasAnyRegistryLockPermission() {
if (userRoles == null) {
return false;
}
if (userRoles.isAdmin() || userRoles.hasGlobalPermission(ConsolePermission.REGISTRY_LOCK)) {
return true;
}
return userRoles.getRegistrarRoles().values().stream()
.anyMatch(role -> role.hasPermission(ConsolePermission.REGISTRY_LOCK));
}
@Override
public Builder<? extends UserBase, ?> asBuilder() {
return new Builder<>(clone(this));
}
/** Builder for constructing immutable {@link UserBase} objects. */
public static class Builder<T extends UserBase, B extends Builder<T, B>>
extends GenericBuilder<T, B> {
public Builder() {}
public Builder(T abstractUser) {
super(abstractUser);
}
@Override
public T build() {
checkArgumentNotNull(getInstance().emailAddress, "Email address cannot be null");
checkArgumentNotNull(getInstance().userRoles, "User roles cannot be null");
return super.build();
}
public B setEmailAddress(String emailAddress) {
getInstance().emailAddress = checkValidEmail(emailAddress);
return thisCastToDerived();
}
public B setRegistryLockEmailAddress(@Nullable String registryLockEmailAddress) {
getInstance().registryLockEmailAddress =
registryLockEmailAddress == null ? null : checkValidEmail(registryLockEmailAddress);
return thisCastToDerived();
}
public B setUserRoles(UserRoles userRoles) {
checkArgumentNotNull(userRoles, "User roles cannot be null");
getInstance().userRoles = userRoles;
return thisCastToDerived();
}
public B removeRegistryLockPassword() {
getInstance().registryLockPasswordHash = null;
getInstance().registryLockPasswordSalt = null;
return thisCastToDerived();
}
public B setRegistryLockPassword(String registryLockPassword) {
checkArgument(
getInstance().hasAnyRegistryLockPermission(), "User has no registry lock permission");
checkArgument(
!getInstance().hasRegistryLockPassword(), "User already has a password, remove it first");
checkArgument(
!isNullOrEmpty(registryLockPassword), "Registry lock password was null or empty");
byte[] salt = SALT_SUPPLIER.get();
getInstance().registryLockPasswordSalt = base64().encode(salt);
getInstance().registryLockPasswordHash = hashPassword(registryLockPassword, salt);
return thisCastToDerived();
}
}
}

View File

@@ -1,85 +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.model.console;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import google.registry.persistence.VKey;
import jakarta.persistence.Access;
import jakarta.persistence.AccessType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Index;
import jakarta.persistence.PostLoad;
import jakarta.persistence.Table;
/**
* A persisted history object representing an update to a User.
*
* <p>In addition to the generic history fields (time, URL, etc.) we also persist a copy of the
* modified User object at this point in time.
*/
@Access(AccessType.FIELD)
@Entity
@Table(indexes = {@Index(columnList = "historyActingUser"), @Index(columnList = "emailAddress")})
public class UserUpdateHistory extends ConsoleUpdateHistory {
UserBase user;
@Column(nullable = false, name = "emailAddress")
String emailAddress;
public UserBase getUser() {
return user;
}
@PostLoad
void postLoad() {
user.setEmailAddress(emailAddress);
}
/** Creates a {@link VKey} instance for this entity. */
@Override
public VKey<UserUpdateHistory> createVKey() {
return VKey.create(UserUpdateHistory.class, getRevisionId());
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
}
/** Builder for the immutable UserUpdateHistory. */
public static class Builder extends ConsoleUpdateHistory.Builder<UserUpdateHistory, Builder> {
public Builder() {}
public Builder(UserUpdateHistory instance) {
super(instance);
}
@Override
public UserUpdateHistory build() {
checkArgumentNotNull(getInstance().user, "User must be specified");
return super.build();
}
public Builder setUser(User user) {
getInstance().user = user;
getInstance().emailAddress = user.getEmailAddress();
return this;
}
}
}

View File

@@ -441,7 +441,8 @@ public class DomainCommand {
private static <T extends EppResource> ImmutableMap<String, VKey<T>> loadByForeignKeysCached(
final Set<String> foreignKeys, final Class<T> clazz, final DateTime now)
throws InvalidReferencesException {
ImmutableMap<String, VKey<T>> fks = ForeignKeyUtils.loadCached(clazz, foreignKeys, now);
ImmutableMap<String, VKey<T>> fks =
ForeignKeyUtils.loadByCacheIfEnabled(clazz, foreignKeys, now);
if (!fks.keySet().equals(foreignKeys)) {
throw new InvalidReferencesException(
clazz, ImmutableSet.copyOf(difference(foreignKeys, fks.keySet())));

View File

@@ -1,4 +1,4 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
// 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.
@@ -14,17 +14,42 @@
package google.registry.model.registrar;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.io.BaseEncoding.base64;
import static google.registry.model.registrar.Registrar.checkValidEmail;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableSortedCopy;
import static google.registry.util.PasswordUtils.SALT_SUPPLIER;
import static google.registry.util.PasswordUtils.hashPassword;
import static java.util.stream.Collectors.joining;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.gson.annotations.Expose;
import google.registry.model.Buildable.GenericBuilder;
import google.registry.model.ImmutableObject;
import google.registry.model.registrar.RegistrarPoc.RegistrarPocId;
import google.registry.model.JsonMapBuilder;
import google.registry.model.Jsonifiable;
import google.registry.model.UnsafeSerializable;
import google.registry.persistence.VKey;
import jakarta.persistence.Access;
import jakarta.persistence.AccessType;
import google.registry.persistence.transaction.QueryComposer;
import google.registry.util.PasswordUtils;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import jakarta.persistence.IdClass;
import java.io.Serializable;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nullable;
/**
* A contact for a Registrar. Note, equality, hashCode and comparable have been overridden to only
@@ -35,21 +60,253 @@ import java.io.Serializable;
* set to true.
*/
@Entity
@IdClass(RegistrarPocId.class)
@Access(AccessType.FIELD)
public class RegistrarPoc extends RegistrarPocBase {
@IdClass(RegistrarPoc.RegistrarPocId.class)
public class RegistrarPoc extends ImmutableObject implements Jsonifiable, UnsafeSerializable {
/**
* Registrar contacts types for partner communication tracking.
*
* <p><b>Note:</b> These types only matter to the registry. They are not meant to be used for
* WHOIS or RDAP results.
*/
public enum Type {
ABUSE("abuse", true),
ADMIN("primary", true),
BILLING("billing", true),
LEGAL("legal", true),
MARKETING("marketing", false),
TECH("technical", true),
WHOIS("whois-inquiry", true);
private final String displayName;
private final boolean required;
public String getDisplayName() {
return displayName;
}
public boolean isRequired() {
return required;
}
Type(String display, boolean required) {
displayName = display;
this.required = required;
}
}
@Expose
@Column(insertable = false, updatable = false)
protected Long id;
/** The name of the contact. */
@Expose String name;
/** The contact email address of the contact. */
@Id @Expose String emailAddress;
@Id @Expose public String registrarId;
/** External email address of this contact used for registry lock confirmations. */
String registryLockEmailAddress;
/** The voice number of the contact. */
@Expose String phoneNumber;
/** The fax number of the contact. */
@Expose String faxNumber;
/**
* Multiple types are used to associate the registrar contact with various mailing groups. This
* data is internal to the registry.
*/
@Enumerated(EnumType.STRING)
@Expose
Set<Type> types;
/**
* Whether this contact is publicly visible in WHOIS registrar query results as an Admin contact.
*/
@Column(nullable = false)
@Expose
boolean visibleInWhoisAsAdmin = false;
/**
* Whether this contact is publicly visible in WHOIS registrar query results as a Technical
* contact.
*/
@Column(nullable = false)
@Expose
boolean visibleInWhoisAsTech = false;
/**
* Whether this contact's phone number and email address is publicly visible in WHOIS domain query
* results as registrar abuse contact info.
*/
@Column(nullable = false)
@Expose
boolean visibleInDomainWhoisAsAbuse = false;
/**
* Whether the contact is allowed to set their registry lock password through the registrar
* console. This will be set to false on contact creation and when the user sets a password.
*/
@Column(nullable = false)
boolean allowedToSetRegistryLockPassword = false;
/**
* A hashed password that exists iff this contact is registry-lock-enabled. The hash is a base64
* encoded SHA256 string.
*/
String registryLockPasswordHash;
/** Randomly generated hash salt. */
String registryLockPasswordSalt;
/**
* Helper to update the contacts associated with a Registrar. This requires querying for the
* existing contacts, deleting existing contacts that are not part of the given {@code contacts}
* set, and then saving the given {@code contacts}.
*
* <p>IMPORTANT NOTE: If you call this method then it is your responsibility to also persist the
* relevant Registrar entity with the {@link Registrar#contactsRequireSyncing} field set to true.
*/
public static void updateContacts(
final Registrar registrar, final ImmutableSet<RegistrarPoc> contacts) {
ImmutableSet<String> emailAddressesToKeep =
contacts.stream().map(RegistrarPoc::getEmailAddress).collect(toImmutableSet());
tm().query(
"DELETE FROM RegistrarPoc WHERE registrarId = :registrarId AND "
+ "emailAddress NOT IN :emailAddressesToKeep")
.setParameter("registrarId", registrar.getRegistrarId())
.setParameter("emailAddressesToKeep", emailAddressesToKeep)
.executeUpdate();
tm().putAll(contacts);
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
@Id
@Access(AccessType.PROPERTY)
@Override
public String getEmailAddress() {
return emailAddress;
}
@Id
@Access(AccessType.PROPERTY)
public String getRegistrarId() {
return registrarId;
public Optional<String> getRegistryLockEmailAddress() {
return Optional.ofNullable(registryLockEmailAddress);
}
public String getPhoneNumber() {
return phoneNumber;
}
public String getFaxNumber() {
return faxNumber;
}
public ImmutableSortedSet<Type> getTypes() {
return nullToEmptyImmutableSortedCopy(types);
}
public boolean getVisibleInWhoisAsAdmin() {
return visibleInWhoisAsAdmin;
}
public boolean getVisibleInWhoisAsTech() {
return visibleInWhoisAsTech;
}
public boolean getVisibleInDomainWhoisAsAbuse() {
return visibleInDomainWhoisAsAbuse;
}
public Builder<? extends RegistrarPoc, ?> asBuilder() {
return new Builder<>(clone(this));
}
public boolean isAllowedToSetRegistryLockPassword() {
return allowedToSetRegistryLockPassword;
}
public boolean isRegistryLockAllowed() {
return !isNullOrEmpty(registryLockPasswordHash) && !isNullOrEmpty(registryLockPasswordSalt);
}
public boolean verifyRegistryLockPassword(String registryLockPassword) {
if (isNullOrEmpty(registryLockPassword)
|| isNullOrEmpty(registryLockPasswordSalt)
|| isNullOrEmpty(registryLockPasswordHash)) {
return false;
}
return PasswordUtils.verifyPassword(
registryLockPassword, registryLockPasswordHash, registryLockPasswordSalt);
}
/**
* Returns a string representation that's human friendly.
*
* <p>The output will look something like this:
*
* <pre>{@code
* Some Person
* person@example.com
* Tel: +1.2125650666
* Types: [ADMIN, WHOIS]
* Visible in WHOIS as Admin contact: Yes
* Visible in WHOIS as Technical contact: No
* Registrar-Console access: Yes
* Login Email Address: person@registry.example
* }</pre>
*/
public String toStringMultilinePlainText() {
StringBuilder result = new StringBuilder(256);
result.append(getName()).append('\n');
result.append(getEmailAddress()).append('\n');
if (phoneNumber != null) {
result.append("Tel: ").append(getPhoneNumber()).append('\n');
}
if (faxNumber != null) {
result.append("Fax: ").append(getFaxNumber()).append('\n');
}
result.append("Types: ").append(getTypes()).append('\n');
result
.append("Visible in registrar WHOIS query as Admin contact: ")
.append(getVisibleInWhoisAsAdmin() ? "Yes" : "No")
.append('\n');
result
.append("Visible in registrar WHOIS query as Technical contact: ")
.append(getVisibleInWhoisAsTech() ? "Yes" : "No")
.append('\n');
result
.append(
"Phone number and email visible in domain WHOIS query as "
+ "Registrar Abuse contact info: ")
.append(getVisibleInDomainWhoisAsAbuse() ? "Yes" : "No")
.append('\n');
return result.toString();
}
@Override
public Map<String, Object> toJsonMap() {
return new JsonMapBuilder()
.put("name", name)
.put("emailAddress", emailAddress)
.put("registryLockEmailAddress", registryLockEmailAddress)
.put("phoneNumber", phoneNumber)
.put("faxNumber", faxNumber)
.put("types", getTypes().stream().map(Object::toString).collect(joining(",")))
.put("visibleInWhoisAsAdmin", visibleInWhoisAsAdmin)
.put("visibleInWhoisAsTech", visibleInWhoisAsTech)
.put("visibleInDomainWhoisAsAbuse", visibleInDomainWhoisAsAbuse)
.put("allowedToSetRegistryLockPassword", allowedToSetRegistryLockPassword)
.put("registryLockAllowed", isRegistryLockAllowed())
.put("id", getId())
.build();
}
@Override
@@ -57,9 +314,130 @@ public class RegistrarPoc extends RegistrarPocBase {
return VKey.create(RegistrarPoc.class, new RegistrarPocId(emailAddress, registrarId));
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
/**
* These methods set the email address and registrar ID
*
* <p>This should only be used for restoring the fields of an object being loaded in a PostLoad
* method (effectively, when it is still under construction by Hibernate). In all other cases, the
* object should be regarded as immutable and changes should go through a Builder.
*
* <p>In addition to this special case use, this method must exist to satisfy Hibernate.
*/
public void setEmailAddress(String emailAddress) {
this.emailAddress = emailAddress;
}
public void setRegistrarId(String registrarId) {
this.registrarId = registrarId;
}
/** A builder for constructing a {@link RegistrarPoc}, since it is immutable. */
public static class Builder<T extends RegistrarPoc, B extends Builder<T, B>>
extends GenericBuilder<T, B> {
public Builder() {}
protected Builder(T instance) {
super(instance);
}
/** Build the registrar, nullifying empty fields. */
@Override
public T build() {
checkNotNull(getInstance().registrarId, "Registrar ID cannot be null");
checkValidEmail(getInstance().emailAddress);
// Check allowedToSetRegistryLockPassword here because if we want to allow the user to set
// a registry lock password, we must also set up the correct registry lock email concurrently
// or beforehand.
if (getInstance().allowedToSetRegistryLockPassword) {
checkArgument(
!isNullOrEmpty(getInstance().registryLockEmailAddress),
"Registry lock email must not be null if allowing registry lock access");
}
return cloneEmptyToNull(super.build());
}
public B setName(String name) {
getInstance().name = name;
return thisCastToDerived();
}
public B setEmailAddress(String emailAddress) {
getInstance().emailAddress = emailAddress;
return thisCastToDerived();
}
public B setRegistryLockEmailAddress(@Nullable String registryLockEmailAddress) {
getInstance().registryLockEmailAddress = registryLockEmailAddress;
return thisCastToDerived();
}
public B setPhoneNumber(String phoneNumber) {
getInstance().phoneNumber = phoneNumber;
return thisCastToDerived();
}
public B setRegistrarId(String registrarId) {
getInstance().registrarId = registrarId;
return thisCastToDerived();
}
public B setRegistrar(Registrar registrar) {
getInstance().registrarId = registrar.getRegistrarId();
return thisCastToDerived();
}
public B setFaxNumber(String faxNumber) {
getInstance().faxNumber = faxNumber;
return thisCastToDerived();
}
public B setTypes(Iterable<Type> types) {
getInstance().types = ImmutableSet.copyOf(types);
return thisCastToDerived();
}
public B setVisibleInWhoisAsAdmin(boolean visible) {
getInstance().visibleInWhoisAsAdmin = visible;
return thisCastToDerived();
}
public B setVisibleInWhoisAsTech(boolean visible) {
getInstance().visibleInWhoisAsTech = visible;
return thisCastToDerived();
}
public B setVisibleInDomainWhoisAsAbuse(boolean visible) {
getInstance().visibleInDomainWhoisAsAbuse = visible;
return thisCastToDerived();
}
public B setAllowedToSetRegistryLockPassword(boolean allowedToSetRegistryLockPassword) {
if (allowedToSetRegistryLockPassword) {
getInstance().registryLockPasswordSalt = null;
getInstance().registryLockPasswordHash = null;
}
getInstance().allowedToSetRegistryLockPassword = allowedToSetRegistryLockPassword;
return thisCastToDerived();
}
public B setRegistryLockPassword(String registryLockPassword) {
checkArgument(
getInstance().allowedToSetRegistryLockPassword,
"Not allowed to set registry lock password for this contact");
checkArgument(
!isNullOrEmpty(registryLockPassword), "Registry lock password was null or empty");
byte[] salt = SALT_SUPPLIER.get();
getInstance().registryLockPasswordSalt = base64().encode(salt);
getInstance().registryLockPasswordHash = hashPassword(registryLockPassword, salt);
getInstance().allowedToSetRegistryLockPassword = false;
return thisCastToDerived();
}
}
public static ImmutableList<RegistrarPoc> loadForRegistrar(String registrarId) {
return tm().createQueryComposer(RegistrarPoc.class)
.where("registrarId", QueryComposer.Comparator.EQ, registrarId)
.list();
}
/** Class to represent the composite primary key for {@link RegistrarPoc} entity. */
@@ -90,13 +468,4 @@ public class RegistrarPoc extends RegistrarPocBase {
return registrarId;
}
}
public static class Builder extends RegistrarPocBase.Builder<RegistrarPoc, Builder> {
public Builder() {}
public Builder(RegistrarPoc registrarPoc) {
super(registrarPoc);
}
}
}

View File

@@ -1,425 +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.model.registrar;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.io.BaseEncoding.base64;
import static google.registry.model.registrar.RegistrarBase.checkValidEmail;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableSortedCopy;
import static google.registry.util.PasswordUtils.SALT_SUPPLIER;
import static google.registry.util.PasswordUtils.hashPassword;
import static java.util.stream.Collectors.joining;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.gson.annotations.Expose;
import google.registry.model.Buildable.GenericBuilder;
import google.registry.model.ImmutableObject;
import google.registry.model.JsonMapBuilder;
import google.registry.model.Jsonifiable;
import google.registry.model.UnsafeSerializable;
import google.registry.util.PasswordUtils;
import jakarta.persistence.Access;
import jakarta.persistence.AccessType;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.Transient;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nullable;
/**
* A contact for a Registrar. Note, equality, hashCode and comparable have been overridden to only
* enable key equality.
*
* <p>IMPORTANT NOTE: Any time that you change, update, or delete RegistrarContact entities, you
* *MUST* also modify the persisted Registrar entity with {@link Registrar#contactsRequireSyncing}
* set to true.
*
* <p>This class deliberately does not include an {@link Id} so that any foreign-keyed fields can
* refer to the proper parent entity's ID, whether we're storing this in the DB itself or as part of
* another entity.
*/
@Access(AccessType.FIELD)
@Embeddable
@MappedSuperclass
public class RegistrarPocBase extends ImmutableObject implements Jsonifiable, UnsafeSerializable {
/**
* Registrar contacts types for partner communication tracking.
*
* <p><b>Note:</b> These types only matter to the registry. They are not meant to be used for
* WHOIS or RDAP results.
*/
public enum Type {
ABUSE("abuse", true),
ADMIN("primary", true),
BILLING("billing", true),
LEGAL("legal", true),
MARKETING("marketing", false),
TECH("technical", true),
WHOIS("whois-inquiry", true);
private final String displayName;
private final boolean required;
public String getDisplayName() {
return displayName;
}
public boolean isRequired() {
return required;
}
Type(String display, boolean required) {
displayName = display;
this.required = required;
}
}
/** The name of the contact. */
@Expose String name;
/** The contact email address of the contact. */
@Expose @Transient String emailAddress;
@Expose @Transient public String registrarId;
/** External email address of this contact used for registry lock confirmations. */
String registryLockEmailAddress;
/** The voice number of the contact. */
@Expose String phoneNumber;
/** The fax number of the contact. */
@Expose String faxNumber;
/**
* Multiple types are used to associate the registrar contact with various mailing groups. This
* data is internal to the registry.
*/
@Enumerated(EnumType.STRING)
@Expose
Set<Type> types;
/**
* Whether this contact is publicly visible in WHOIS registrar query results as an Admin contact.
*/
@Column(nullable = false)
@Expose
boolean visibleInWhoisAsAdmin = false;
/**
* Whether this contact is publicly visible in WHOIS registrar query results as a Technical
* contact.
*/
@Column(nullable = false)
@Expose
boolean visibleInWhoisAsTech = false;
/**
* Whether this contact's phone number and email address is publicly visible in WHOIS domain query
* results as registrar abuse contact info.
*/
@Column(nullable = false)
@Expose
boolean visibleInDomainWhoisAsAbuse = false;
/**
* Whether the contact is allowed to set their registry lock password through the registrar
* console. This will be set to false on contact creation and when the user sets a password.
*/
@Column(nullable = false)
boolean allowedToSetRegistryLockPassword = false;
/**
* A hashed password that exists iff this contact is registry-lock-enabled. The hash is a base64
* encoded SHA256 string.
*/
String registryLockPasswordHash;
/** Randomly generated hash salt. */
String registryLockPasswordSalt;
/**
* Helper to update the contacts associated with a Registrar. This requires querying for the
* existing contacts, deleting existing contacts that are not part of the given {@code contacts}
* set, and then saving the given {@code contacts}.
*
* <p>IMPORTANT NOTE: If you call this method then it is your responsibility to also persist the
* relevant Registrar entity with the {@link Registrar#contactsRequireSyncing} field set to true.
*/
public static void updateContacts(
final Registrar registrar, final ImmutableSet<RegistrarPoc> contacts) {
ImmutableSet<String> emailAddressesToKeep =
contacts.stream().map(RegistrarPoc::getEmailAddress).collect(toImmutableSet());
tm().query(
"DELETE FROM RegistrarPoc WHERE registrarId = :registrarId AND "
+ "emailAddress NOT IN :emailAddressesToKeep")
.setParameter("registrarId", registrar.getRegistrarId())
.setParameter("emailAddressesToKeep", emailAddressesToKeep)
.executeUpdate();
tm().putAll(contacts);
}
public String getName() {
return name;
}
public String getEmailAddress() {
return emailAddress;
}
public Optional<String> getRegistryLockEmailAddress() {
return Optional.ofNullable(registryLockEmailAddress);
}
public String getPhoneNumber() {
return phoneNumber;
}
public String getFaxNumber() {
return faxNumber;
}
public ImmutableSortedSet<Type> getTypes() {
return nullToEmptyImmutableSortedCopy(types);
}
public boolean getVisibleInWhoisAsAdmin() {
return visibleInWhoisAsAdmin;
}
public boolean getVisibleInWhoisAsTech() {
return visibleInWhoisAsTech;
}
public boolean getVisibleInDomainWhoisAsAbuse() {
return visibleInDomainWhoisAsAbuse;
}
public Builder<? extends RegistrarPocBase, ?> asBuilder() {
return new Builder<>(clone(this));
}
public boolean isAllowedToSetRegistryLockPassword() {
return allowedToSetRegistryLockPassword;
}
public boolean isRegistryLockAllowed() {
return !isNullOrEmpty(registryLockPasswordHash) && !isNullOrEmpty(registryLockPasswordSalt);
}
public boolean verifyRegistryLockPassword(String registryLockPassword) {
if (isNullOrEmpty(registryLockPassword)
|| isNullOrEmpty(registryLockPasswordSalt)
|| isNullOrEmpty(registryLockPasswordHash)) {
return false;
}
return PasswordUtils.verifyPassword(
registryLockPassword, registryLockPasswordHash, registryLockPasswordSalt);
}
/**
* Returns a string representation that's human friendly.
*
* <p>The output will look something like this:
*
* <pre>{@code
* Some Person
* person@example.com
* Tel: +1.2125650666
* Types: [ADMIN, WHOIS]
* Visible in WHOIS as Admin contact: Yes
* Visible in WHOIS as Technical contact: No
* Registrar-Console access: Yes
* Login Email Address: person@registry.example
* }</pre>
*/
public String toStringMultilinePlainText() {
StringBuilder result = new StringBuilder(256);
result.append(getName()).append('\n');
result.append(getEmailAddress()).append('\n');
if (phoneNumber != null) {
result.append("Tel: ").append(getPhoneNumber()).append('\n');
}
if (faxNumber != null) {
result.append("Fax: ").append(getFaxNumber()).append('\n');
}
result.append("Types: ").append(getTypes()).append('\n');
result
.append("Visible in registrar WHOIS query as Admin contact: ")
.append(getVisibleInWhoisAsAdmin() ? "Yes" : "No")
.append('\n');
result
.append("Visible in registrar WHOIS query as Technical contact: ")
.append(getVisibleInWhoisAsTech() ? "Yes" : "No")
.append('\n');
result
.append(
"Phone number and email visible in domain WHOIS query as "
+ "Registrar Abuse contact info: ")
.append(getVisibleInDomainWhoisAsAbuse() ? "Yes" : "No")
.append('\n');
return result.toString();
}
@Override
public Map<String, Object> toJsonMap() {
return new JsonMapBuilder()
.put("name", name)
.put("emailAddress", emailAddress)
.put("registryLockEmailAddress", registryLockEmailAddress)
.put("phoneNumber", phoneNumber)
.put("faxNumber", faxNumber)
.put("types", getTypes().stream().map(Object::toString).collect(joining(",")))
.put("visibleInWhoisAsAdmin", visibleInWhoisAsAdmin)
.put("visibleInWhoisAsTech", visibleInWhoisAsTech)
.put("visibleInDomainWhoisAsAbuse", visibleInDomainWhoisAsAbuse)
.put("allowedToSetRegistryLockPassword", allowedToSetRegistryLockPassword)
.put("registryLockAllowed", isRegistryLockAllowed())
.build();
}
/**
* These methods set the email address and registrar ID
*
* <p>This should only be used for restoring the fields of an object being loaded in a PostLoad
* method (effectively, when it is still under construction by Hibernate). In all other cases, the
* object should be regarded as immutable and changes should go through a Builder.
*
* <p>In addition to this special case use, this method must exist to satisfy Hibernate.
*/
public void setEmailAddress(String emailAddress) {
this.emailAddress = emailAddress;
}
public void setRegistrarId(String registrarId) {
this.registrarId = registrarId;
}
/** A builder for constructing a {@link RegistrarPoc}, since it is immutable. */
public static class Builder<T extends RegistrarPocBase, B extends Builder<T, B>>
extends GenericBuilder<T, B> {
public Builder() {}
protected Builder(T instance) {
super(instance);
}
/** Build the registrar, nullifying empty fields. */
@Override
public T build() {
checkNotNull(getInstance().registrarId, "Registrar ID cannot be null");
checkValidEmail(getInstance().emailAddress);
// Check allowedToSetRegistryLockPassword here because if we want to allow the user to set
// a registry lock password, we must also set up the correct registry lock email concurrently
// or beforehand.
if (getInstance().allowedToSetRegistryLockPassword) {
checkArgument(
!isNullOrEmpty(getInstance().registryLockEmailAddress),
"Registry lock email must not be null if allowing registry lock access");
}
return cloneEmptyToNull(super.build());
}
public B setName(String name) {
getInstance().name = name;
return thisCastToDerived();
}
public B setEmailAddress(String emailAddress) {
getInstance().emailAddress = emailAddress;
return thisCastToDerived();
}
public B setRegistryLockEmailAddress(@Nullable String registryLockEmailAddress) {
getInstance().registryLockEmailAddress = registryLockEmailAddress;
return thisCastToDerived();
}
public B setPhoneNumber(String phoneNumber) {
getInstance().phoneNumber = phoneNumber;
return thisCastToDerived();
}
public B setRegistrarId(String registrarId) {
getInstance().registrarId = registrarId;
return thisCastToDerived();
}
public B setRegistrar(Registrar registrar) {
getInstance().registrarId = registrar.getRegistrarId();
return thisCastToDerived();
}
public B setFaxNumber(String faxNumber) {
getInstance().faxNumber = faxNumber;
return thisCastToDerived();
}
public B setTypes(Iterable<Type> types) {
getInstance().types = ImmutableSet.copyOf(types);
return thisCastToDerived();
}
public B setVisibleInWhoisAsAdmin(boolean visible) {
getInstance().visibleInWhoisAsAdmin = visible;
return thisCastToDerived();
}
public B setVisibleInWhoisAsTech(boolean visible) {
getInstance().visibleInWhoisAsTech = visible;
return thisCastToDerived();
}
public B setVisibleInDomainWhoisAsAbuse(boolean visible) {
getInstance().visibleInDomainWhoisAsAbuse = visible;
return thisCastToDerived();
}
public B setAllowedToSetRegistryLockPassword(boolean allowedToSetRegistryLockPassword) {
if (allowedToSetRegistryLockPassword) {
getInstance().registryLockPasswordSalt = null;
getInstance().registryLockPasswordHash = null;
}
getInstance().allowedToSetRegistryLockPassword = allowedToSetRegistryLockPassword;
return thisCastToDerived();
}
public B setRegistryLockPassword(String registryLockPassword) {
checkArgument(
getInstance().allowedToSetRegistryLockPassword,
"Not allowed to set registry lock password for this contact");
checkArgument(
!isNullOrEmpty(registryLockPassword), "Registry lock password was null or empty");
byte[] salt = SALT_SUPPLIER.get();
getInstance().registryLockPasswordSalt = base64().encode(salt);
getInstance().registryLockPasswordHash = hashPassword(registryLockPassword, salt);
getInstance().allowedToSetRegistryLockPassword = false;
return thisCastToDerived();
}
}
}

View File

@@ -38,10 +38,8 @@ import google.registry.dns.PublishDnsUpdatesAction;
import google.registry.dns.ReadDnsRefreshRequestsAction;
import google.registry.dns.RefreshDnsAction;
import google.registry.dns.RefreshDnsOnHostRenameAction;
import google.registry.dns.writer.VoidDnsWriterModule;
import google.registry.dns.writer.clouddns.CloudDnsWriterModule;
import google.registry.dns.writer.DnsWritersModule;
import google.registry.dns.writer.dnsupdate.DnsUpdateConfigModule;
import google.registry.dns.writer.dnsupdate.DnsUpdateWriterModule;
import google.registry.export.ExportDomainListsAction;
import google.registry.export.ExportPremiumTermsAction;
import google.registry.export.ExportReservedTermsAction;
@@ -140,14 +138,13 @@ import google.registry.whois.WhoisModule;
BatchModule.class,
BillingModule.class,
CheckApiModule.class,
CloudDnsWriterModule.class,
ConsoleModule.class,
CronModule.class,
CustomLogicModule.class,
DnsCountQueryCoordinatorModule.class,
DnsModule.class,
DnsUpdateConfigModule.class,
DnsUpdateWriterModule.class,
DnsWritersModule.class,
EppTlsModule.class,
EppToolModule.class,
IcannReportingModule.class,
@@ -160,7 +157,6 @@ import google.registry.whois.WhoisModule;
Spec11Module.class,
TmchModule.class,
ToolsServerModule.class,
VoidDnsWriterModule.class,
WhiteboxModule.class,
WhoisModule.class,
})

View File

@@ -22,7 +22,6 @@ import google.registry.bigquery.BigqueryModule;
import google.registry.config.CloudTasksUtilsModule;
import google.registry.config.CredentialModule;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.dns.writer.VoidDnsWriterModule;
import google.registry.export.DriveModule;
import google.registry.export.sheet.SheetsServiceModule;
import google.registry.flows.ServerTridProviderModule;
@@ -73,7 +72,6 @@ import jakarta.inject.Singleton;
SheetsServiceModule.class,
StackdriverModule.class,
UrlConnectionServiceModule.class,
VoidDnsWriterModule.class,
UtilsModule.class
})
interface BackendComponent {

View File

@@ -34,10 +34,8 @@ import google.registry.dns.PublishDnsUpdatesAction;
import google.registry.dns.ReadDnsRefreshRequestsAction;
import google.registry.dns.RefreshDnsAction;
import google.registry.dns.RefreshDnsOnHostRenameAction;
import google.registry.dns.writer.VoidDnsWriterModule;
import google.registry.dns.writer.clouddns.CloudDnsWriterModule;
import google.registry.dns.writer.DnsWritersModule;
import google.registry.dns.writer.dnsupdate.DnsUpdateConfigModule;
import google.registry.dns.writer.dnsupdate.DnsUpdateWriterModule;
import google.registry.export.ExportDomainListsAction;
import google.registry.export.ExportPremiumTermsAction;
import google.registry.export.ExportReservedTermsAction;
@@ -82,13 +80,12 @@ import google.registry.tmch.TmchSmdrlAction;
modules = {
BatchModule.class,
BillingModule.class,
CloudDnsWriterModule.class,
CronModule.class,
CustomLogicModule.class,
DnsCountQueryCoordinatorModule.class,
DnsModule.class,
DnsUpdateConfigModule.class,
DnsUpdateWriterModule.class,
DnsWritersModule.class,
IcannReportingModule.class,
RdeModule.class,
ReportingModule.class,
@@ -96,7 +93,6 @@ import google.registry.tmch.TmchSmdrlAction;
SheetModule.class,
Spec11Module.class,
TmchModule.class,
VoidDnsWriterModule.class,
WhiteboxModule.class,
})
public interface BackendRequestComponent {

View File

@@ -276,10 +276,6 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
}
T result = work.call();
txn.commit();
long duration = clock.nowUtc().getMillis() - txnInfo.transactionTime.getMillis();
if (duration >= 100) {
logger.atInfo().log("Transaction duration: %d milliseconds", duration);
}
return result;
} catch (Throwable e) {
// Catch a Throwable here so even Errors would lead to a rollback.

View File

@@ -336,7 +336,7 @@ abstract class AbstractJsonableObject implements Jsonable {
// According to RFC 9083 section 3, the syntax of dates and times is defined in RFC3339.
//
// According to RFC3339, we should use ISO8601, which is what DateTime.toString does!
return new JsonPrimitive(((DateTime) object).toString());
return new JsonPrimitive(object.toString());
}
if (object == null) {
return JsonNull.INSTANCE;

View File

@@ -24,10 +24,14 @@ import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.EppResource;
import google.registry.model.registrar.Registrar;
@@ -41,6 +45,7 @@ import google.registry.request.HttpException;
import google.registry.request.Parameter;
import google.registry.request.RequestMethod;
import google.registry.request.RequestPath;
import google.registry.request.RequestUrl;
import google.registry.request.Response;
import google.registry.util.Clock;
import jakarta.inject.Inject;
@@ -75,6 +80,7 @@ public abstract class RdapActionBase implements Runnable {
@Inject Response response;
@Inject @RequestMethod Action.Method requestMethod;
@Inject @RequestPath String requestPath;
@Inject @RequestUrl String requestUrl;
@Inject RdapAuthorization rdapAuthorization;
@Inject RdapJsonFormatter rdapJsonFormatter;
@Inject @Parameter("includeDeleted") Optional<Boolean> includeDeletedParam;
@@ -198,7 +204,9 @@ public abstract class RdapActionBase implements Runnable {
TopLevelReplyObject topLevelObject =
TopLevelReplyObject.create(replyObject, rdapJsonFormatter.createTosNotice());
Gson gson = formatOutputParam.orElse(false) ? FORMATTED_OUTPUT_GSON : GSON;
response.setPayload(gson.toJson(topLevelObject.toJson()));
JsonObject jsonResult = topLevelObject.toJson();
addLinkValuesRecursively(jsonResult);
response.setPayload(gson.toJson(jsonResult));
}
/**
@@ -264,4 +272,34 @@ public abstract class RdapActionBase implements Runnable {
return rdapJsonFormatter.getRequestTime();
}
/**
* Adds a request-referencing "value" to each link object.
*
* <p>This is the "context URI" as described in RFC 8288. Basically, this contains a reference to
* the request URL that generated this RDAP response.
*
* <p>This is required per the RDAP February 2024 response profile sections 2.6.3 and 2.10, and
* the technical implementation guide sections 3.2 and 3.3.2.
*
* <p>We must do this here (instead of where the links are generated) because many of the links
* (e.g. terms of service) are static constants, and thus cannot by default know what the request
* URL was.
*/
private void addLinkValuesRecursively(JsonElement jsonElement) {
if (jsonElement instanceof JsonArray jsonArray) {
jsonArray.forEach(this::addLinkValuesRecursively);
} else if (jsonElement instanceof JsonObject jsonObject) {
if (jsonObject.get("links") instanceof JsonArray linksArray) {
addLinkValues(linksArray);
}
jsonObject.entrySet().forEach(entry -> addLinkValuesRecursively(entry.getValue()));
}
}
private void addLinkValues(JsonArray linksArray) {
Streams.stream(linksArray)
.map(JsonElement::getAsJsonObject)
.filter(o -> !o.has("value"))
.forEach(o -> o.addProperty("value", requestUrl));
}
}

View File

@@ -15,7 +15,7 @@
package google.registry.rdap;
import static google.registry.flows.domain.DomainFlowUtils.validateDomainName;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCache;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
@@ -65,7 +65,7 @@ public class RdapDomainAction extends RdapActionBase {
}
// The query string is not used; the RDAP syntax is /rdap/domain/mydomain.com.
Optional<Domain> domain =
loadByForeignKeyCached(
loadByForeignKeyByCache(
Domain.class,
pathSearchString,
shouldIncludeDeleted() ? START_OF_TIME : rdapJsonFormatter.getRequestTime());

View File

@@ -15,7 +15,7 @@
package google.registry.rdap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCache;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
@@ -184,7 +184,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
private DomainSearchResponse searchByDomainNameWithoutWildcard(
final RdapSearchPattern partialStringQuery) {
Optional<Domain> domain =
loadByForeignKeyCached(
loadByForeignKeyByCache(
Domain.class, partialStringQuery.getInitialString(), getRequestTime());
return makeSearchResults(
shouldBeVisible(domain) ? ImmutableList.of(domain.get()) : ImmutableList.of());
@@ -339,7 +339,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
Optional<String> desiredRegistrar = getDesiredRegistrar();
if (desiredRegistrar.isPresent()) {
Optional<Host> host =
loadByForeignKeyCached(
loadByForeignKeyByCache(
Host.class,
partialStringQuery.getInitialString(),
shouldIncludeDeleted() ? START_OF_TIME : getRequestTime());
@@ -364,7 +364,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
// through the subordinate hosts. This is more efficient, and lets us permit wildcard searches
// with no initial string.
Domain domain =
loadByForeignKeyCached(
loadByForeignKeyByCache(
Domain.class,
partialStringQuery.getSuffix(),
shouldIncludeDeleted() ? START_OF_TIME : getRequestTime())
@@ -381,7 +381,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
if (partialStringQuery.matches(fqhn)) {
if (desiredRegistrar.isPresent()) {
Optional<Host> host =
loadByForeignKeyCached(
loadByForeignKeyByCache(
Host.class, fqhn, shouldIncludeDeleted() ? START_OF_TIME : getRequestTime());
if (host.isPresent()
&& desiredRegistrar

View File

@@ -44,7 +44,7 @@ public class RdapIcannStandardInformation {
+ " https://icann.org/epp")
.addLink(
Link.builder()
.setRel("alternate")
.setRel("glossary")
.setHref("https://icann.org/epp")
.setType("text/html")
.build())
@@ -57,7 +57,7 @@ public class RdapIcannStandardInformation {
.setDescription("URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf")
.addLink(
Link.builder()
.setRel("alternate")
.setRel("help")
.setHref("https://icann.org/wicf")
.setType("text/html")
.build())

View File

@@ -23,20 +23,21 @@ import static google.registry.model.EppResourceUtils.isLinked;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import static google.registry.util.CollectionUtils.union;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.InetAddresses;
import com.google.gson.JsonArray;
import google.registry.config.RegistryConfig;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.CacheUtils;
import google.registry.model.EppResource;
import google.registry.model.adapters.EnumToAttributeAdapter.EppEnum;
import google.registry.model.contact.Contact;
@@ -73,13 +74,11 @@ import google.registry.rdap.RdapObjectClasses.VcardArray;
import google.registry.request.RequestServerName;
import google.registry.util.Clock;
import jakarta.inject.Inject;
import jakarta.persistence.Entity;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.URI;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
@@ -103,6 +102,16 @@ public class RdapJsonFormatter {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@VisibleForTesting
record HistoryTimeAndRegistrar(DateTime modificationTime, String registrarId) {}
private static final LoadingCache<String, ImmutableMap<EventAction, HistoryTimeAndRegistrar>>
DOMAIN_HISTORIES_BY_REPO_ID =
CacheUtils.newCacheBuilder(RegistryConfig.getEppResourceCachingDuration())
// Cache more than the EPP resource cache because we're only caching small objects
.maximumSize(RegistryConfig.getEppResourceMaxCachedEntries() * 4L)
.build(repoId -> getLastHistoryByType(repoId, Domain.class));
private DateTime requestTime = null;
@Inject
@@ -271,7 +280,7 @@ public class RdapJsonFormatter {
URI htmlUri = htmlBaseURI.resolve(rdapTosStaticUrl);
noticeBuilder.addLink(
Link.builder()
.setRel("alternate")
.setRel("terms-of-service")
.setHref(htmlUri.toString())
.setType("text/html")
.build());
@@ -740,7 +749,7 @@ public class RdapJsonFormatter {
//
if (outputDataType != OutputDataType.SUMMARY) {
ImmutableList<RdapContactEntity> registrarContacts =
registrar.getContacts().stream()
registrar.getContactsFromReplica().stream()
.map(RdapJsonFormatter::makeRdapJsonForRegistrarContact)
.filter(Optional::isPresent)
.map(Optional::get)
@@ -860,8 +869,18 @@ public class RdapJsonFormatter {
}
@VisibleForTesting
ImmutableMap<EventAction, HistoryEntry> getLastHistoryEntryByType(EppResource resource) {
HashMap<EventAction, HistoryEntry> lastEntryOfType = Maps.newHashMap();
static ImmutableMap<EventAction, HistoryTimeAndRegistrar> getLastHistoryByType(
EppResource eppResource) {
if (eppResource instanceof Domain) {
return DOMAIN_HISTORIES_BY_REPO_ID.get(eppResource.getRepoId());
}
return getLastHistoryByType(eppResource.getRepoId(), eppResource.getClass());
}
private static ImmutableMap<EventAction, HistoryTimeAndRegistrar> getLastHistoryByType(
String repoId, Class<? extends EppResource> resourceType) {
ImmutableMap.Builder<EventAction, HistoryTimeAndRegistrar> lastEntryOfType =
new ImmutableMap.Builder<>();
// Events (such as transfer, but also create) can appear multiple times. We only want the last
// time they appeared.
//
@@ -873,35 +892,33 @@ public class RdapJsonFormatter {
// 2.3.2.3 An event of *eventAction* type *transfer*, with the last date and time that the
// domain was transferred. The event of *eventAction* type *transfer* MUST be omitted if the
// domain name has not been transferred since it was created.
VKey<? extends EppResource> resourceVkey = resource.createVKey();
Class<? extends HistoryEntry> historyClass =
HistoryEntryDao.getHistoryClassFromParent(resourceVkey.getKind());
String entityName = historyClass.getAnnotation(Entity.class).name();
if (Strings.isNullOrEmpty(entityName)) {
entityName = historyClass.getSimpleName();
}
String entityName = HistoryEntryDao.getHistoryClassFromParent(resourceType).getSimpleName();
String jpql =
GET_LAST_HISTORY_BY_TYPE_JPQL_TEMPLATE
.replace("%entityName%", entityName)
.replace("%repoIdValue%", resourceVkey.getKey().toString());
Iterable<HistoryEntry> historyEntries =
replicaTm()
.transact(
() ->
replicaTm()
.getEntityManager()
.createQuery(jpql, HistoryEntry.class)
.getResultList());
for (HistoryEntry historyEntry : historyEntries) {
EventAction rdapEventAction =
HISTORY_ENTRY_TYPE_TO_RDAP_EVENT_ACTION_MAP.get(historyEntry.getType());
// Only save the historyEntries if this is a type we care about.
if (rdapEventAction == null) {
continue;
}
lastEntryOfType.put(rdapEventAction, historyEntry);
}
return ImmutableMap.copyOf(lastEntryOfType);
.replace("%repoIdValue%", repoId);
replicaTm()
.transact(
() ->
replicaTm()
.getEntityManager()
.createQuery(jpql, HistoryEntry.class)
.getResultStream()
.forEach(
historyEntry -> {
EventAction rdapEventAction =
HISTORY_ENTRY_TYPE_TO_RDAP_EVENT_ACTION_MAP.get(
historyEntry.getType());
// Only save the entries if this is a type we care about.
if (rdapEventAction != null) {
lastEntryOfType.put(
rdapEventAction,
new HistoryTimeAndRegistrar(
historyEntry.getModificationTime(),
historyEntry.getRegistrarId()));
}
}));
return lastEntryOfType.buildKeepingLast();
}
/**
@@ -915,7 +932,8 @@ public class RdapJsonFormatter {
* that we don't need to load HistoryEntries for "summary" responses).
*/
private ImmutableList<Event> makeOptionalEvents(EppResource resource) {
ImmutableMap<EventAction, HistoryEntry> lastEntryOfType = getLastHistoryEntryByType(resource);
ImmutableMap<EventAction, HistoryTimeAndRegistrar> lastHistoryOfType =
getLastHistoryByType(resource);
ImmutableList.Builder<Event> eventsBuilder = new ImmutableList.Builder<>();
DateTime creationTime = resource.getCreationTime();
DateTime lastChangeTime =
@@ -923,12 +941,12 @@ public class RdapJsonFormatter {
// The order of the elements is stable - it's the order in which the enum elements are defined
// in EventAction
for (EventAction rdapEventAction : EventAction.values()) {
HistoryEntry historyEntry = lastEntryOfType.get(rdapEventAction);
HistoryTimeAndRegistrar historyTimeAndRegistrar = lastHistoryOfType.get(rdapEventAction);
// Check if there was any entry of this type
if (historyEntry == null) {
if (historyTimeAndRegistrar == null) {
continue;
}
DateTime modificationTime = historyEntry.getModificationTime();
DateTime modificationTime = historyTimeAndRegistrar.modificationTime();
// We will ignore all events that happened before the "creation time", since these events are
// from a "previous incarnation of the domain" (for a domain that was owned by someone,
// deleted, and then bought by someone else)
@@ -938,7 +956,7 @@ public class RdapJsonFormatter {
eventsBuilder.add(
Event.builder()
.setEventAction(rdapEventAction)
.setEventActor(historyEntry.getRegistrarId())
.setEventActor(historyTimeAndRegistrar.registrarId())
.setEventDate(modificationTime)
.build());
// The last change time might not be the lastEppUpdateTime, since some changes happen without
@@ -951,21 +969,16 @@ public class RdapJsonFormatter {
// The event of eventAction type last changed MUST be omitted if the domain name has not been
// updated since it was created
if (lastChangeTime.isAfter(creationTime)) {
eventsBuilder.add(makeEvent(EventAction.LAST_CHANGED, null, lastChangeTime));
// Creates an RDAP event object as defined by RFC 9083
eventsBuilder.add(
Event.builder()
.setEventAction(EventAction.LAST_CHANGED)
.setEventDate(lastChangeTime)
.build());
}
return eventsBuilder.build();
}
/** Creates an RDAP event object as defined by RFC 9083. */
private static Event makeEvent(
EventAction eventAction, @Nullable String eventActor, DateTime eventDate) {
Event.Builder builder = Event.builder().setEventAction(eventAction).setEventDate(eventDate);
if (eventActor != null) {
builder.setEventActor(eventActor);
}
return builder.build();
}
/**
* Creates a vCard address entry: array of strings specifying the components of the address.
*

View File

@@ -15,7 +15,7 @@
package google.registry.rdap;
import static google.registry.flows.host.HostFlowUtils.validateHostName;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCache;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
@@ -63,7 +63,7 @@ public class RdapNameserverAction extends RdapActionBase {
// If there are no undeleted nameservers with the given name, the foreign key should point to
// the most recently deleted one.
Optional<Host> host =
loadByForeignKeyCached(
loadByForeignKeyByCache(
Host.class,
pathSearchString,
shouldIncludeDeleted() ? START_OF_TIME : getRequestTime());

View File

@@ -14,7 +14,7 @@
package google.registry.rdap;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCache;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
@@ -159,7 +159,8 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase {
.setIncompletenessWarningType(IncompletenessWarningType.COMPLETE);
Optional<Host> host =
loadByForeignKeyCached(Host.class, partialStringQuery.getInitialString(), getRequestTime());
loadByForeignKeyByCache(
Host.class, partialStringQuery.getInitialString(), getRequestTime());
metricInformationBuilder.setNumHostsRetrieved(host.isPresent() ? 1 : 0);
@@ -175,7 +176,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase {
private NameserverSearchResponse searchByNameUsingSuperordinateDomain(
RdapSearchPattern partialStringQuery) {
Optional<Domain> domain =
loadByForeignKeyCached(Domain.class, partialStringQuery.getSuffix(), getRequestTime());
loadByForeignKeyByCache(Domain.class, partialStringQuery.getSuffix(), getRequestTime());
if (domain.isEmpty()) {
// Don't allow wildcards with suffixes which are not domains we manage. That would risk a
// table scan in many easily foreseeable cases. The user might ask for ns*.zombo.com,
@@ -193,7 +194,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase {
// We can't just check that the host name starts with the initial query string, because
// then the query ns.exam*.example.com would match against nameserver ns.example.com.
if (partialStringQuery.matches(fqhn)) {
Optional<Host> host = loadByForeignKeyCached(Host.class, fqhn, getRequestTime());
Optional<Host> host = loadByForeignKeyByCache(Host.class, fqhn, getRequestTime());
if (shouldBeVisible(host)) {
hostList.add(host.get());
if (hostList.size() > rdapResultSetMaxSize) {

View File

@@ -31,7 +31,6 @@ import google.registry.request.HttpException.BadRequestException;
import google.registry.request.HttpException.UnprocessableEntityException;
import google.registry.request.Parameter;
import google.registry.request.ParameterMap;
import google.registry.request.RequestUrl;
import jakarta.inject.Inject;
import jakarta.persistence.criteria.CriteriaBuilder;
import java.io.UnsupportedEncodingException;
@@ -54,7 +53,6 @@ public abstract class RdapSearchActionBase extends RdapActionBase {
private static final int RESULT_SET_SIZE_SCALING_FACTOR = 30;
@Inject @RequestUrl String requestUrl;
@Inject @ParameterMap ImmutableListMultimap<String, String> parameterMap;
@Inject @Parameter("cursor") Optional<String> cursorTokenParam;
@Inject @Parameter("registrar") Optional<String> registrarParam;

View File

@@ -19,9 +19,8 @@ import static com.google.common.base.Preconditions.checkState;
import com.google.common.collect.ImmutableMap;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.Registrar.State;
import google.registry.model.registrar.RegistrarAddress;
import google.registry.model.registrar.RegistrarBase;
import google.registry.model.registrar.RegistrarBase.State;
import google.registry.xjc.contact.XjcContactE164Type;
import google.registry.xjc.rderegistrar.XjcRdeRegistrar;
import google.registry.xjc.rderegistrar.XjcRdeRegistrarAddrType;
@@ -41,7 +40,7 @@ final class RegistrarToXjcConverter {
private static final String UNKNOWN_CC = "US";
/** A conversion map between internal Registrar states and external RDE states. */
private static final ImmutableMap<RegistrarBase.State, XjcRdeRegistrarStatusType>
private static final ImmutableMap<Registrar.State, XjcRdeRegistrarStatusType>
REGISTRAR_STATUS_CONVERSIONS =
ImmutableMap.of(
State.ACTIVE, XjcRdeRegistrarStatusType.OK,

View File

@@ -27,7 +27,7 @@ import google.registry.config.RegistryConfig.Config;
import google.registry.groups.GroupsConnection;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarBase.State;
import google.registry.model.registrar.Registrar.State;
import jakarta.inject.Inject;
import java.util.Optional;
import javax.annotation.concurrent.Immutable;

View File

@@ -72,11 +72,11 @@ final class CreateCdnsTld extends ConfirmingCommand {
.setDescription(description)
.setNameServerSet(
RegistryToolEnvironment.get() == RegistryToolEnvironment.PRODUCTION
? "cloud-dns-registry"
: "cloud-dns-registry-test")
? "cloud-dns-registry"
: "cloud-dns-registry-test")
.setDnsName(dnsName)
.setName((name != null) ? name : dnsName)
.setDnssecConfig(new ManagedZoneDnsSecConfig().setNonExistence("NSEC").setState("ON"));
.setDnssecConfig(new ManagedZoneDnsSecConfig().setNonExistence("nsec").setState("on"));
}
@Override

View File

@@ -16,6 +16,7 @@ package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Strings.isNullOrEmpty;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.pricing.PricingEngineProxy.getPricesForDomainName;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import static org.joda.time.DateTimeZone.UTC;
@@ -23,6 +24,7 @@ import static org.joda.time.DateTimeZone.UTC;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.template.soy.data.SoyMapData;
import google.registry.model.common.FeatureFlag;
import google.registry.model.pricing.PremiumPricingEngine.DomainPrices;
import google.registry.tools.soy.DomainCreateSoyInfo;
import google.registry.util.StringGenerator;
@@ -58,9 +60,15 @@ final class CreateDomainCommand extends CreateOrUpdateDomainCommand {
@Override
protected void initMutatingEppToolCommand() {
checkArgumentNotNull(registrant, "Registrant must be specified");
checkArgument(!admins.isEmpty(), "At least one admin must be specified");
checkArgument(!techs.isEmpty(), "At least one tech must be specified");
tm().transact(
() -> {
if (!FeatureFlag.isActiveNowOrElse(
FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL, false)) {
checkArgumentNotNull(registrant, "Registrant must be specified");
checkArgument(!admins.isEmpty(), "At least one admin must be specified");
checkArgument(!techs.isEmpty(), "At least one tech must be specified");
}
});
if (isNullOrEmpty(password)) {
password = passwordGenerator.createString(PASSWORD_LENGTH);
}

View File

@@ -30,7 +30,6 @@ import com.google.common.collect.ImmutableSet;
import google.registry.flows.certs.CertificateChecker;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarAddress;
import google.registry.model.registrar.RegistrarBase;
import google.registry.tools.params.KeyValueMapParameter.CurrencyUnitToStringMap;
import google.registry.tools.params.OptionalLongParameter;
import google.registry.tools.params.OptionalPhoneNumberParameter;
@@ -61,11 +60,11 @@ abstract class CreateOrUpdateRegistrarCommand extends MutatingCommand {
List<String> mainParameters;
@Parameter(names = "--registrar_type", description = "Type of the registrar")
RegistrarBase.Type registrarType;
Registrar.Type registrarType;
@Nullable
@Parameter(names = "--registrar_state", description = "Initial state of the registrar")
RegistrarBase.State registrarState;
Registrar.State registrarState;
@Parameter(
names = "--allowed_tlds",

View File

@@ -18,7 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Strings.emptyToNull;
import static com.google.common.collect.Iterables.getOnlyElement;
import static google.registry.model.registrar.RegistrarBase.State.ACTIVE;
import static google.registry.model.registrar.Registrar.State.ACTIVE;
import static google.registry.tools.RegistryToolEnvironment.PRODUCTION;
import static google.registry.tools.RegistryToolEnvironment.SANDBOX;
import static google.registry.tools.RegistryToolEnvironment.UNITTEST;

View File

@@ -16,7 +16,7 @@ package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_ACTIONS;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCacheIfEnabled;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STATUSES;
@@ -326,7 +326,7 @@ public final class DomainLockUtils {
private Domain getDomain(String domainName, String registrarId, DateTime now) {
Domain domain =
loadByForeignKeyCached(Domain.class, domainName, now)
loadByForeignKeyByCacheIfEnabled(Domain.class, domainName, now)
.orElseThrow(() -> new IllegalArgumentException("Domain doesn't exist"));
// The user must have specified either the correct registrar ID or the admin registrar ID
checkArgument(

View File

@@ -29,7 +29,6 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.registrar.RegistrarPocBase;
import google.registry.tools.params.OptionalPhoneNumberParameter;
import google.registry.tools.params.PathParameter;
import google.registry.tools.params.StringListParameter;
@@ -153,7 +152,7 @@ final class RegistrarPocCommand extends MutatingCommand {
private static final ImmutableSet<Mode> MODES_REQUIRING_CONTACT_SYNC =
ImmutableSet.of(Mode.CREATE, Mode.UPDATE, Mode.DELETE);
@Nullable private ImmutableSet<RegistrarPocBase.Type> contactTypes;
@Nullable private ImmutableSet<RegistrarPoc.Type> contactTypes;
@Override
protected void init() throws Exception {
@@ -169,7 +168,7 @@ final class RegistrarPocCommand extends MutatingCommand {
} else {
contactTypes =
contactTypeNames.stream()
.map(Enums.stringConverter(RegistrarPocBase.Type.class))
.map(Enums.stringConverter(RegistrarPoc.Type.class))
.collect(toImmutableSet());
}
ImmutableSet<RegistrarPoc> contacts = registrar.getContacts();

View File

@@ -23,9 +23,7 @@ import google.registry.config.CloudTasksUtilsModule;
import google.registry.config.CredentialModule.LocalCredentialJson;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.dns.writer.VoidDnsWriterModule;
import google.registry.dns.writer.clouddns.CloudDnsWriterModule;
import google.registry.dns.writer.dnsupdate.DnsUpdateWriterModule;
import google.registry.dns.writer.DnsWritersModule;
import google.registry.keyring.KeyringModule;
import google.registry.keyring.api.KeyModule;
import google.registry.model.ModelModule;
@@ -56,9 +54,8 @@ import javax.annotation.Nullable;
BatchModule.class,
BigqueryModule.class,
ConfigModule.class,
CloudDnsWriterModule.class,
CloudTasksUtilsModule.class,
DnsUpdateWriterModule.class,
DnsWritersModule.class,
GsonModule.class,
KeyModule.class,
KeyringModule.class,
@@ -71,7 +68,6 @@ import javax.annotation.Nullable;
SecretManagerModule.class,
UrlConnectionServiceModule.class,
UtilsModule.class,
VoidDnsWriterModule.class,
NonCachingWhoisModule.class
})
interface RegistryToolComponent {

View File

@@ -251,11 +251,12 @@ final class UniformRapidSuspensionCommand extends MutatingEppToolCommand {
if (undo) {
return "";
}
StringBuilder undoBuilder = new StringBuilder("UNDO COMMAND:\n\n)")
.append("nomulus -e ")
.append(RegistryToolEnvironment.get())
.append(" uniform_rapid_suspension --undo --domain_name ")
.append(domainName);
StringBuilder undoBuilder =
new StringBuilder("UNDO COMMAND:\n\n")
.append("nomulus -e ")
.append(RegistryToolEnvironment.get())
.append(" uniform_rapid_suspension --undo --domain_name ")
.append(domainName);
if (!existingNameservers.isEmpty()) {
undoBuilder.append(" --hosts ").append(Joiner.on(',').join(existingNameservers));
}

View File

@@ -24,7 +24,7 @@ import google.registry.config.RegistryConfig.Config;
import google.registry.groups.GroupsConnection;
import google.registry.groups.GroupsConnection.Role;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPocBase;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.request.Action;
import google.registry.request.Action.GaeService;
import google.registry.request.HttpException.BadRequestException;
@@ -65,7 +65,7 @@ public class CreateGroupsAction implements Runnable {
if (registrar == null) {
return;
}
List<RegistrarPocBase.Type> types = asList(RegistrarPocBase.Type.values());
List<RegistrarPoc.Type> types = asList(RegistrarPoc.Type.values());
// Concurrently create the groups for each RegistrarContact.Type, collecting the results from
// each call (which are either an Exception if it failed, or absent() if it succeeded).
List<Optional<Exception>> results =

View File

@@ -29,7 +29,6 @@ import com.google.re2j.Pattern;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarAddress;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.registrar.RegistrarPocBase;
import google.registry.ui.forms.FormException;
import google.registry.ui.forms.FormField;
import google.registry.ui.forms.FormFieldException;
@@ -201,10 +200,10 @@ public final class RegistrarFormFields {
public static final FormField<String, String> CONTACT_REGISTRY_LOCK_PASSWORD_FIELD =
FormFields.NAME.asBuilderNamed("registryLockPassword").build();
public static final FormField<String, Set<RegistrarPocBase.Type>> CONTACT_TYPES =
public static final FormField<String, Set<RegistrarPoc.Type>> CONTACT_TYPES =
FormField.named("types")
.uppercased()
.asEnum(RegistrarPocBase.Type.class)
.asEnum(RegistrarPoc.Type.class)
.asSet(Splitter.on(',').omitEmptyStrings().trimResults())
.build();

View File

@@ -37,11 +37,10 @@ import google.registry.batch.CloudTasksUtils;
import google.registry.config.RegistryConfig;
import google.registry.export.sheet.SyncRegistrarsSheetAction;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.SimpleConsoleUpdateHistory;
import google.registry.model.console.ConsoleUpdateHistory;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.registrar.RegistrarPocBase;
import google.registry.request.HttpException;
import google.registry.security.XsrfTokenManager;
import google.registry.util.DiffUtils;
@@ -218,7 +217,7 @@ public abstract class ConsoleApiAction implements Runnable {
consoleApiParams.authResult().userIdForLogging(),
DiffUtils.prettyPrintDiffedMap(diffs, null)),
contacts.stream()
.filter(c -> c.getTypes().contains(RegistrarPocBase.Type.ADMIN))
.filter(c -> c.getTypes().contains(RegistrarPoc.Type.ADMIN))
.map(RegistrarPoc::getEmailAddress)
.collect(toImmutableList()));
}
@@ -262,7 +261,7 @@ public abstract class ConsoleApiAction implements Runnable {
}
}
protected void finishAndPersistConsoleUpdateHistory(SimpleConsoleUpdateHistory.Builder builder) {
protected void finishAndPersistConsoleUpdateHistory(ConsoleUpdateHistory.Builder builder) {
builder.setActingUser(consoleApiParams.authResult().user().get());
builder.setUrl(consoleApiParams.request().getRequestURI());
builder.setMethod(consoleApiParams.request().getMethod());

View File

@@ -55,7 +55,7 @@ public class ConsoleDomainGetAction extends ConsoleApiAction {
Optional<Domain> possibleDomain =
tm().transact(
() ->
EppResourceUtils.loadByForeignKeyCached(
EppResourceUtils.loadByForeignKeyByCacheIfEnabled(
Domain.class, paramDomain, tm().getTransactionTime()));
if (possibleDomain.isEmpty()) {
consoleApiParams.response().setStatus(SC_NOT_FOUND);

View File

@@ -28,7 +28,6 @@ import com.google.gson.annotations.Expose;
import google.registry.flows.EppException.AuthenticationErrorException;
import google.registry.flows.PasswordOnlyTransportCredentials;
import google.registry.model.console.ConsoleUpdateHistory;
import google.registry.model.console.SimpleConsoleUpdateHistory;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.request.Action;
@@ -108,7 +107,7 @@ public class ConsoleEppPasswordAction extends ConsoleApiAction {
registrar.asBuilder().setPassword(eppRequestBody.newPassword()).build();
tm().put(updatedRegistrar);
finishAndPersistConsoleUpdateHistory(
new SimpleConsoleUpdateHistory.Builder()
new ConsoleUpdateHistory.Builder()
.setType(ConsoleUpdateHistory.Type.EPP_PASSWORD_UPDATE)
.setDescription(registrar.getRegistrarId()));
sendExternalUpdates(

View File

@@ -19,7 +19,6 @@ import static google.registry.request.RequestParameters.extractOptionalIntParame
import static google.registry.request.RequestParameters.extractOptionalParameter;
import static google.registry.request.RequestParameters.extractRequiredParameter;
import com.google.common.collect.ImmutableSet;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import dagger.Module;
@@ -192,10 +191,10 @@ public final class ConsoleModule {
}
@Provides
@Parameter("contacts")
public static Optional<ImmutableSet<RegistrarPoc>> provideContacts(
@Parameter("contact")
public static Optional<RegistrarPoc> provideContacts(
Gson gson, @OptionalJsonPayload Optional<JsonElement> payload) {
return payload.map(s -> ImmutableSet.copyOf(gson.fromJson(s, RegistrarPoc[].class)));
return payload.map(s -> gson.fromJson(s, RegistrarPoc.class));
}
@Provides

View File

@@ -35,7 +35,6 @@ import google.registry.model.OteStats.StatType;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarBase;
import google.registry.request.Action;
import google.registry.request.Action.GkeService;
import google.registry.request.Parameter;
@@ -140,7 +139,7 @@ public class ConsoleOteAction extends ConsoleApiAction {
SC_BAD_REQUEST);
return;
}
if (!RegistrarBase.Type.OTE.equals(registrar.get().getType())) {
if (!Registrar.Type.OTE.equals(registrar.get().getType())) {
setFailedResponse(
String.format("Registrar with ID %s is not an OT&E registrar", registrarId),
SC_BAD_REQUEST);

View File

@@ -17,6 +17,7 @@ package google.registry.ui.server.console;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.POST;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
import static org.apache.http.HttpStatus.SC_OK;
@@ -24,7 +25,6 @@ import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.ConsoleUpdateHistory;
import google.registry.model.console.SimpleConsoleUpdateHistory;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.request.Action;
@@ -38,6 +38,7 @@ import google.registry.util.RegistryEnvironment;
import jakarta.inject.Inject;
import java.util.Optional;
import java.util.stream.Collectors;
import org.joda.time.DateTime;
@Action(
service = GaeService.DEFAULT,
@@ -89,20 +90,38 @@ public class ConsoleUpdateRegistrarAction extends ConsoleApiAction {
}
}
Registrar updatedRegistrar =
DateTime now = tm().getTransactionTime();
DateTime newLastPocVerificationDate =
registrarParam.getLastPocVerificationDate() == null
? START_OF_TIME
: registrarParam.getLastPocVerificationDate();
checkArgument(
newLastPocVerificationDate.isBefore(now),
"Invalid value of LastPocVerificationDate - value is in the future");
var updatedRegistrarBuilder =
existingRegistrar
.get()
.asBuilder()
.setAllowedTlds(
registrarParam.getAllowedTlds().stream()
.map(DomainNameUtils::canonicalizeHostname)
.collect(Collectors.toSet()))
.setRegistryLockAllowed(registrarParam.isRegistryLockAllowed())
.build();
.setLastPocVerificationDate(newLastPocVerificationDate);
if (user.getUserRoles()
.hasGlobalPermission(ConsolePermission.EDIT_REGISTRAR_DETAILS)) {
updatedRegistrarBuilder =
updatedRegistrarBuilder
.setAllowedTlds(
registrarParam.getAllowedTlds().stream()
.map(DomainNameUtils::canonicalizeHostname)
.collect(Collectors.toSet()))
.setRegistryLockAllowed(registrarParam.isRegistryLockAllowed())
.setLastPocVerificationDate(newLastPocVerificationDate);
}
var updatedRegistrar = updatedRegistrarBuilder.build();
tm().put(updatedRegistrar);
finishAndPersistConsoleUpdateHistory(
new SimpleConsoleUpdateHistory.Builder()
new ConsoleUpdateHistory.Builder()
.setType(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE)
.setDescription(updatedRegistrar.getRegistrarId()));
sendExternalUpdatesIfNecessary(

View File

@@ -165,7 +165,7 @@ public class ConsoleUsersAction extends ConsoleApiAction {
User updatedUser = updateUserRegistrarRoles(email, registrarId, null);
// User has no registrars assigned
if (updatedUser.getUserRoles().getRegistrarRoles().size() == 0) {
if (updatedUser.getUserRoles().getRegistrarRoles().isEmpty()) {
try {
directory.users().delete(email).execute();
} catch (IOException e) {

View File

@@ -28,11 +28,9 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.ConsoleUpdateHistory;
import google.registry.model.console.SimpleConsoleUpdateHistory;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarBase;
import google.registry.model.registrar.RegistrarBase.State;
import google.registry.model.registrar.Registrar.State;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.request.Action;
import google.registry.request.Action.GaeService;
@@ -55,8 +53,8 @@ import java.util.Optional;
public class RegistrarsAction extends ConsoleApiAction {
private static final int PASSWORD_LENGTH = 16;
private static final int PASSCODE_LENGTH = 5;
private static final ImmutableList<RegistrarBase.Type> allowedRegistrarTypes =
ImmutableList.of(Registrar.Type.REAL, RegistrarBase.Type.OTE);
private static final ImmutableList<Registrar.Type> allowedRegistrarTypes =
ImmutableList.of(Registrar.Type.REAL, Registrar.Type.OTE);
private static final String SQL_TEMPLATE =
"""
SELECT * FROM "Registrar"
@@ -174,7 +172,7 @@ public class RegistrarsAction extends ConsoleApiAction {
registrar.getRegistrarId());
tm().putAll(registrar, contact);
finishAndPersistConsoleUpdateHistory(
new SimpleConsoleUpdateHistory.Builder()
new ConsoleUpdateHistory.Builder()
.setType(ConsoleUpdateHistory.Type.REGISTRAR_CREATE)
.setDescription(registrar.getRegistrarId()));
});

View File

@@ -26,7 +26,7 @@ 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.SimpleConsoleUpdateHistory;
import google.registry.model.console.ConsoleUpdateHistory;
import google.registry.model.console.User;
import google.registry.model.eppcommon.ProtocolDefinition;
import google.registry.model.eppoutput.EppOutput;
@@ -113,7 +113,7 @@ public class ConsoleBulkDomainAction extends ConsoleApiAction {
.forEach(
e ->
finishAndPersistConsoleUpdateHistory(
new SimpleConsoleUpdateHistory.Builder()
new ConsoleUpdateHistory.Builder()
.setDescription(e.getKey())
.setType(actionType.getConsoleUpdateHistoryType()))));
}

View File

@@ -15,12 +15,13 @@
package google.registry.ui.server.console.settings;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Sets.difference;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.DELETE;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.POST;
import static google.registry.request.Action.Method.PUT;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import com.google.common.collect.HashMultimap;
@@ -32,67 +33,123 @@ import google.registry.model.console.ConsolePermission;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.registrar.RegistrarPocBase.Type;
import google.registry.persistence.transaction.QueryComposer.Comparator;
import google.registry.model.registrar.RegistrarPoc.Type;
import google.registry.request.Action;
import google.registry.request.Action.GaeService;
import google.registry.request.Action.GkeService;
import google.registry.request.Parameter;
import google.registry.request.auth.Auth;
import google.registry.ui.forms.FormException;
import google.registry.ui.server.RegistrarFormFields;
import google.registry.ui.server.console.ConsoleApiAction;
import google.registry.ui.server.console.ConsoleApiParams;
import jakarta.inject.Inject;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
@Action(
service = GaeService.DEFAULT,
gkeService = GkeService.CONSOLE,
path = ContactAction.PATH,
method = {GET, POST},
method = {GET, POST, DELETE, PUT},
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public class ContactAction extends ConsoleApiAction {
static final String PATH = "/console-api/settings/contacts";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final Optional<ImmutableSet<RegistrarPoc>> contacts;
private final Optional<RegistrarPoc> contact;
private final String registrarId;
@Inject
public ContactAction(
ConsoleApiParams consoleApiParams,
@Parameter("registrarId") String registrarId,
@Parameter("contacts") Optional<ImmutableSet<RegistrarPoc>> contacts) {
@Parameter("contact") Optional<RegistrarPoc> contact) {
super(consoleApiParams);
this.registrarId = registrarId;
this.contacts = contacts;
this.contact = contact;
}
@Override
protected void getHandler(User user) {
checkPermission(user, registrarId, ConsolePermission.VIEW_REGISTRAR_DETAILS);
ImmutableList<RegistrarPoc> am =
tm().transact(
() ->
tm()
.createQueryComposer(RegistrarPoc.class)
.where("registrarId", Comparator.EQ, registrarId)
.stream()
.filter(r -> !r.getTypes().isEmpty())
.collect(toImmutableList()));
ImmutableList<RegistrarPoc> contacts =
tm().transact(() -> RegistrarPoc.loadForRegistrar(registrarId));
consoleApiParams.response().setStatus(SC_OK);
consoleApiParams.response().setPayload(consoleApiParams.gson().toJson(am));
consoleApiParams.response().setPayload(consoleApiParams.gson().toJson(contacts));
}
@Override
protected void deleteHandler(User user) {
updateContacts(
user,
(registrar, oldContacts) ->
oldContacts.stream()
.filter(
oldContact ->
!oldContact.getEmailAddress().equals(contact.get().getEmailAddress()))
.collect(toImmutableSet()));
}
@Override
protected void postHandler(User user) {
updateContacts(
user,
(registrar, oldContacts) -> {
RegistrarPoc newContact = contact.get();
return ImmutableSet.<RegistrarPoc>builder()
.addAll(oldContacts)
.add(
new RegistrarPoc()
.asBuilder()
.setTypes(newContact.getTypes())
.setVisibleInWhoisAsTech(newContact.getVisibleInWhoisAsTech())
.setVisibleInWhoisAsAdmin(newContact.getVisibleInWhoisAsAdmin())
.setVisibleInDomainWhoisAsAbuse(newContact.getVisibleInDomainWhoisAsAbuse())
.setFaxNumber(newContact.getFaxNumber())
.setName(newContact.getName())
.setEmailAddress(newContact.getEmailAddress())
.setPhoneNumber(newContact.getPhoneNumber())
.setRegistrar(registrar)
.build())
.build();
});
}
@Override
protected void putHandler(User user) {
updateContacts(
user,
(registrar, oldContacts) -> {
RegistrarPoc updatedContact = contact.get();
return oldContacts.stream()
.map(
oldContact ->
oldContact.getId().equals(updatedContact.getId())
? oldContact
.asBuilder()
.setTypes(updatedContact.getTypes())
.setVisibleInWhoisAsTech(updatedContact.getVisibleInWhoisAsTech())
.setVisibleInWhoisAsAdmin(updatedContact.getVisibleInWhoisAsAdmin())
.setVisibleInDomainWhoisAsAbuse(
updatedContact.getVisibleInDomainWhoisAsAbuse())
.setFaxNumber(updatedContact.getFaxNumber())
.setName(updatedContact.getName())
.setEmailAddress(updatedContact.getEmailAddress())
.setPhoneNumber(updatedContact.getPhoneNumber())
.build()
: oldContact)
.collect(toImmutableSet());
});
}
private void updateContacts(
User user,
BiFunction<Registrar, ImmutableSet<RegistrarPoc>, ImmutableSet<RegistrarPoc>>
contactsUpdater) {
checkPermission(user, registrarId, ConsolePermission.EDIT_REGISTRAR_DETAILS);
checkArgument(contacts.isPresent(), "Contacts parameter is not present");
checkArgument(contact.isPresent(), "Contact parameter is not present");
Registrar registrar =
Registrar.loadByRegistrarId(registrarId)
.orElseThrow(
@@ -101,20 +158,10 @@ public class ContactAction extends ConsoleApiAction {
String.format("Unknown registrar %s", registrarId)));
ImmutableSet<RegistrarPoc> oldContacts = registrar.getContacts();
ImmutableSet<RegistrarPoc> updatedContacts =
RegistrarFormFields.getRegistrarContactBuilders(
oldContacts,
Collections.singletonMap(
"contacts",
contacts.get().stream()
.map(RegistrarPoc::toJsonMap)
.collect(toImmutableList())))
.stream()
.map(builder -> builder.setRegistrar(registrar).build())
.collect(toImmutableSet());
ImmutableSet<RegistrarPoc> newContacts = contactsUpdater.apply(registrar, oldContacts);
try {
checkContactRequirements(oldContacts, updatedContacts);
checkContactRequirements(oldContacts, newContacts);
} catch (FormException e) {
logger.atWarning().withCause(e).log(
"Error processing contacts post request for registrar: %s", registrarId);
@@ -123,14 +170,13 @@ public class ContactAction extends ConsoleApiAction {
tm().transact(
() -> {
RegistrarPoc.updateContacts(registrar, updatedContacts);
RegistrarPoc.updateContacts(registrar, newContacts);
Registrar updatedRegistrar =
registrar.asBuilder().setContactsRequireSyncing(true).build();
tm().put(updatedRegistrar);
sendExternalUpdatesIfNecessary(
EmailInfo.create(registrar, updatedRegistrar, oldContacts, updatedContacts));
EmailInfo.create(registrar, updatedRegistrar, oldContacts, newContacts));
});
consoleApiParams.response().setStatus(SC_OK);
}
@@ -169,6 +215,8 @@ public class ContactAction extends ConsoleApiAction {
throw new ContactRequirementException(t);
}
}
enforcePrimaryContactRestrictions(oldContactsByType, newContactsByType);
ensurePhoneNumberNotRemovedForContactTypes(oldContactsByType, newContactsByType, Type.TECH);
Optional<RegistrarPoc> domainWhoisAbuseContact =
getDomainWhoisVisibleAbuseContact(updatedContacts);
@@ -187,6 +235,23 @@ public class ContactAction extends ConsoleApiAction {
checkContactRegistryLockRequirements(existingContacts, updatedContacts);
}
private static void enforcePrimaryContactRestrictions(
Multimap<Type, RegistrarPoc> oldContactsByType,
Multimap<Type, RegistrarPoc> newContactsByType) {
ImmutableSet<String> oldAdminEmails =
oldContactsByType.get(Type.ADMIN).stream()
.map(RegistrarPoc::getEmailAddress)
.collect(toImmutableSet());
ImmutableSet<String> newAdminEmails =
newContactsByType.get(Type.ADMIN).stream()
.map(RegistrarPoc::getEmailAddress)
.collect(toImmutableSet());
if (!newAdminEmails.containsAll(oldAdminEmails)) {
throw new ContactRequirementException(
"Cannot remove or change the email address of primary contacts");
}
}
private static void checkContactRegistryLockRequirements(
ImmutableSet<RegistrarPoc> existingContacts, ImmutableSet<RegistrarPoc> updatedContacts) {
// Any contact(s) with new passwords must be allowed to set them

View File

@@ -22,7 +22,6 @@ import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.ConsoleUpdateHistory;
import google.registry.model.console.SimpleConsoleUpdateHistory;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.request.Action;
@@ -93,7 +92,7 @@ public class RdapRegistrarFieldsAction extends ConsoleApiAction {
.build();
tm().put(newRegistrar);
finishAndPersistConsoleUpdateHistory(
new SimpleConsoleUpdateHistory.Builder()
new ConsoleUpdateHistory.Builder()
.setType(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE)
.setDescription(newRegistrar.getRegistrarId()));
sendExternalUpdatesIfNecessary(

View File

@@ -26,7 +26,6 @@ import google.registry.flows.certs.CertificateChecker;
import google.registry.flows.certs.CertificateChecker.InsecureCertificateException;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.ConsoleUpdateHistory;
import google.registry.model.console.SimpleConsoleUpdateHistory;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.request.Action;
@@ -120,7 +119,7 @@ public class SecurityAction extends ConsoleApiAction {
Registrar updatedRegistrar = updatedRegistrarBuilder.build();
tm().put(updatedRegistrar);
finishAndPersistConsoleUpdateHistory(
new SimpleConsoleUpdateHistory.Builder()
new ConsoleUpdateHistory.Builder()
.setType(ConsoleUpdateHistory.Type.REGISTRAR_SECURITY_UPDATE)
.setDescription(registrarId));

View File

@@ -17,7 +17,7 @@ package google.registry.whois;
import static com.google.common.base.Preconditions.checkNotNull;
import static google.registry.flows.domain.DomainFlowUtils.isBlockedByBsa;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCache;
import static google.registry.model.tld.Tlds.findTldForName;
import static google.registry.model.tld.Tlds.getTlds;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
@@ -94,7 +94,7 @@ public class DomainLookupCommand implements WhoisCommand {
private Optional<WhoisResponse> getResponse(InternetDomainName domainName, DateTime now) {
Optional<Domain> domainResource =
cached
? loadByForeignKeyCached(Domain.class, domainName.toString(), now)
? loadByForeignKeyByCache(Domain.class, domainName.toString(), now)
: loadByForeignKey(Domain.class, domainName.toString(), now);
return domainResource.map(
domain -> new DomainWhoisResponse(domain, fullOutput, whoisRedactedEmailText, now));

View File

@@ -81,7 +81,7 @@ final class DomainWhoisResponse extends WhoisResponseImpl {
domain.getCurrentSponsorRegistrarId());
Registrar registrar = registrarOptional.get();
Optional<RegistrarPoc> abuseContact =
registrar.getContacts().stream()
registrar.getContactsFromReplica().stream()
.filter(RegistrarPoc::getVisibleInDomainWhoisAsAbuse)
.findFirst();
return WhoisResponseResults.create(
@@ -154,7 +154,7 @@ final class DomainWhoisResponse extends WhoisResponseImpl {
// If we refer to a contact that doesn't exist, that's a bug. It means referential integrity
// has somehow been broken. We skip the rest of this contact, but log it to hopefully bring it
// someone's attention.
Contact contact1 = EppResource.loadCached(contact.get());
Contact contact1 = EppResource.loadByCache(contact.get());
if (contact1 == null) {
logger.atSevere().log(
"(BUG) Broken reference found from domain %s to contact %s.",

View File

@@ -16,7 +16,7 @@ package google.registry.whois;
import static com.google.common.base.Preconditions.checkNotNull;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCache;
import static google.registry.model.tld.Tlds.findTldForName;
import static google.registry.model.tld.Tlds.getTlds;
import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND;
@@ -57,7 +57,7 @@ public class NameserverLookupByHostCommand implements WhoisCommand {
private Optional<WhoisResponse> getResponse(InternetDomainName hostName, DateTime now) {
Optional<Host> host =
cached
? loadByForeignKeyCached(Host.class, hostName.toString(), now)
? loadByForeignKeyByCache(Host.class, hostName.toString(), now)
: loadByForeignKey(Host.class, hostName.toString(), now);
return host.map(h -> new NameserverWhoisResponse(h, now));
}

View File

@@ -44,7 +44,7 @@ class RegistrarWhoisResponse extends WhoisResponseImpl {
@Override
public WhoisResponseResults getResponse(boolean preferUnicode, String disclaimer) {
Set<RegistrarPoc> contacts = registrar.getContacts();
Set<RegistrarPoc> contacts = registrar.getContactsFromReplica();
String plaintext =
new RegistrarEmitter()
.emitField("Registrar", registrar.getRegistrarName())

View File

@@ -47,17 +47,14 @@
<class>google.registry.model.billing.BillingRecurrence</class>
<class>google.registry.model.common.Cursor</class>
<class>google.registry.model.common.DnsRefreshRequest</class>
<class>google.registry.model.console.ConsoleEppActionHistory</class>
<class>google.registry.model.console.RegistrarPocUpdateHistory</class>
<class>google.registry.model.console.RegistrarUpdateHistory</class>
<class>google.registry.model.console.SimpleConsoleUpdateHistory</class>
<class>google.registry.model.common.FeatureFlag</class>
<class>google.registry.model.console.ConsoleUpdateHistory</class>
<class>google.registry.model.console.PasswordResetRequest</class>
<class>google.registry.model.console.User</class>
<class>google.registry.model.console.UserUpdateHistory</class>
<class>google.registry.model.contact.ContactHistory</class>
<class>google.registry.model.contact.Contact</class>
<class>google.registry.model.domain.Domain</class>
<class>google.registry.model.domain.DomainHistory</class>
<class>google.registry.model.common.FeatureFlag</class>
<class>google.registry.model.domain.GracePeriod</class>
<class>google.registry.model.domain.GracePeriod$GracePeriodHistory</class>
<class>google.registry.model.domain.secdns.DomainDsData</class>

View File

@@ -20,9 +20,9 @@
{@param domain: string}
{@param period: int}
{@param nameservers: list<string>}
{@param registrant: string}
{@param admins: list<string>}
{@param techs: list<string>}
{@param? registrant: string|null}
{@param? admins: list<string>|null}
{@param? techs: list<string>|null}
{@param password: string}
{@param? currency: string|null}
{@param? price: string|null}
@@ -45,13 +45,19 @@
{/for}
</domain:ns>
{/if}
<domain:registrant>{$registrant}</domain:registrant>
{for $admin in $admins}
<domain:contact type="admin">{$admin}</domain:contact>
{/for}
{for $tech in $techs}
<domain:contact type="tech">{$tech}</domain:contact>
{/for}
{if $registrant != null}
<domain:registrant>{$registrant}</domain:registrant>
{/if}
{if $admins != null}
{for $admin in $admins}
<domain:contact type="admin">{$admin}</domain:contact>
{/for}
{/if}
{if $techs != null}
{for $tech in $techs}
<domain:contact type="tech">{$tech}</domain:contact>
{/for}
{/if}
<domain:authInfo>
<domain:pw>{$password}</domain:pw>
</domain:authInfo>

View File

@@ -36,8 +36,7 @@ import google.registry.groups.GmailClient;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarAddress;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.registrar.RegistrarPocBase;
import google.registry.model.registrar.RegistrarPocBase.Type;
import google.registry.model.registrar.RegistrarPoc.Type;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.FakeClock;
@@ -57,13 +56,13 @@ class SendExpiringCertificateNotificationEmailActionTest {
private static final String EXPIRATION_WARNING_EMAIL_BODY_TEXT =
"""
Dear %1$s,
Dear %1$s,
We would like to inform you that your %2$s certificate will expire at %3$s.
Kind update your account using the following steps:
1. Navigate to support and login using your %4$s@registry.example credentials.
2. Click Settings -> Privacy on the top left corner.
3. Click Edit and enter certificate string. 3. Click SaveRegards,Example Registry""";
We would like to inform you that your %2$s certificate will expire at %3$s.
Kind update your account using the following steps:
1. Navigate to support and login using your %4$s@registry.example credentials.
2. Click Settings -> Privacy on the top left corner.
3. Click Edit and enter certificate string. 3. Click SaveRegards,Example Registry""";
private static final String EXPIRATION_WARNING_EMAIL_SUBJECT_TEXT = "Expiration Warning Email";
@@ -220,7 +219,7 @@ class SendExpiringCertificateNotificationEmailActionTest {
.setEmailAddress("will@example-registrar.tld")
.setPhoneNumber("+1.3105551213")
.setFaxNumber("+1.3105551213")
.setTypes(ImmutableSet.of(RegistrarPocBase.Type.TECH))
.setTypes(ImmutableSet.of(RegistrarPoc.Type.TECH))
.setVisibleInWhoisAsAdmin(true)
.setVisibleInWhoisAsTech(false)
.build());
@@ -510,7 +509,7 @@ class SendExpiringCertificateNotificationEmailActionTest {
.setEmailAddress("jd@example-registrar.tld")
.setPhoneNumber("+1.3105551213")
.setFaxNumber("+1.3105551213")
.setTypes(ImmutableSet.of(RegistrarPocBase.Type.TECH))
.setTypes(ImmutableSet.of(RegistrarPoc.Type.TECH))
.setVisibleInWhoisAsAdmin(true)
.setVisibleInWhoisAsTech(false)
.build(),
@@ -520,7 +519,7 @@ class SendExpiringCertificateNotificationEmailActionTest {
.setEmailAddress("js@example-registrar.tld")
.setPhoneNumber("+1.1111111111")
.setFaxNumber("+1.1111111111")
.setTypes(ImmutableSet.of(RegistrarPocBase.Type.TECH))
.setTypes(ImmutableSet.of(RegistrarPoc.Type.TECH))
.build(),
new RegistrarPoc.Builder()
.setRegistrar(registrar)
@@ -528,7 +527,7 @@ class SendExpiringCertificateNotificationEmailActionTest {
.setEmailAddress("will@example-registrar.tld")
.setPhoneNumber("+1.3105551213")
.setFaxNumber("+1.3105551213")
.setTypes(ImmutableSet.of(RegistrarPocBase.Type.TECH))
.setTypes(ImmutableSet.of(RegistrarPoc.Type.TECH))
.setVisibleInWhoisAsAdmin(true)
.setVisibleInWhoisAsTech(false)
.build(),
@@ -538,7 +537,7 @@ class SendExpiringCertificateNotificationEmailActionTest {
.setEmailAddress("mike@example-registrar.tld")
.setPhoneNumber("+1.1111111111")
.setFaxNumber("+1.1111111111")
.setTypes(ImmutableSet.of(RegistrarPocBase.Type.ADMIN))
.setTypes(ImmutableSet.of(RegistrarPoc.Type.ADMIN))
.build(),
new RegistrarPoc.Builder()
.setRegistrar(registrar)
@@ -546,7 +545,7 @@ class SendExpiringCertificateNotificationEmailActionTest {
.setEmailAddress("john@example-registrar.tld")
.setPhoneNumber("+1.3105551215")
.setFaxNumber("+1.3105551216")
.setTypes(ImmutableSet.of(RegistrarPocBase.Type.ADMIN))
.setTypes(ImmutableSet.of(RegistrarPoc.Type.ADMIN))
.setVisibleInWhoisAsTech(true)
.build());
persistSimpleResources(contacts);
@@ -700,7 +699,7 @@ class SendExpiringCertificateNotificationEmailActionTest {
/** Returns persisted sample contacts with a customized contact email type. */
private static ImmutableList<RegistrarPoc> persistSampleContacts(
Registrar registrar, RegistrarPocBase.Type emailType) {
Registrar registrar, RegistrarPoc.Type emailType) {
return persistSimpleResources(
ImmutableList.of(
new RegistrarPoc.Builder()

View File

@@ -85,7 +85,7 @@ class BillingEventTest {
assertThat(invoiceKey.startDate()).isEqualTo("2017-10-01");
assertThat(invoiceKey.endDate()).isEqualTo("2022-09-30");
assertThat(invoiceKey.productAccountKey()).isEqualTo("12345-CRRHELLO");
assertThat(invoiceKey.usageGroupingKey()).isEqualTo("myRegistrar");
assertThat(invoiceKey.usageGroupingKey()).isEqualTo("");
assertThat(invoiceKey.description()).isEqualTo("RENEW | TLD: test | TERM: 5-year");
assertThat(invoiceKey.unitPrice()).isEqualTo(20.5);
assertThat(invoiceKey.unitPriceCurrency()).isEqualTo("USD");
@@ -106,7 +106,7 @@ class BillingEventTest {
assertThat(invoiceKey.toCsv(3L))
.isEqualTo(
"2017-10-01,2022-09-30,12345-CRRHELLO,61.50,USD,10125,1,PURCHASE,"
+ "myRegistrar,3,RENEW | TLD: test | TERM: 5-year,20.50,USD,");
+ ",3,RENEW | TLD: test | TERM: 5-year,20.50,USD,");
}
@Test
@@ -116,7 +116,7 @@ class BillingEventTest {
assertThat(invoiceKey.toCsv(3L))
.isEqualTo(
"2017-10-01,,12345-CRRHELLO,61.50,USD,10125,1,PURCHASE,"
+ "myRegistrar,3,RENEW | TLD: test | TERM: 0-year,20.50,USD,");
+ ",3,RENEW | TLD: test | TERM: 0-year,20.50,USD,");
}
@Test

View File

@@ -199,6 +199,36 @@ class InvoicingPipelineTest {
0,
"USD",
20.0,
""),
google.registry.beam.billing.BillingEvent.create(
15,
DateTime.parse("2017-10-02T00:00:00.0Z"),
DateTime.parse("2017-10-04T00:00:00.0Z"),
"theRegistrarCopy",
"234",
"",
"test",
"CREATE",
"mydomainfromanotherclient.test",
"REPO-ID",
5,
"JPY",
70.0,
""),
google.registry.beam.billing.BillingEvent.create(
16,
DateTime.parse("2017-10-04T00:00:00Z"),
DateTime.parse("2017-10-04T00:00:00Z"),
"theRegistrarCopy",
"234",
"",
"test",
"RENEW",
"mydomain2fromanotherclient.test",
"REPO-ID",
3,
"USD",
20.5,
""));
private static final ImmutableMap<String, ImmutableList<String>> EXPECTED_DETAILED_REPORT_MAP =
@@ -224,18 +254,26 @@ class InvoicingPipelineTest {
"invoice_details_2017-10_anotherRegistrar_test.csv",
ImmutableList.of(
"5,2017-10-04 00:00:00 UTC,2017-10-04 00:00:00 UTC,anotherRegistrar,789,,"
+ "test,CREATE,mydomain5.test,REPO-ID,1,USD,0.00,SUNRISE ANCHOR_TENANT"));
+ "test,CREATE,mydomain5.test,REPO-ID,1,USD,0.00,SUNRISE ANCHOR_TENANT"),
"invoice_details_2017-10_theRegistrarCopy_test.csv",
ImmutableList.of(
"15,2017-10-02 00:00:00 UTC,2017-10-04 00:00:00"
+ " UTC,theRegistrarCopy,234,,test,CREATE,mydomainfromanotherclient.test,REPO-ID,5,JPY,70.00,",
"16,2017-10-04 00:00:00 UTC,2017-10-04 00:00:00"
+ " UTC,theRegistrarCopy,234,,test,RENEW,mydomain2fromanotherclient.test,REPO-ID,3,USD,20.50,"));
private static final ImmutableList<String> EXPECTED_INVOICE_OUTPUT =
ImmutableList.of(
"2017-10-01,2020-09-30,234,41.00,USD,10125,1,PURCHASE,theRegistrar,2,"
"2017-10-01,2020-09-30,234,61.50,USD,10125,1,PURCHASE,,3,"
+ "RENEW | TLD: test | TERM: 3-year,20.50,USD,",
"2017-10-01,2022-09-30,234,70.00,JPY,10125,1,PURCHASE,theRegistrar,1,"
"2017-10-01,2022-09-30,234,70.00,JPY,10125,1,PURCHASE,,1,"
+ "CREATE | TLD: hello | TERM: 5-year,70.00,JPY,",
"2017-10-01,,234,20.00,USD,10125,1,PURCHASE,theRegistrar,1,"
"2017-10-01,,234,20.00,USD,10125,1,PURCHASE,,1,"
+ "SERVER_STATUS | TLD: test | TERM: 0-year,20.00,USD,",
"2017-10-01,2018-09-30,456,20.50,USD,10125,1,PURCHASE,bestdomains,1,"
+ "RENEW | TLD: test | TERM: 1-year,20.50,USD,116688");
"2017-10-01,2018-09-30,456,20.50,USD,10125,1,PURCHASE,,1,"
+ "RENEW | TLD: test | TERM: 1-year,20.50,USD,116688",
"2017-10-01,2022-09-30,234,70.00,JPY,10125,1,PURCHASE,,1,CREATE | TLD: test | TERM:"
+ " 5-year,70.00,JPY,");
private final InvoicingPipelineOptions options =
PipelineOptionsFactory.create().as(InvoicingPipelineOptions.class);
@@ -355,21 +393,21 @@ class InvoicingPipelineTest {
.isEqualTo(
"""
SELECT b, r FROM BillingEvent b
JOIN Registrar r ON b.clientId = r.registrarId
JOIN Domain d ON b.domainRepoId = d.repoId
JOIN Tld t ON t.tldStr = d.tld
LEFT JOIN BillingCancellation c ON b.id = c.billingEvent
LEFT JOIN BillingCancellation cr ON b.cancellationMatchingBillingEvent = cr.billingRecurrence
WHERE r.billingAccountMap IS NOT NULL
AND r.type = 'REAL'
AND t.invoicingEnabled IS TRUE
AND CAST(b.billingTime AS timestamp)
BETWEEN CAST('2017-10-01T00:00:00Z' AS timestamp)
AND CAST('2017-11-01T00:00:00Z' AS timestamp)
AND c.id IS NULL
AND cr.id IS NULL
""");
SELECT b, r FROM BillingEvent b
JOIN Registrar r ON b.clientId = r.registrarId
JOIN Domain d ON b.domainRepoId = d.repoId
JOIN Tld t ON t.tldStr = d.tld
LEFT JOIN BillingCancellation c ON b.id = c.billingEvent
LEFT JOIN BillingCancellation cr ON b.cancellationMatchingBillingEvent = cr.billingRecurrence
WHERE r.billingAccountMap IS NOT NULL
AND r.type = 'REAL'
AND t.invoicingEnabled IS TRUE
AND CAST(b.billingTime AS timestamp)
BETWEEN CAST('2017-10-01T00:00:00Z' AS timestamp)
AND CAST('2017-11-01T00:00:00Z' AS timestamp)
AND c.id IS NULL
AND cr.id IS NULL
""");
}
/** Returns the text contents of a file under the beamBucket/results directory. */
@@ -391,6 +429,13 @@ class InvoicingPipelineTest {
.setBillingAccountMap(ImmutableMap.of(JPY, "234", USD, "234"))
.build();
persistResource(registrar1);
Registrar registrar11 = persistNewRegistrar("theRegistrarCopy");
registrar11 =
registrar11
.asBuilder()
.setBillingAccountMap(ImmutableMap.of(JPY, "234", USD, "234"))
.build();
persistResource(registrar11);
Registrar registrar2 = persistNewRegistrar("bestdomains");
registrar2 =
registrar2
@@ -547,6 +592,21 @@ class InvoicingPipelineTest {
.setDomainHistory(domainHistoryRecurrence)
.build();
persistResource(cancellationRecurrence);
// Domains created for registrar with = key but != client id.
Domain domain14 = persistActiveDomain("mydomainfromanotherclient.test");
Domain domain15 = persistActiveDomain("mydomain2fromanotherclient.test");
persistBillingEvent(
15,
domain14,
registrar11,
Reason.CREATE,
5,
Money.ofMajor(JPY, 70),
DateTime.parse("2017-10-04T00:00:00.0Z"),
DateTime.parse("2017-10-02T00:00:00.0Z"));
persistBillingEvent(16, domain15, registrar11, Reason.RENEW, 3, Money.of(USD, 20.5));
}
private static DomainHistory persistDomainHistory(Domain domain, Registrar registrar) {

View File

@@ -0,0 +1,36 @@
// 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.beam.common;
import static com.google.common.truth.Truth.assertThat;
import google.registry.util.RegistryEnvironment;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.junit.jupiter.api.Test;
public class RegistryPipelineWorkerInitializerTest {
@Test
void test() {
RegistryPipelineOptions options =
PipelineOptionsFactory.fromArgs(
"--registryEnvironment=ALPHA", "--isolationOverride=TRANSACTION_SERIALIZABLE")
.withValidation()
.as(RegistryPipelineOptions.class);
new RegistryPipelineWorkerInitializer().beforeProcessing(options);
assertThat(RegistryEnvironment.isOnJetty()).isTrue();
System.clearProperty("google.registry.jetty");
}
}

View File

@@ -70,7 +70,7 @@ import google.registry.model.rde.RdeMode;
import google.registry.model.rde.RdeRevision;
import google.registry.model.rde.RdeRevision.RdeRevisionId;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarBase.State;
import google.registry.model.registrar.Registrar.State;
import google.registry.model.reporting.DomainTransactionRecord;
import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField;
import google.registry.model.reporting.HistoryEntry;
@@ -388,22 +388,22 @@ public class RdePipelineTest {
assertThat(domainFrags.stream().findFirst().get().xml().strip())
.isEqualTo(
"""
<rdeDomain:domain>
<rdeDomain:name>cat.fun</rdeDomain:name>
<rdeDomain:roid>15-FUN</rdeDomain:roid>
<rdeDomain:uName>cat.fun</rdeDomain:uName>
<rdeDomain:status s="ok"/>
<rdeDomain:contact type="admin">contact456</rdeDomain:contact>
<rdeDomain:contact type="tech">contact456</rdeDomain:contact>
<rdeDomain:ns>
<domain:hostObj>ns1.external.tld</domain:hostObj>
<domain:hostObj>ns1.hello.soy</domain:hostObj>
</rdeDomain:ns>
<rdeDomain:clID>TheRegistrar</rdeDomain:clID>
<rdeDomain:crRr>TheRegistrar</rdeDomain:crRr>
<rdeDomain:crDate>1970-01-01T00:00:00Z</rdeDomain:crDate>
<rdeDomain:exDate>294247-01-10T04:00:54Z</rdeDomain:exDate>
</rdeDomain:domain>""");
<rdeDomain:domain>
<rdeDomain:name>cat.fun</rdeDomain:name>
<rdeDomain:roid>15-FUN</rdeDomain:roid>
<rdeDomain:uName>cat.fun</rdeDomain:uName>
<rdeDomain:status s="ok"/>
<rdeDomain:contact type="admin">contact456</rdeDomain:contact>
<rdeDomain:contact type="tech">contact456</rdeDomain:contact>
<rdeDomain:ns>
<domain:hostObj>ns1.external.tld</domain:hostObj>
<domain:hostObj>ns1.hello.soy</domain:hostObj>
</rdeDomain:ns>
<rdeDomain:clID>TheRegistrar</rdeDomain:clID>
<rdeDomain:crRr>TheRegistrar</rdeDomain:crRr>
<rdeDomain:crDate>1970-01-01T00:00:00Z</rdeDomain:crDate>
<rdeDomain:exDate>294247-01-10T04:00:54Z</rdeDomain:exDate>
</rdeDomain:domain>""");
}
if (kv.getKey().mode().equals(FULL)) {
// Contact fragments for hello.soy.
@@ -425,23 +425,23 @@ public class RdePipelineTest {
assertThat(domainFrags.stream().findFirst().get().xml().strip())
.isEqualTo(
"""
<rdeDomain:domain>
<rdeDomain:name>hello.soy</rdeDomain:name>
<rdeDomain:roid>E-SOY</rdeDomain:roid>
<rdeDomain:uName>hello.soy</rdeDomain:uName>
<rdeDomain:status s="ok"/>
<rdeDomain:registrant>contact1234</rdeDomain:registrant>
<rdeDomain:contact type="admin">contact789</rdeDomain:contact>
<rdeDomain:contact type="tech">contact1234</rdeDomain:contact>
<rdeDomain:ns>
<domain:hostObj>ns1.external.tld</domain:hostObj>
<domain:hostObj>ns1.lol.cat</domain:hostObj>
</rdeDomain:ns>
<rdeDomain:clID>TheRegistrar</rdeDomain:clID>
<rdeDomain:crRr>TheRegistrar</rdeDomain:crRr>
<rdeDomain:crDate>1970-01-01T00:00:00Z</rdeDomain:crDate>
<rdeDomain:exDate>294247-01-10T04:00:54Z</rdeDomain:exDate>
</rdeDomain:domain>""");
<rdeDomain:domain>
<rdeDomain:name>hello.soy</rdeDomain:name>
<rdeDomain:roid>E-SOY</rdeDomain:roid>
<rdeDomain:uName>hello.soy</rdeDomain:uName>
<rdeDomain:status s="ok"/>
<rdeDomain:registrant>contact1234</rdeDomain:registrant>
<rdeDomain:contact type="admin">contact789</rdeDomain:contact>
<rdeDomain:contact type="tech">contact1234</rdeDomain:contact>
<rdeDomain:ns>
<domain:hostObj>ns1.external.tld</domain:hostObj>
<domain:hostObj>ns1.lol.cat</domain:hostObj>
</rdeDomain:ns>
<rdeDomain:clID>TheRegistrar</rdeDomain:clID>
<rdeDomain:crRr>TheRegistrar</rdeDomain:crRr>
<rdeDomain:crDate>1970-01-01T00:00:00Z</rdeDomain:crDate>
<rdeDomain:exDate>294247-01-10T04:00:54Z</rdeDomain:exDate>
</rdeDomain:domain>""");
} else {
// Contact fragments for cat.fun.
assertThat(
@@ -471,20 +471,20 @@ public class RdePipelineTest {
assertThat(domainFrags.stream().findFirst().get().xml().strip())
.isEqualTo(
"""
<rdeDomain:domain>
<rdeDomain:name>hello.soy</rdeDomain:name>
<rdeDomain:roid>E-SOY</rdeDomain:roid>
<rdeDomain:uName>hello.soy</rdeDomain:uName>
<rdeDomain:status s="ok"/>
<rdeDomain:ns>
<domain:hostObj>ns1.external.tld</domain:hostObj>
<domain:hostObj>ns1.lol.cat</domain:hostObj>
</rdeDomain:ns>
<rdeDomain:clID>TheRegistrar</rdeDomain:clID>
<rdeDomain:crRr>TheRegistrar</rdeDomain:crRr>
<rdeDomain:crDate>1970-01-01T00:00:00Z</rdeDomain:crDate>
<rdeDomain:exDate>294247-01-10T04:00:54Z</rdeDomain:exDate>
</rdeDomain:domain>""");
<rdeDomain:domain>
<rdeDomain:name>hello.soy</rdeDomain:name>
<rdeDomain:roid>E-SOY</rdeDomain:roid>
<rdeDomain:uName>hello.soy</rdeDomain:uName>
<rdeDomain:status s="ok"/>
<rdeDomain:ns>
<domain:hostObj>ns1.external.tld</domain:hostObj>
<domain:hostObj>ns1.lol.cat</domain:hostObj>
</rdeDomain:ns>
<rdeDomain:clID>TheRegistrar</rdeDomain:clID>
<rdeDomain:crRr>TheRegistrar</rdeDomain:crRr>
<rdeDomain:crDate>1970-01-01T00:00:00Z</rdeDomain:crDate>
<rdeDomain:exDate>294247-01-10T04:00:54Z</rdeDomain:exDate>
</rdeDomain:domain>""");
}
});
return null;

View File

@@ -16,9 +16,9 @@ package google.registry.export;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.export.SyncGroupMembersAction.getGroupEmailAddressForContactType;
import static google.registry.model.registrar.RegistrarPocBase.Type.ADMIN;
import static google.registry.model.registrar.RegistrarPocBase.Type.MARKETING;
import static google.registry.model.registrar.RegistrarPocBase.Type.TECH;
import static google.registry.model.registrar.RegistrarPoc.Type.ADMIN;
import static google.registry.model.registrar.RegistrarPoc.Type.MARKETING;
import static google.registry.model.registrar.RegistrarPoc.Type.TECH;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.persistResource;

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