mirror of
https://github.com/google/nomulus
synced 2026-01-16 18:53:18 +00:00
Compare commits
52 Commits
nomulus-20
...
test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a67b04f3a | ||
|
|
9f191e9392 | ||
|
|
39c2a79898 | ||
|
|
e2e9d4cfc7 | ||
|
|
2948dcc1be | ||
|
|
c5644d5c8b | ||
|
|
514d24ed67 | ||
|
|
c6868b771b | ||
|
|
f34aec8b56 | ||
|
|
b27b077638 | ||
|
|
0e8cd75a58 | ||
|
|
2a1748ba9c | ||
|
|
f4889191a4 | ||
|
|
9eddecf70f | ||
|
|
d4bcff0c31 | ||
|
|
62065f88fb | ||
|
|
c9ac9437fd | ||
|
|
1f6a09182d | ||
|
|
a0eff00031 | ||
|
|
89698c6ed6 | ||
|
|
a7696c3fac | ||
|
|
7ec599f849 | ||
|
|
70291af9ad | ||
|
|
5fb95f38ed | ||
|
|
dfe8e24761 | ||
|
|
bd30fcc81c | ||
|
|
8cecc8d3a8 | ||
|
|
c5a39bccc5 | ||
|
|
a90a117341 | ||
|
|
b40ad54daf | ||
|
|
b4d239c329 | ||
|
|
daa7ab3bfa | ||
|
|
56cd2ad282 | ||
|
|
0472dda860 | ||
|
|
083a9dc8c9 | ||
|
|
0153c6284a | ||
|
|
ca240adfb6 | ||
|
|
b17125ae9a | ||
|
|
dfef733360 | ||
|
|
04a0659197 | ||
|
|
70010886b1 | ||
|
|
3cd50dc929 | ||
|
|
03872b508f | ||
|
|
1096f201cd | ||
|
|
9dc3215624 | ||
|
|
af321fb65e | ||
|
|
c5132c04be | ||
|
|
a64dc21f96 | ||
|
|
0381533a35 | ||
|
|
4999a72d96 | ||
|
|
2d072c3844 | ||
|
|
c15dec4419 |
@@ -90,7 +90,6 @@ explodeWar.doLast {
|
||||
|
||||
appengineDeployAll.mustRunAfter ':console-webapp:deploy'
|
||||
appengineDeployAll.finalizedBy ':deployCloudSchedulerAndQueue'
|
||||
rootProject.deploy.dependsOn appengineDeployAll
|
||||
|
||||
rootProject.stage.dependsOn appengineStage
|
||||
tasks['war'].dependsOn ':core:processResources'
|
||||
|
||||
@@ -101,16 +101,9 @@ task checkFormatting(type: Exec) {
|
||||
args 'run', 'prettify:check'
|
||||
}
|
||||
|
||||
task deploy(type: Exec) {
|
||||
workingDir "${consoleDir}/staged"
|
||||
executable 'gcloud'
|
||||
args 'app', 'deploy', "${projectParam}", '--quiet'
|
||||
}
|
||||
|
||||
tasks.buildConsoleWebapp.dependsOn(tasks.npmInstallDeps)
|
||||
tasks.runConsoleWebappUnitTests.dependsOn(tasks.npmInstallDeps)
|
||||
tasks.applyFormatting.dependsOn(tasks.npmInstallDeps)
|
||||
tasks.checkFormatting.dependsOn(tasks.npmInstallDeps)
|
||||
tasks.build.dependsOn(tasks.checkFormatting)
|
||||
tasks.build.dependsOn(tasks.runConsoleWebappUnitTests)
|
||||
tasks.deploy.dependsOn(tasks.buildConsoleWebapp)
|
||||
|
||||
@@ -24,8 +24,8 @@ import { ResourcesComponent } from './resources/resources.component';
|
||||
import ContactComponent from './settings/contact/contact.component';
|
||||
import SecurityComponent from './settings/security/security.component';
|
||||
import { SettingsComponent } from './settings/settings.component';
|
||||
import WhoisComponent from './settings/whois/whois.component';
|
||||
import { SupportComponent } from './support/support.component';
|
||||
import RdapComponent from './settings/rdap/rdap.component';
|
||||
|
||||
export interface RouteWithIcon extends Route {
|
||||
iconName?: string;
|
||||
@@ -83,9 +83,9 @@ export const routes: RouteWithIcon[] = [
|
||||
title: 'Contacts',
|
||||
},
|
||||
{
|
||||
path: WhoisComponent.PATH,
|
||||
component: WhoisComponent,
|
||||
title: 'WHOIS Info',
|
||||
path: RdapComponent.PATH,
|
||||
component: RdapComponent,
|
||||
title: 'RDAP Info',
|
||||
},
|
||||
{
|
||||
path: SecurityComponent.PATH,
|
||||
|
||||
@@ -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();
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -47,8 +47,6 @@ import EppPasswordEditComponent from './settings/security/eppPasswordEdit.compon
|
||||
import SecurityComponent from './settings/security/security.component';
|
||||
import SecurityEditComponent from './settings/security/securityEdit.component';
|
||||
import { SettingsComponent } from './settings/settings.component';
|
||||
import WhoisComponent from './settings/whois/whois.component';
|
||||
import WhoisEditComponent from './settings/whois/whoisEdit.component';
|
||||
import { NotificationsComponent } from './shared/components/notifications/notifications.component';
|
||||
import { SelectedRegistrarWrapper } from './shared/components/selectedRegistrarWrapper/selectedRegistrarWrapper.component';
|
||||
import { LocationBackDirective } from './shared/directives/locationBack.directive';
|
||||
@@ -60,6 +58,9 @@ import { SnackBarModule } from './snackbar.module';
|
||||
import { SupportComponent } from './support/support.component';
|
||||
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],
|
||||
@@ -76,30 +77,31 @@ export class SelectedRegistrarModule {}
|
||||
ContactDetailsComponent,
|
||||
DomainListComponent,
|
||||
EppPasswordEditComponent,
|
||||
ForceFocusDirective,
|
||||
HeaderComponent,
|
||||
HomeComponent,
|
||||
LocationBackDirective,
|
||||
ForceFocusDirective,
|
||||
UserLevelVisibility,
|
||||
NavigationComponent,
|
||||
NewRegistrarComponent,
|
||||
NotificationsComponent,
|
||||
RdapComponent,
|
||||
RdapEditComponent,
|
||||
ReasonDialogComponent,
|
||||
PocReminderComponent,
|
||||
RegistrarComponent,
|
||||
RegistrarDetailsComponent,
|
||||
RegistryLockComponent,
|
||||
RegistrarSelectorComponent,
|
||||
RegistryLockComponent,
|
||||
RegistryLockVerifyComponent,
|
||||
ResourcesComponent,
|
||||
ResponseDialogComponent,
|
||||
SecurityComponent,
|
||||
SecurityEditComponent,
|
||||
SettingsComponent,
|
||||
SettingsContactComponent,
|
||||
SupportComponent,
|
||||
TldsComponent,
|
||||
WhoisComponent,
|
||||
WhoisEditComponent,
|
||||
ReasonDialogComponent,
|
||||
ResponseDialogComponent,
|
||||
UserLevelVisibility,
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
imports: [
|
||||
@@ -108,8 +110,8 @@ export class SelectedRegistrarModule {}
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
MaterialModule,
|
||||
SnackBarModule,
|
||||
SelectedRegistrarModule,
|
||||
SnackBarModule,
|
||||
],
|
||||
providers: [
|
||||
BackendService,
|
||||
|
||||
@@ -57,7 +57,7 @@ export class NavigationComponent {
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subscription.unsubscribe();
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
getElementId(node: RouteWithIcon) {
|
||||
|
||||
@@ -48,7 +48,6 @@ export default class NewRegistrarComponent {
|
||||
this.newRegistrar = {
|
||||
registrarId: '',
|
||||
url: '',
|
||||
whoisServer: '',
|
||||
registrarName: '',
|
||||
icannReferralEmail: '',
|
||||
localizedAddress: {
|
||||
|
||||
@@ -50,17 +50,16 @@ export interface SecuritySettings
|
||||
ipAddressAllowList?: Array<IpAllowListItem>;
|
||||
}
|
||||
|
||||
export interface WhoisRegistrarFields {
|
||||
export interface RdapRegistrarFields {
|
||||
ianaIdentifier?: number;
|
||||
icannReferralEmail: string;
|
||||
localizedAddress: Address;
|
||||
registrarId: string;
|
||||
url: string;
|
||||
whoisServer: string;
|
||||
}
|
||||
|
||||
export interface Registrar
|
||||
extends WhoisRegistrarFields,
|
||||
extends RdapRegistrarFields,
|
||||
SecuritySettingsBackendModel {
|
||||
allowedTlds?: string[];
|
||||
billingAccountMap?: object;
|
||||
@@ -72,6 +71,7 @@ export interface Registrar
|
||||
registrarName: string;
|
||||
registryLockAllowed?: boolean;
|
||||
type?: string;
|
||||
lastPocVerificationDate?: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -35,7 +35,7 @@ export const contactTypeToTextMap: contactTypesToUserFriendlyTypes = {
|
||||
LEGAL: 'Legal contact',
|
||||
MARKETING: 'Marketing contact',
|
||||
TECH: 'Technical contact',
|
||||
WHOIS: 'WHOIS-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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
[required]="true"
|
||||
[(ngModel)]="contactService.contactInEdit.emailAddress"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
[disabled]="emailAddressIsDisabled()"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
@@ -85,24 +86,28 @@
|
||||
<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>
|
||||
|
||||
<section>
|
||||
<h1>WHOIS Preferences</h1>
|
||||
<h1>RDAP Preferences</h1>
|
||||
<div>
|
||||
<mat-checkbox
|
||||
[(ngModel)]="contactService.contactInEdit.visibleInWhoisAsAdmin"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
>Show in Registrar WHOIS record as admin contact</mat-checkbox
|
||||
>Show in Registrar RDAP record as admin contact</mat-checkbox
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -110,7 +115,7 @@
|
||||
<mat-checkbox
|
||||
[(ngModel)]="contactService.contactInEdit.visibleInWhoisAsTech"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
>Show in Registrar WHOIS record as technical contact</mat-checkbox
|
||||
>Show in Registrar RDAP record as technical contact</mat-checkbox
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -118,8 +123,8 @@
|
||||
<mat-checkbox
|
||||
[(ngModel)]="contactService.contactInEdit.visibleInDomainWhoisAsAbuse"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
>Show Phone and Email in Domain WHOIS Record as registrar abuse
|
||||
contact (per CL&D requirements)</mat-checkbox
|
||||
>Show Phone and Email in Domain RDAP Record as registrar abuse contact
|
||||
(per CL&D requirements)</mat-checkbox
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
@@ -176,13 +181,13 @@
|
||||
<mat-card-content>
|
||||
<mat-list role="list">
|
||||
<mat-list-item role="listitem">
|
||||
<h2>WHOIS Preferences</h2>
|
||||
<h2>RDAP Preferences</h2>
|
||||
</mat-list-item>
|
||||
@if(contactService.contactInEdit.visibleInWhoisAsAdmin) {
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-value"
|
||||
>Show in Registrar WHOIS record as admin contact</span
|
||||
>Show in Registrar RDAP record as admin contact</span
|
||||
>
|
||||
</mat-list-item>
|
||||
} @if(contactService.contactInEdit.visibleInWhoisAsTech) {
|
||||
@@ -192,14 +197,14 @@
|
||||
*ngIf="contactService.contactInEdit.visibleInWhoisAsTech"
|
||||
>
|
||||
<span class="console-app__list-value"
|
||||
>Show in Registrar WHOIS record as technical contact</span
|
||||
>Show in Registrar RDAP record as technical contact</span
|
||||
>
|
||||
</mat-list-item>
|
||||
} @if(contactService.contactInEdit.visibleInDomainWhoisAsAbuse) {
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-value"
|
||||
>Show Phone and Email in Domain WHOIS Record as registrar abuse
|
||||
>Show Phone and Email in Domain RDAP Record as registrar abuse
|
||||
contact (per CL&D requirements)</span
|
||||
>
|
||||
</mat-list-item>
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
@if(whoisService.editing) {
|
||||
<app-whois-edit></app-whois-edit>
|
||||
@if(rdapService.editing) {
|
||||
<app-rdap-edit></app-rdap-edit>
|
||||
} @else {
|
||||
<div class="console-app__whois">
|
||||
<div class="console-app__whois-controls">
|
||||
<div class="console-app__rdap">
|
||||
<div class="console-app__rdap-controls">
|
||||
<span>
|
||||
General registrar information for your WHOIS record. This information is
|
||||
always visible in WHOIS.
|
||||
General registrar information for your RDAP record. This information is
|
||||
always visible in RDAP.
|
||||
</span>
|
||||
<div class="spacer"></div>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
aria-label="Edit WHOIS record"
|
||||
(click)="whoisService.editing = true"
|
||||
aria-label="Edit RDAP record"
|
||||
(click)="rdapService.editing = true"
|
||||
>
|
||||
<mat-icon>edit</mat-icon>
|
||||
Edit
|
||||
@@ -61,45 +61,5 @@
|
||||
</mat-list>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<mat-card appearance="outlined">
|
||||
<mat-card-content>
|
||||
<mat-list role="list">
|
||||
<mat-list-item role="listitem">
|
||||
<h2>Technical Info</h2>
|
||||
</mat-list-item>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-key">IANA Identifier</span>
|
||||
<span class="console-app__list-value">{{
|
||||
registrarService.registrar()?.ianaIdentifier
|
||||
}}</span>
|
||||
</mat-list-item>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item role="listitem">
|
||||
<div>
|
||||
<span class="console-app__list-key">ICANN Referral Email</span>
|
||||
<span class="console-app__list-value">{{
|
||||
registrarService.registrar()?.icannReferralEmail
|
||||
}}</span>
|
||||
</div>
|
||||
</mat-list-item>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-key">WHOIS server</span>
|
||||
<span class="console-app__list-value">{{
|
||||
registrarService.registrar()?.whoisServer
|
||||
}}</span>
|
||||
</mat-list-item>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-key">Referral URL</span>
|
||||
<span class="console-app__list-value">{{
|
||||
registrarService.registrar()?.url
|
||||
}}</span>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
.console-app__whois {
|
||||
.console-app__rdap {
|
||||
max-width: 616px;
|
||||
|
||||
&-controls {
|
||||
@@ -20,15 +20,15 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { MaterialModule } from 'src/app/material.module';
|
||||
import { RegistrarService } from 'src/app/registrar/registrar.service';
|
||||
import { BackendService } from 'src/app/shared/services/backend.service';
|
||||
import WhoisComponent from './whois.component';
|
||||
import RdapComponent from './rdap.component';
|
||||
|
||||
describe('WhoisComponent', () => {
|
||||
let component: WhoisComponent;
|
||||
let fixture: ComponentFixture<WhoisComponent>;
|
||||
describe('RdapComponent', () => {
|
||||
let component: RdapComponent;
|
||||
let fixture: ComponentFixture<RdapComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [WhoisComponent],
|
||||
declarations: [RdapComponent],
|
||||
imports: [MaterialModule, BrowserAnimationsModule],
|
||||
providers: [
|
||||
BackendService,
|
||||
@@ -45,7 +45,7 @@ describe('WhoisComponent', () => {
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(WhoisComponent);
|
||||
fixture = TestBed.createComponent(RdapComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
@@ -14,17 +14,16 @@
|
||||
|
||||
import { Component, computed } from '@angular/core';
|
||||
import { RegistrarService } from 'src/app/registrar/registrar.service';
|
||||
|
||||
import { WhoisService } from './whois.service';
|
||||
import { RdapService } from './rdap.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-whois',
|
||||
templateUrl: './whois.component.html',
|
||||
styleUrls: ['./whois.component.scss'],
|
||||
selector: 'app-rdap',
|
||||
templateUrl: './rdap.component.html',
|
||||
styleUrls: ['./rdap.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export default class WhoisComponent {
|
||||
public static PATH = 'whois';
|
||||
export default class RdapComponent {
|
||||
public static PATH = 'rdap';
|
||||
formattedAddress = computed(() => {
|
||||
let result = '';
|
||||
const registrar = this.registrarService.registrar();
|
||||
@@ -47,7 +46,7 @@ export default class WhoisComponent {
|
||||
});
|
||||
|
||||
constructor(
|
||||
public whoisService: WhoisService,
|
||||
public rdapService: RdapService,
|
||||
public registrarService: RegistrarService
|
||||
) {}
|
||||
}
|
||||
@@ -16,14 +16,14 @@ import { Injectable } from '@angular/core';
|
||||
import { switchMap } from 'rxjs';
|
||||
import {
|
||||
RegistrarService,
|
||||
WhoisRegistrarFields,
|
||||
RdapRegistrarFields,
|
||||
} from 'src/app/registrar/registrar.service';
|
||||
import { BackendService } from 'src/app/shared/services/backend.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class WhoisService {
|
||||
export class RdapService {
|
||||
editing: boolean = false;
|
||||
|
||||
constructor(
|
||||
@@ -31,8 +31,8 @@ export class WhoisService {
|
||||
private registrarService: RegistrarService
|
||||
) {}
|
||||
|
||||
saveChanges(newWhoisRegistrarFields: WhoisRegistrarFields) {
|
||||
return this.backend.postWhoisRegistrarFields(newWhoisRegistrarFields).pipe(
|
||||
saveChanges(newRdapRegistrarFields: RdapRegistrarFields) {
|
||||
return this.backend.postRdapRegistrarFields(newRdapRegistrarFields).pipe(
|
||||
switchMap(() => {
|
||||
return this.registrarService.loadRegistrars();
|
||||
})
|
||||
@@ -1,27 +1,27 @@
|
||||
<div
|
||||
class="console-app__whois-edit"
|
||||
class="console-app__rdap-edit"
|
||||
*ngIf="registrarInEdit"
|
||||
cdkTrapFocus
|
||||
[cdkTrapFocusAutoCapture]="true"
|
||||
>
|
||||
<button
|
||||
mat-icon-button
|
||||
class="console-app__whois-edit-back"
|
||||
aria-label="Back to whois view"
|
||||
(click)="whoisService.editing = false"
|
||||
class="console-app__rdap-edit-back"
|
||||
aria-label="Back to rdap view"
|
||||
(click)="rdapService.editing = false"
|
||||
>
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
|
||||
<div class="console-app__whois-edit-controls">
|
||||
<div class="console-app__rdap-edit-controls">
|
||||
<span>
|
||||
General registrar information for your WHOIS record. This information is
|
||||
always visible in WHOIS.
|
||||
General registrar information for your RDAP record. This information is
|
||||
always visible in RDAP.
|
||||
</span>
|
||||
<div class="spacer"></div>
|
||||
</div>
|
||||
|
||||
<div class="console-app__whois-edit">
|
||||
<div class="console-app__rdap-edit">
|
||||
<h1>Personal info</h1>
|
||||
|
||||
<form (ngSubmit)="save($event)">
|
||||
@@ -115,45 +115,11 @@
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<h1>Technical info</h1>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>WHOIS server: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="registrarInEdit.whoisServer"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Referral URL: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="registrarInEdit.url"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
@if((userDataService.userData()?.globalRole || 'NONE') !== "NONE") {
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>ICANN Referral Email: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="registrarInEdit.icannReferralEmail"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
aria-label="Save WHOIS settings"
|
||||
aria-label="Save RDAO settings"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
@@ -1,4 +1,4 @@
|
||||
.console-app__whois-edit {
|
||||
.console-app__rdap-edit {
|
||||
max-width: 616px;
|
||||
|
||||
&-controls {
|
||||
@@ -20,20 +20,20 @@ import {
|
||||
RegistrarService,
|
||||
} from 'src/app/registrar/registrar.service';
|
||||
import { UserDataService } from 'src/app/shared/services/userData.service';
|
||||
import { WhoisService } from './whois.service';
|
||||
import { RdapService } from './rdap.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-whois-edit',
|
||||
templateUrl: './whoisEdit.component.html',
|
||||
styleUrls: ['./whoisEdit.component.scss'],
|
||||
selector: 'app-rdap-edit',
|
||||
templateUrl: './rdapEdit.component.html',
|
||||
styleUrls: ['./rdapEdit.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export default class WhoisEditComponent {
|
||||
export default class RdapEditComponent {
|
||||
registrarInEdit: Registrar | undefined;
|
||||
|
||||
constructor(
|
||||
public userDataService: UserDataService,
|
||||
public whoisService: WhoisService,
|
||||
public rdapService: RdapService,
|
||||
public registrarService: RegistrarService,
|
||||
private _snackBar: MatSnackBar
|
||||
) {
|
||||
@@ -49,9 +49,9 @@ export default class WhoisEditComponent {
|
||||
e.preventDefault();
|
||||
if (!this.registrarInEdit) return;
|
||||
|
||||
this.whoisService.saveChanges(this.registrarInEdit).subscribe({
|
||||
this.rdapService.saveChanges(this.registrarInEdit).subscribe({
|
||||
complete: () => {
|
||||
this.whoisService.editing = false;
|
||||
this.rdapService.editing = false;
|
||||
},
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error);
|
||||
@@ -19,13 +19,13 @@
|
||||
>
|
||||
<a
|
||||
mat-tab-link
|
||||
routerLink="whois"
|
||||
routerLink="rdap"
|
||||
routerLinkActive
|
||||
queryParamsHandling="merge"
|
||||
#rla2="routerLinkActive"
|
||||
[active]="rla2.isActive"
|
||||
aria-label="Access whois settings"
|
||||
>WHOIS Info</a
|
||||
aria-label="Access rdap settings"
|
||||
>RDAP Info</a
|
||||
>
|
||||
<a
|
||||
mat-tab-link
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,5 @@
|
||||
.console-app__pocReminder {
|
||||
a {
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ import { User } from 'src/app/users/users.service';
|
||||
import {
|
||||
Registrar,
|
||||
SecuritySettingsBackendModel,
|
||||
WhoisRegistrarFields,
|
||||
RdapRegistrarFields,
|
||||
} from '../../registrar/registrar.service';
|
||||
import { Contact } from '../../settings/contact/contact.service';
|
||||
import { EppPasswordBackendModel } from '../../settings/security/security.service';
|
||||
@@ -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),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -209,12 +222,12 @@ export class BackendService {
|
||||
.pipe(catchError((err) => this.errorCatcher<UserData>(err)));
|
||||
}
|
||||
|
||||
postWhoisRegistrarFields(
|
||||
whoisRegistrarFields: WhoisRegistrarFields
|
||||
): Observable<WhoisRegistrarFields> {
|
||||
return this.http.post<WhoisRegistrarFields>(
|
||||
'/console-api/settings/whois-fields',
|
||||
whoisRegistrarFields
|
||||
postRdapRegistrarFields(
|
||||
rdapRegistrarFields: RdapRegistrarFields
|
||||
): Observable<RdapRegistrarFields> {
|
||||
return this.http.post<RdapRegistrarFields>(
|
||||
'/console-api/settings/rdap-fields',
|
||||
rdapRegistrarFields
|
||||
);
|
||||
}
|
||||
|
||||
@@ -267,4 +280,15 @@ export class BackendService {
|
||||
`/console-api/registry-lock-verify?lockVerificationCode=${lockVerificationCode}`
|
||||
);
|
||||
}
|
||||
|
||||
requestRegistryLockPasswordReset(
|
||||
registrarId: string,
|
||||
registryLockEmail: string
|
||||
) {
|
||||
return this.http.post('/console-api/password-reset-request', {
|
||||
type: 'REGISTRY_LOCK',
|
||||
registrarId,
|
||||
registryLockEmail,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,15 @@
|
||||
roleToDescription(userDetails().role)
|
||||
}}</span>
|
||||
</mat-list-item>
|
||||
@if (userDetails().password) {
|
||||
@if (userDetails().registryLockEmailAddress) {
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-key">Registry Lock email</span>
|
||||
<span class="console-app__list-value">{{
|
||||
userDetails().registryLockEmailAddress
|
||||
}}</span>
|
||||
</mat-list-item>
|
||||
} @if (userDetails().password) {
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-key">Password</span>
|
||||
|
||||
@@ -35,5 +35,8 @@
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.console-app__list-key {
|
||||
width: 160px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,57 @@
|
||||
<form (ngSubmit)="saveEdit($event)" #form>
|
||||
<p *ngIf="isNew()">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label
|
||||
>User name prefix:
|
||||
<mat-icon
|
||||
matTooltip="Prefix will be combined with registrar ID to create a unique user name - {prefix}.{registrarId}@registry.google"
|
||||
>help_outline</mat-icon
|
||||
></mat-label
|
||||
>
|
||||
<input
|
||||
matInput
|
||||
minlength="3"
|
||||
maxlength="3"
|
||||
[required]="true"
|
||||
[(ngModel)]="user().emailAddress"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</p>
|
||||
<p>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label
|
||||
>User Role:
|
||||
<mat-icon
|
||||
matTooltip="Viewer role doesn't allow making updates; Editor role allows updates, like Contacts delete or SSL certificate change"
|
||||
>help_outline</mat-icon
|
||||
></mat-label
|
||||
>
|
||||
<mat-select [(ngModel)]="user().role" name="userRole">
|
||||
<mat-option value="PRIMARY_CONTACT">Editor</mat-option>
|
||||
<mat-option value="ACCOUNT_MANAGER">Viewer</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</p>
|
||||
<div class="console-app__user-edit">
|
||||
<form (ngSubmit)="saveEdit($event)" #form>
|
||||
<p *ngIf="isNew()">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label
|
||||
>User name prefix:
|
||||
<mat-icon
|
||||
matTooltip="Prefix will be combined with registrar ID to create a unique user name - {prefix}.{registrarId}@registry.google"
|
||||
>help_outline</mat-icon
|
||||
></mat-label
|
||||
>
|
||||
<input
|
||||
matInput
|
||||
minlength="3"
|
||||
maxlength="3"
|
||||
[required]="true"
|
||||
[(ngModel)]="user().emailAddress"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</p>
|
||||
<p>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label
|
||||
>User Role:
|
||||
<mat-icon
|
||||
matTooltip="Viewer role doesn't allow making updates; Editor role allows updates, like Contacts delete or SSL certificate change"
|
||||
>help_outline</mat-icon
|
||||
></mat-label
|
||||
>
|
||||
<mat-select [(ngModel)]="user().role" name="userRole">
|
||||
<mat-option value="PRIMARY_CONTACT">Editor</mat-option>
|
||||
<mat-option value="ACCOUNT_MANAGER">Viewer</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</p>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
aria-label="Save user"
|
||||
type="submit"
|
||||
aria-label="Save changes to the user"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</form>
|
||||
@if(userDataService.userData()?.isAdmin) {
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
aria-label="Save user"
|
||||
type="submit"
|
||||
aria-label="Save changes to the user"
|
||||
aria-label="Reset registry lock password"
|
||||
(click)="requestRegistryLockPasswordReset()"
|
||||
>
|
||||
Save
|
||||
Reset registry lock password
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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.
|
||||
|
||||
.console-app__user-edit {
|
||||
button {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,56 @@ import {
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Inject,
|
||||
input,
|
||||
Output,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { MaterialModule } from '../material.module';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { User } from './users.service';
|
||||
import { User, UsersService } from './users.service';
|
||||
import { UserDataService } from '../shared/services/userData.service';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialog,
|
||||
MatDialogRef,
|
||||
} from '@angular/material/dialog';
|
||||
import { filter, switchMap, take } from 'rxjs';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
|
||||
@Component({
|
||||
selector: 'app-reset-lock-password-dialog',
|
||||
template: `
|
||||
<h2 mat-dialog-title>Please confirm the password reset:</h2>
|
||||
<mat-dialog-content>
|
||||
This will send a registry lock password reset email to
|
||||
{{ data.registryLockEmailAddress }}.
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button (click)="onCancel()">Cancel</button>
|
||||
<button mat-button color="warn" (click)="onSave()">Confirm</button>
|
||||
</mat-dialog-actions>
|
||||
`,
|
||||
imports: [CommonModule, MaterialModule],
|
||||
})
|
||||
export class ResetRegistryLockPasswordComponent {
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<ResetRegistryLockPasswordComponent>,
|
||||
@Inject(MAT_DIALOG_DATA)
|
||||
public data: { registryLockEmailAddress: string }
|
||||
) {}
|
||||
|
||||
onSave(): void {
|
||||
this.dialogRef.close(true);
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-edit-form',
|
||||
@@ -39,12 +82,22 @@ export class UserEditFormComponent {
|
||||
{
|
||||
emailAddress: '',
|
||||
role: 'ACCOUNT_MANAGER',
|
||||
registryLockEmailAddress: '',
|
||||
},
|
||||
{ transform: (user: User) => structuredClone(user) }
|
||||
);
|
||||
|
||||
@Output() onEditComplete = new EventEmitter<User>();
|
||||
|
||||
constructor(
|
||||
protected userDataService: UserDataService,
|
||||
private backendService: BackendService,
|
||||
private resetRegistryLockPasswordDialog: MatDialog,
|
||||
private registrarService: RegistrarService,
|
||||
private usersService: UsersService,
|
||||
private _snackBar: MatSnackBar
|
||||
) {}
|
||||
|
||||
saveEdit(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
if (this.form.nativeElement.checkValidity()) {
|
||||
@@ -53,4 +106,34 @@ export class UserEditFormComponent {
|
||||
this.form.nativeElement.reportValidity();
|
||||
}
|
||||
}
|
||||
|
||||
sendRegistryLockPasswordResetRequest() {
|
||||
return this.backendService.requestRegistryLockPasswordReset(
|
||||
this.registrarService.registrarId(),
|
||||
this.user().registryLockEmailAddress!
|
||||
);
|
||||
}
|
||||
|
||||
requestRegistryLockPasswordReset() {
|
||||
const dialogRef = this.resetRegistryLockPasswordDialog.open(
|
||||
ResetRegistryLockPasswordComponent,
|
||||
{
|
||||
data: {
|
||||
registryLockEmailAddress: this.user().registryLockEmailAddress,
|
||||
},
|
||||
}
|
||||
);
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
take(1),
|
||||
filter((result) => !!result)
|
||||
)
|
||||
.pipe(switchMap((_) => this.sendRegistryLockPasswordResetRequest()))
|
||||
.subscribe({
|
||||
next: (_) => this.usersService.currentlyOpenUserEmail.set(''),
|
||||
error: (err: HttpErrorResponse) =>
|
||||
this._snackBar.open(err.error || err.message),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface User {
|
||||
emailAddress: string;
|
||||
role: string;
|
||||
password?: string;
|
||||
registryLockEmailAddress?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -17,7 +17,7 @@ import java.util.Optional
|
||||
|
||||
plugins {
|
||||
id 'java-library'
|
||||
id "org.flywaydb.flyway" version "11.0.1"
|
||||
id "org.flywaydb.flyway" version "9.22.3"
|
||||
id 'maven-publish'
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -243,7 +243,7 @@ hibernate:
|
||||
# that BEAM pipelines are not subject to the maximumPoolSize value defined
|
||||
# here. See PersistenceModule.java for more information.
|
||||
hikariMinimumIdle: 1
|
||||
hikariMaximumPoolSize: 10
|
||||
hikariMaximumPoolSize: 20
|
||||
hikariIdleTimeout: 300000
|
||||
# The batch size is basically the number of insertions / updates in a single
|
||||
# transaction that will be batched together into one INSERT/UPDATE statement.
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 {}
|
||||
@@ -15,6 +15,7 @@
|
||||
package google.registry.export;
|
||||
|
||||
import static com.google.common.base.Verify.verifyNotNull;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.INCLUDE_PENDING_DELETE_DATE_FOR_DOMAINS;
|
||||
import static google.registry.model.tld.Tlds.getTldsOfType;
|
||||
import static google.registry.persistence.PersistenceModule.TransactionIsolationLevel.TRANSACTION_REPEATABLE_READ;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
|
||||
@@ -75,7 +76,8 @@ public class ExportDomainListsAction implements Runnable {
|
||||
ON d.repo_id = gp.domain_repo_id
|
||||
WHERE d.tld = :tld
|
||||
AND d.deletion_time > CAST(:now AS timestamptz)
|
||||
ORDER BY d.domain_name""";
|
||||
ORDER BY d.domain_name
|
||||
""";
|
||||
|
||||
// This may be a CSV, but it is uses a .txt file extension for back-compatibility
|
||||
static final String REGISTERED_DOMAINS_FILENAME_FORMAT = "registered_domains_%s.txt";
|
||||
@@ -93,10 +95,7 @@ public class ExportDomainListsAction implements Runnable {
|
||||
logger.atInfo().log("Exporting domain lists for TLDs %s.", realTlds);
|
||||
|
||||
boolean includeDeletionTimes =
|
||||
tm().transact(
|
||||
() ->
|
||||
FeatureFlag.isActiveNowOrElse(
|
||||
FeatureFlag.FeatureName.INCLUDE_PENDING_DELETE_DATE_FOR_DOMAINS, false));
|
||||
tm().transact(() -> FeatureFlag.isActiveNow(INCLUDE_PENDING_DELETE_DATE_FOR_DOMAINS));
|
||||
realTlds.forEach(
|
||||
tld -> {
|
||||
List<String> domainsList =
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.flows;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import google.registry.request.Response;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A metadata class that saves the data directly in cookies.
|
||||
*
|
||||
* <p>Unlike {@link HttpSessionMetadata}, this class does not rely on a session manager to translate
|
||||
* an opaque session cookie into the metadata. This means that the locality of the session manager
|
||||
* is irrelevant and as long as the client (the proxy) respects the {@code Set-Cookie} headers and
|
||||
* sets the respective cookies in subsequent requests in a session, the metadata will be available
|
||||
* to all servers, not just the one that created the session.
|
||||
*
|
||||
* <p>The string representation of the metadata is saved in Base64 URL-safe format in a cookie named
|
||||
* {@code SESSION_INFO}.
|
||||
*/
|
||||
public class CookieSessionMetadata extends SessionMetadata {
|
||||
|
||||
protected static final String COOKIE_NAME = "SESSION_INFO";
|
||||
protected static final String REGISTRAR_ID = "clientId";
|
||||
protected static final String SERVICE_EXTENSIONS = "serviceExtensionUris";
|
||||
protected static final String FAILED_LOGIN_ATTEMPTS = "failedLoginAttempts";
|
||||
|
||||
private static final Pattern COOKIE_PATTERN = Pattern.compile("SESSION_INFO=([^;\\s]+)?");
|
||||
private static final Pattern REGISTRAR_ID_PATTERN = Pattern.compile("clientId=([^,\\s]+)?");
|
||||
private static final Pattern SERVICE_EXTENSIONS_PATTERN =
|
||||
Pattern.compile("serviceExtensionUris=([^,\\s}]+)?");
|
||||
private static final Pattern FAILED_LOGIN_ATTEMPTS_PATTERN =
|
||||
Pattern.compile("failedLoginAttempts=([^,\\s]+)?");
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
private final Map<String, String> data = new HashMap<>();
|
||||
|
||||
public CookieSessionMetadata(HttpServletRequest request) {
|
||||
Optional.ofNullable(request.getHeader("Cookie"))
|
||||
.ifPresent(
|
||||
cookie -> {
|
||||
Matcher matcher = COOKIE_PATTERN.matcher(cookie);
|
||||
if (matcher.find()) {
|
||||
String sessionInfo = decode(matcher.group(1));
|
||||
logger.atInfo().log("SESSION INFO: %s", sessionInfo);
|
||||
matcher = REGISTRAR_ID_PATTERN.matcher(sessionInfo);
|
||||
if (matcher.find()) {
|
||||
String registrarId = matcher.group(1);
|
||||
if (!registrarId.equals("null")) {
|
||||
data.put(REGISTRAR_ID, registrarId);
|
||||
}
|
||||
}
|
||||
matcher = SERVICE_EXTENSIONS_PATTERN.matcher(sessionInfo);
|
||||
if (matcher.find()) {
|
||||
String serviceExtensions = matcher.group(1);
|
||||
if (serviceExtensions != null) {
|
||||
data.put(SERVICE_EXTENSIONS, serviceExtensions);
|
||||
}
|
||||
}
|
||||
matcher = FAILED_LOGIN_ATTEMPTS_PATTERN.matcher(sessionInfo);
|
||||
if (matcher.find()) {
|
||||
String failedLoginAttempts = matcher.group(1);
|
||||
data.put(FAILED_LOGIN_ATTEMPTS, failedLoginAttempts);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidate() {
|
||||
data.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRegistrarId() {
|
||||
return data.getOrDefault(REGISTRAR_ID, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getServiceExtensionUris() {
|
||||
return Optional.ofNullable(data.getOrDefault(SERVICE_EXTENSIONS, null))
|
||||
.map(s -> Splitter.on(URI_SEPARATOR).splitToList(s))
|
||||
.map(ImmutableSet::copyOf)
|
||||
.orElse(ImmutableSet.of());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFailedLoginAttempts() {
|
||||
return Optional.ofNullable(data.getOrDefault(FAILED_LOGIN_ATTEMPTS, null))
|
||||
.map(Integer::parseInt)
|
||||
.orElse(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRegistrarId(String registrarId) {
|
||||
data.put(REGISTRAR_ID, registrarId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setServiceExtensionUris(Set<String> serviceExtensionUris) {
|
||||
if (serviceExtensionUris == null || serviceExtensionUris.isEmpty()) {
|
||||
data.remove(SERVICE_EXTENSIONS);
|
||||
} else {
|
||||
data.put(SERVICE_EXTENSIONS, Joiner.on(URI_SEPARATOR).join(serviceExtensionUris));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void incrementFailedLoginAttempts() {
|
||||
data.put(FAILED_LOGIN_ATTEMPTS, String.valueOf(getFailedLoginAttempts() + 1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetFailedLoginAttempts() {
|
||||
data.remove(FAILED_LOGIN_ATTEMPTS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(Response response) {
|
||||
String value = encode(toString());
|
||||
response.setHeader("Set-Cookie", COOKIE_NAME + "=" + value);
|
||||
}
|
||||
|
||||
protected static String encode(String plainText) {
|
||||
return BaseEncoding.base64Url().encode(plainText.getBytes(US_ASCII));
|
||||
}
|
||||
|
||||
protected static String decode(String cipherText) {
|
||||
return new String(BaseEncoding.base64Url().decode(cipherText), US_ASCII);
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,8 @@ public class EppRequestHandler {
|
||||
} catch (Exception e) {
|
||||
logger.atWarning().withCause(e).log("handleEppCommand general exception.");
|
||||
response.setStatus(SC_BAD_REQUEST);
|
||||
} finally {
|
||||
sessionMetadata.save(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import google.registry.request.Action.Method;
|
||||
import google.registry.request.Payload;
|
||||
import google.registry.request.auth.Auth;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* Establishes a transport for EPP+TLS over HTTP. All commands and responses are EPP XML according
|
||||
@@ -35,18 +35,18 @@ public class EppTlsAction implements Runnable {
|
||||
|
||||
@Inject @Payload byte[] inputXmlBytes;
|
||||
@Inject TlsCredentials tlsCredentials;
|
||||
@Inject HttpSession session;
|
||||
@Inject HttpServletRequest request;
|
||||
@Inject EppRequestHandler eppRequestHandler;
|
||||
@Inject EppTlsAction() {}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
eppRequestHandler.executeEpp(
|
||||
new HttpSessionMetadata(session),
|
||||
new CookieSessionMetadata(request),
|
||||
tlsCredentials,
|
||||
EppRequestSource.TLS,
|
||||
false, // This endpoint is never a dry run.
|
||||
false, // This endpoint is never a superuser.
|
||||
false, // This endpoint is never a dry run.
|
||||
false, // This endpoint is never a superuser.
|
||||
inputXmlBytes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,16 +14,14 @@
|
||||
|
||||
package google.registry.flows;
|
||||
|
||||
import static com.google.common.base.MoreObjects.toStringHelper;
|
||||
import static google.registry.util.CollectionUtils.nullToEmpty;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/** A metadata class that is a wrapper around {@link HttpSession}. */
|
||||
public class HttpSessionMetadata implements SessionMetadata {
|
||||
public class HttpSessionMetadata extends SessionMetadata {
|
||||
|
||||
private static final String REGISTRAR_ID = "REGISTRAR_ID";
|
||||
private static final String SERVICE_EXTENSIONS = "SERVICE_EXTENSIONS";
|
||||
@@ -75,13 +73,4 @@ public class HttpSessionMetadata implements SessionMetadata {
|
||||
public void resetFailedLoginAttempts() {
|
||||
session.removeAttribute(FAILED_LOGIN_ATTEMPTS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toStringHelper(getClass())
|
||||
.add("clientId", getRegistrarId())
|
||||
.add("failedLoginAttempts", getFailedLoginAttempts())
|
||||
.add("serviceExtensionUris", Joiner.on('.').join(nullToEmpty(getServiceExtensionUris())))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,29 +14,49 @@
|
||||
|
||||
package google.registry.flows;
|
||||
|
||||
import static com.google.common.base.MoreObjects.toStringHelper;
|
||||
import static google.registry.util.CollectionUtils.nullToEmpty;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import google.registry.request.Response;
|
||||
import java.util.Set;
|
||||
|
||||
/** Object to allow setting and retrieving session information in flows. */
|
||||
public interface SessionMetadata {
|
||||
public abstract class SessionMetadata {
|
||||
|
||||
protected static final char URI_SEPARATOR = '|';
|
||||
|
||||
/**
|
||||
* Invalidates the session. A new instance must be created after this for future sessions.
|
||||
* Attempts to invoke methods of this class after this method has been called will throw
|
||||
* {@code IllegalStateException}.
|
||||
* Attempts to invoke methods of this class after this method has been called will throw {@code
|
||||
* IllegalStateException}.
|
||||
*/
|
||||
void invalidate();
|
||||
public abstract void invalidate();
|
||||
|
||||
String getRegistrarId();
|
||||
public abstract String getRegistrarId();
|
||||
|
||||
Set<String> getServiceExtensionUris();
|
||||
public abstract Set<String> getServiceExtensionUris();
|
||||
|
||||
int getFailedLoginAttempts();
|
||||
public abstract int getFailedLoginAttempts();
|
||||
|
||||
void setRegistrarId(String registrarId);
|
||||
public abstract void setRegistrarId(String registrarId);
|
||||
|
||||
void setServiceExtensionUris(Set<String> serviceExtensionUris);
|
||||
public abstract void setServiceExtensionUris(Set<String> serviceExtensionUris);
|
||||
|
||||
void incrementFailedLoginAttempts();
|
||||
public abstract void incrementFailedLoginAttempts();
|
||||
|
||||
void resetFailedLoginAttempts();
|
||||
public abstract void resetFailedLoginAttempts();
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toStringHelper(getClass())
|
||||
.add("clientId", getRegistrarId())
|
||||
.add("failedLoginAttempts", getFailedLoginAttempts())
|
||||
.add(
|
||||
"serviceExtensionUris",
|
||||
Joiner.on(URI_SEPARATOR).join(nullToEmpty(getServiceExtensionUris())))
|
||||
.toString();
|
||||
}
|
||||
|
||||
public void save(Response response) {}
|
||||
}
|
||||
|
||||
@@ -14,16 +14,13 @@
|
||||
|
||||
package google.registry.flows;
|
||||
|
||||
import static com.google.common.base.MoreObjects.toStringHelper;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static google.registry.util.CollectionUtils.nullToEmpty;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import java.util.Set;
|
||||
|
||||
/** A read-only {@link SessionMetadata} that doesn't support login/logout. */
|
||||
public class StatelessRequestSessionMetadata implements SessionMetadata {
|
||||
public class StatelessRequestSessionMetadata extends SessionMetadata {
|
||||
|
||||
private final String registrarId;
|
||||
private final ImmutableSet<String> serviceExtensionUris;
|
||||
@@ -74,13 +71,6 @@ public class StatelessRequestSessionMetadata implements SessionMetadata {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toStringHelper(getClass())
|
||||
.add("clientId", getRegistrarId())
|
||||
.add("failedLoginAttempts", getFailedLoginAttempts())
|
||||
.add("serviceExtensionUris", Joiner.on('.').join(nullToEmpty(getServiceExtensionUris())))
|
||||
.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import static google.registry.flows.ResourceFlowUtils.verifyResourceDoesNotExist
|
||||
import static google.registry.flows.contact.ContactFlowUtils.validateAsciiPostalInfo;
|
||||
import static google.registry.flows.contact.ContactFlowUtils.validateContactAgainstPolicy;
|
||||
import static google.registry.model.EppResourceUtils.createRepoId;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
@@ -29,8 +30,10 @@ import google.registry.flows.FlowModule.RegistrarId;
|
||||
import google.registry.flows.FlowModule.TargetId;
|
||||
import google.registry.flows.MutatingFlow;
|
||||
import google.registry.flows.annotations.ReportingSpec;
|
||||
import google.registry.flows.exceptions.ContactsProhibitedException;
|
||||
import google.registry.flows.exceptions.ResourceAlreadyExistsForThisClientException;
|
||||
import google.registry.flows.exceptions.ResourceCreateContentionException;
|
||||
import google.registry.model.common.FeatureFlag;
|
||||
import google.registry.model.contact.Contact;
|
||||
import google.registry.model.contact.ContactCommand.Create;
|
||||
import google.registry.model.contact.ContactHistory;
|
||||
@@ -47,6 +50,7 @@ import org.joda.time.DateTime;
|
||||
* An EPP flow that creates a new contact.
|
||||
*
|
||||
* @error {@link google.registry.flows.FlowUtils.NotLoggedInException}
|
||||
* @error {@link ContactsProhibitedException}
|
||||
* @error {@link ResourceAlreadyExistsForThisClientException}
|
||||
* @error {@link ResourceCreateContentionException}
|
||||
* @error {@link ContactFlowUtils.BadInternationalizedPostalInfoException}
|
||||
@@ -69,6 +73,9 @@ public final class ContactCreateFlow implements MutatingFlow {
|
||||
extensionManager.register(MetadataExtension.class);
|
||||
validateRegistrarIsLoggedIn(registrarId);
|
||||
extensionManager.validate();
|
||||
if (FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_PROHIBITED)) {
|
||||
throw new ContactsProhibitedException();
|
||||
}
|
||||
Create command = (Create) resourceCommand;
|
||||
DateTime now = tm().getTransactionTime();
|
||||
verifyResourceDoesNotExist(Contact.class, targetId, now, registrarId);
|
||||
|
||||
@@ -24,6 +24,7 @@ import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfo;
|
||||
import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
|
||||
import static google.registry.flows.contact.ContactFlowUtils.validateAsciiPostalInfo;
|
||||
import static google.registry.flows.contact.ContactFlowUtils.validateContactAgainstPolicy;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED;
|
||||
import static google.registry.model.reporting.HistoryEntry.Type.CONTACT_UPDATE;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
@@ -35,7 +36,9 @@ import google.registry.flows.FlowModule.Superuser;
|
||||
import google.registry.flows.FlowModule.TargetId;
|
||||
import google.registry.flows.MutatingFlow;
|
||||
import google.registry.flows.annotations.ReportingSpec;
|
||||
import google.registry.flows.exceptions.ContactsProhibitedException;
|
||||
import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException;
|
||||
import google.registry.model.common.FeatureFlag;
|
||||
import google.registry.model.contact.Contact;
|
||||
import google.registry.model.contact.ContactCommand.Update;
|
||||
import google.registry.model.contact.ContactCommand.Update.Change;
|
||||
@@ -55,6 +58,7 @@ import org.joda.time.DateTime;
|
||||
/**
|
||||
* An EPP flow that updates a contact.
|
||||
*
|
||||
* @error {@link ContactsProhibitedException}
|
||||
* @error {@link google.registry.flows.FlowUtils.NotLoggedInException}
|
||||
* @error {@link google.registry.flows.ResourceFlowUtils.AddRemoveSameValueException}
|
||||
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException}
|
||||
@@ -92,6 +96,9 @@ public final class ContactUpdateFlow implements MutatingFlow {
|
||||
extensionManager.register(MetadataExtension.class);
|
||||
validateRegistrarIsLoggedIn(registrarId);
|
||||
extensionManager.validate();
|
||||
if (FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_PROHIBITED)) {
|
||||
throw new ContactsProhibitedException();
|
||||
}
|
||||
Update command = (Update) resourceCommand;
|
||||
DateTime now = tm().getTransactionTime();
|
||||
Contact existingContact = loadAndVerifyExistence(Contact.class, targetId, now);
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
@@ -72,7 +72,9 @@ import google.registry.flows.custom.DomainCreateFlowCustomLogic;
|
||||
import google.registry.flows.custom.DomainCreateFlowCustomLogic.BeforeResponseParameters;
|
||||
import google.registry.flows.custom.DomainCreateFlowCustomLogic.BeforeResponseReturnData;
|
||||
import google.registry.flows.custom.EntityChanges;
|
||||
import google.registry.flows.domain.DomainFlowUtils.RegistrantProhibitedException;
|
||||
import google.registry.flows.domain.token.AllocationTokenFlowUtils;
|
||||
import google.registry.flows.exceptions.ContactsProhibitedException;
|
||||
import google.registry.flows.exceptions.ResourceAlreadyExistsForThisClientException;
|
||||
import google.registry.flows.exceptions.ResourceCreateContentionException;
|
||||
import google.registry.model.ImmutableObject;
|
||||
@@ -133,11 +135,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}
|
||||
@@ -150,6 +149,7 @@ import org.joda.time.Duration;
|
||||
* @error {@link DomainCreateFlow.NoGeneralRegistrationsInCurrentPhaseException}
|
||||
* @error {@link DomainCreateFlow.NoTrademarkedRegistrationsBeforeSunriseException}
|
||||
* @error {@link BulkDomainRegisteredForTooManyYearsException}
|
||||
* @error {@link ContactsProhibitedException}
|
||||
* @error {@link DomainCreateFlow.SignedMarksOnlyDuringSunriseException}
|
||||
* @error {@link DomainFlowTmchUtils.NoMarksFoundMatchingDomainException}
|
||||
* @error {@link DomainFlowTmchUtils.FoundMarkNotYetValidException}
|
||||
@@ -197,6 +197,7 @@ import org.joda.time.Duration;
|
||||
* @error {@link DomainFlowUtils.NameserversNotSpecifiedForTldWithNameserverAllowListException}
|
||||
* @error {@link DomainFlowUtils.PremiumNameBlockedException}
|
||||
* @error {@link DomainFlowUtils.RegistrantNotAllowedException}
|
||||
* @error {@link RegistrantProhibitedException}
|
||||
* @error {@link DomainFlowUtils.RegistrarMustBeActiveForThisOperationException}
|
||||
* @error {@link DomainFlowUtils.TldDoesNotExistException}
|
||||
* @error {@link DomainFlowUtils.TooManyDsRecordsException}
|
||||
@@ -205,7 +206,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 +221,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;
|
||||
@@ -249,7 +248,7 @@ public final class DomainCreateFlow implements MutatingFlow {
|
||||
verifyResourceDoesNotExist(Domain.class, targetId, now, registrarId);
|
||||
// Validate that this is actually a legal domain name on a TLD that the registrar has access to.
|
||||
InternetDomainName domainName = validateDomainName(command.getDomainName());
|
||||
String domainLabel = domainName.parts().get(0);
|
||||
String domainLabel = domainName.parts().getFirst();
|
||||
Tld tld = Tld.get(domainName.parent().toString());
|
||||
validateCreateCommandContactsAndNameservers(command, tld, domainName);
|
||||
TldState tldState = tld.getTldState(now);
|
||||
@@ -264,25 +263,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 +410,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()) {
|
||||
|
||||
@@ -25,7 +25,7 @@ import static com.google.common.collect.Sets.intersection;
|
||||
import static com.google.common.collect.Sets.union;
|
||||
import static google.registry.bsa.persistence.BsaLabelUtils.isLabelBlocked;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL;
|
||||
import static google.registry.model.common.FeatureFlag.isActiveNow;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED;
|
||||
import static google.registry.model.domain.Domain.MAX_REGISTRATION_YEARS;
|
||||
import static google.registry.model.domain.token.AllocationToken.TokenType.REGISTER_BSA;
|
||||
import static google.registry.model.tld.Tld.TldState.GENERAL_AVAILABILITY;
|
||||
@@ -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,13 +75,13 @@ 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.ContactsProhibitedException;
|
||||
import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException;
|
||||
import google.registry.model.EppResource;
|
||||
import google.registry.model.billing.BillingBase.Flag;
|
||||
import google.registry.model.billing.BillingBase.Reason;
|
||||
import google.registry.model.billing.BillingRecurrence;
|
||||
import google.registry.model.common.FeatureFlag;
|
||||
import google.registry.model.contact.Contact;
|
||||
import google.registry.model.domain.DesignatedContact;
|
||||
import google.registry.model.domain.DesignatedContact.Type;
|
||||
@@ -101,7 +99,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 +121,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 +419,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)
|
||||
@@ -483,27 +480,37 @@ public class DomainFlowUtils {
|
||||
}
|
||||
}
|
||||
|
||||
static void validateRequiredContactsPresentIfRequiredForDataset(
|
||||
/**
|
||||
* Enforces the presence/absence of contact data depending on the minimum data set migration
|
||||
* schedule.
|
||||
*/
|
||||
static void validateContactDataPresence(
|
||||
Optional<VKey<Contact>> registrant, Set<DesignatedContact> contacts)
|
||||
throws RequiredParameterMissingException {
|
||||
// TODO(b/353347632): Change this flag check to a registry config check.
|
||||
if (isActiveNow(MINIMUM_DATASET_CONTACTS_OPTIONAL)) {
|
||||
// Contacts are not required once we have begun the migration to the minimum dataset
|
||||
return;
|
||||
}
|
||||
if (registrant.isEmpty()) {
|
||||
throw new MissingRegistrantException();
|
||||
}
|
||||
throws RequiredParameterMissingException, ParameterValuePolicyErrorException {
|
||||
// TODO(b/353347632): Change these flag checks to a registry config check once minimum data set
|
||||
// migration is completed.
|
||||
if (FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_PROHIBITED)) {
|
||||
if (registrant.isPresent()) {
|
||||
throw new RegistrantProhibitedException();
|
||||
}
|
||||
if (!contacts.isEmpty()) {
|
||||
throw new ContactsProhibitedException();
|
||||
}
|
||||
} else if (!FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_OPTIONAL)) {
|
||||
if (registrant.isEmpty()) {
|
||||
throw new MissingRegistrantException();
|
||||
}
|
||||
|
||||
Set<Type> roles = new HashSet<>();
|
||||
for (DesignatedContact contact : contacts) {
|
||||
roles.add(contact.getType());
|
||||
}
|
||||
if (!roles.contains(Type.ADMIN)) {
|
||||
throw new MissingAdminContactException();
|
||||
}
|
||||
if (!roles.contains(Type.TECH)) {
|
||||
throw new MissingTechnicalContactException();
|
||||
Set<Type> roles = new HashSet<>();
|
||||
for (DesignatedContact contact : contacts) {
|
||||
roles.add(contact.getType());
|
||||
}
|
||||
if (!roles.contains(Type.ADMIN)) {
|
||||
throw new MissingAdminContactException();
|
||||
}
|
||||
if (!roles.contains(Type.TECH)) {
|
||||
throw new MissingTechnicalContactException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1047,8 +1054,7 @@ public class DomainFlowUtils {
|
||||
String tldStr = tld.getTldStr();
|
||||
validateRegistrantAllowedOnTld(tldStr, command.getRegistrantContactId());
|
||||
validateNoDuplicateContacts(command.getContacts());
|
||||
validateRequiredContactsPresentIfRequiredForDataset(
|
||||
command.getRegistrant(), command.getContacts());
|
||||
validateContactDataPresence(command.getRegistrant(), command.getContacts());
|
||||
ImmutableSet<String> hostNames = command.getNameserverHostNames();
|
||||
validateNameserversCountForTld(tldStr, domainName, hostNames.size());
|
||||
validateNameserversAllowedOnTld(tldStr, hostNames);
|
||||
@@ -1233,52 +1239,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) {
|
||||
@@ -1418,6 +1378,13 @@ public class DomainFlowUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/** Having a registrant is prohibited by registry policy. */
|
||||
static class RegistrantProhibitedException extends ParameterValuePolicyErrorException {
|
||||
public RegistrantProhibitedException() {
|
||||
super("Having a registrant is prohibited by registry policy");
|
||||
}
|
||||
}
|
||||
|
||||
/** Admin contact is required. */
|
||||
static class MissingAdminContactException extends RequiredParameterMissingException {
|
||||
public MissingAdminContactException() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -30,6 +30,7 @@ import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.cloneAndLinkReferences;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.updateDsData;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.validateContactDataPresence;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.validateContactsHaveTypes;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.validateDsData;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.validateFeesAckedIfPresent;
|
||||
@@ -37,11 +38,10 @@ import static google.registry.flows.domain.DomainFlowUtils.validateNameserversAl
|
||||
import static google.registry.flows.domain.DomainFlowUtils.validateNameserversCountForTld;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.validateNoDuplicateContacts;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.validateRegistrantAllowedOnTld;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.validateRequiredContactsPresentIfRequiredForDataset;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyClientUpdateNotProhibited;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyNotInPendingDelete;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL;
|
||||
import static google.registry.model.common.FeatureFlag.isActiveNow;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED;
|
||||
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_UPDATE;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
@@ -64,9 +64,11 @@ import google.registry.flows.custom.DomainUpdateFlowCustomLogic.BeforeSaveParame
|
||||
import google.registry.flows.custom.EntityChanges;
|
||||
import google.registry.flows.domain.DomainFlowUtils.MissingRegistrantException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.NameserversNotSpecifiedForTldWithNameserverAllowListException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.RegistrantProhibitedException;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.billing.BillingBase.Reason;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.common.FeatureFlag;
|
||||
import google.registry.model.contact.Contact;
|
||||
import google.registry.model.domain.DesignatedContact;
|
||||
import google.registry.model.domain.Domain;
|
||||
@@ -130,6 +132,7 @@ import org.joda.time.DateTime;
|
||||
* @error {@link NameserversNotSpecifiedForTldWithNameserverAllowListException}
|
||||
* @error {@link DomainFlowUtils.NotAuthorizedForTldException}
|
||||
* @error {@link DomainFlowUtils.RegistrantNotAllowedException}
|
||||
* @error {@link RegistrantProhibitedException}
|
||||
* @error {@link DomainFlowUtils.SecDnsAllUsageException}
|
||||
* @error {@link DomainFlowUtils.TooManyDsRecordsException}
|
||||
* @error {@link DomainFlowUtils.TooManyNameserversException}
|
||||
@@ -304,11 +307,12 @@ public final class DomainUpdateFlow implements MutatingFlow {
|
||||
|
||||
private Optional<VKey<Contact>> determineUpdatedRegistrant(Change change, Domain domain)
|
||||
throws EppException {
|
||||
// During phase 1 of minimum dataset transition, allow registrant to be removed
|
||||
// During or after the minimum dataset transition, allow registrant to be removed.
|
||||
if (change.getRegistrantContactId().isPresent()
|
||||
&& change.getRegistrantContactId().get().isEmpty()) {
|
||||
// TODO(b/353347632): Change this flag check to a registry config check.
|
||||
if (isActiveNow(MINIMUM_DATASET_CONTACTS_OPTIONAL)) {
|
||||
if (FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_OPTIONAL)
|
||||
|| FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_PROHIBITED)) {
|
||||
return Optional.empty();
|
||||
} else {
|
||||
throw new MissingRegistrantException();
|
||||
@@ -325,8 +329,7 @@ public final class DomainUpdateFlow implements MutatingFlow {
|
||||
* cause Domain update failure.
|
||||
*/
|
||||
private static void validateNewState(Domain newDomain) throws EppException {
|
||||
validateRequiredContactsPresentIfRequiredForDataset(
|
||||
newDomain.getRegistrant(), newDomain.getContacts());
|
||||
validateContactDataPresence(newDomain.getRegistrant(), newDomain.getContacts());
|
||||
validateDsData(newDomain.getDsData());
|
||||
validateNameserversCountForTld(
|
||||
newDomain.getTld(),
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.flows.exceptions;
|
||||
|
||||
import google.registry.flows.EppException.ParameterValuePolicyErrorException;
|
||||
|
||||
/** Having contacts is prohibited by registry policy */
|
||||
public class ContactsProhibitedException extends ParameterValuePolicyErrorException {
|
||||
public ContactsProhibitedException() {
|
||||
super("Having contacts is prohibited by registry policy");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -62,11 +62,31 @@ public class FeatureFlag extends ImmutableObject implements Buildable {
|
||||
INACTIVE
|
||||
}
|
||||
|
||||
/** The names of the feature flags that can be individually set. */
|
||||
public enum FeatureName {
|
||||
TEST_FEATURE,
|
||||
MINIMUM_DATASET_CONTACTS_OPTIONAL,
|
||||
MINIMUM_DATASET_CONTACTS_PROHIBITED,
|
||||
INCLUDE_PENDING_DELETE_DATE_FOR_DOMAINS
|
||||
/** Feature flag name used for testing only. */
|
||||
TEST_FEATURE(FeatureStatus.INACTIVE),
|
||||
|
||||
/** If we're not requiring the presence of contact data on domain EPP commands. */
|
||||
MINIMUM_DATASET_CONTACTS_OPTIONAL(FeatureStatus.INACTIVE),
|
||||
|
||||
/** If we're not permitting the presence of contact data on any EPP commands. */
|
||||
MINIMUM_DATASET_CONTACTS_PROHIBITED(FeatureStatus.INACTIVE),
|
||||
|
||||
/**
|
||||
* If we're including the upcoming domain drop date in the exported list of registered domains.
|
||||
*/
|
||||
INCLUDE_PENDING_DELETE_DATE_FOR_DOMAINS(FeatureStatus.INACTIVE);
|
||||
|
||||
private final FeatureStatus defaultStatus;
|
||||
|
||||
FeatureName(FeatureStatus defaultStatus) {
|
||||
this.defaultStatus = defaultStatus;
|
||||
}
|
||||
|
||||
FeatureStatus getDefaultStatus() {
|
||||
return this.defaultStatus;
|
||||
}
|
||||
}
|
||||
|
||||
/** The name of the flag/feature. */
|
||||
@@ -155,24 +175,24 @@ public class FeatureFlag extends ImmutableObject implements Buildable {
|
||||
return status.getValueAtTime(time);
|
||||
}
|
||||
|
||||
/** Returns if the flag is active, or the default value if the flag does not exist. */
|
||||
public static boolean isActiveNowOrElse(FeatureName featureName, boolean defaultValue) {
|
||||
tm().assertInTransaction();
|
||||
return CACHE
|
||||
.get(featureName)
|
||||
.map(flag -> flag.getStatus(tm().getTransactionTime()).equals(ACTIVE))
|
||||
.orElse(defaultValue);
|
||||
}
|
||||
|
||||
/** Returns if the FeatureFlag with the given FeatureName is active now. */
|
||||
/**
|
||||
* Returns whether the flag is active now, or else the flag's default value if it doesn't exist.
|
||||
*/
|
||||
public static boolean isActiveNow(FeatureName featureName) {
|
||||
tm().assertInTransaction();
|
||||
return isActiveAt(featureName, tm().getTransactionTime());
|
||||
}
|
||||
|
||||
/** Returns if the FeatureFlag with the given FeatureName is active at a given time. */
|
||||
/**
|
||||
* Returns whether the flag is active at the given time, or else the flag's default value if it
|
||||
* doesn't exist.
|
||||
*/
|
||||
public static boolean isActiveAt(FeatureName featureName, DateTime dateTime) {
|
||||
return FeatureFlag.get(featureName).getStatus(dateTime).equals(ACTIVE);
|
||||
tm().assertInTransaction();
|
||||
return CACHE
|
||||
.get(featureName)
|
||||
.map(flag -> flag.getStatus(dateTime).equals(ACTIVE))
|
||||
.orElse(featureName.getDefaultStatus().equals(ACTIVE));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@ package google.registry.model.console;
|
||||
|
||||
/** Permissions that users may have in the UI, either per-registrar or globally. */
|
||||
public enum ConsolePermission {
|
||||
AUDIT_ACTIVITY_BY_USER,
|
||||
AUDIT_ACTIVITY_BY_REGISTRAR,
|
||||
/** View basic information about a registrar. */
|
||||
VIEW_REGISTRAR_DETAILS,
|
||||
/** Edit basic information about a registrar. */
|
||||
|
||||
@@ -55,6 +55,8 @@ public class ConsoleRoleDefinitions {
|
||||
new ImmutableSet.Builder<ConsolePermission>()
|
||||
.addAll(SUPPORT_AGENT_PERMISSIONS)
|
||||
.add(
|
||||
ConsolePermission.AUDIT_ACTIVITY_BY_USER,
|
||||
ConsolePermission.AUDIT_ACTIVITY_BY_REGISTRAR,
|
||||
ConsolePermission.MANAGE_REGISTRARS,
|
||||
ConsolePermission.GET_REGISTRANT_EMAIL,
|
||||
ConsolePermission.SUSPEND_DOMAIN,
|
||||
@@ -111,6 +113,7 @@ public class ConsoleRoleDefinitions {
|
||||
new ImmutableSet.Builder<ConsolePermission>()
|
||||
.addAll(TECH_CONTACT_PERMISSIONS)
|
||||
.add(ConsolePermission.MANAGE_USERS)
|
||||
.add(ConsolePermission.AUDIT_ACTIVITY_BY_REGISTRAR)
|
||||
.build();
|
||||
|
||||
private ConsoleRoleDefinitions() {}
|
||||
|
||||
@@ -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.
|
||||
@@ -16,155 +16,159 @@ package google.registry.model.console;
|
||||
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
|
||||
import com.google.gson.annotations.Expose;
|
||||
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 {
|
||||
|
||||
public enum Type {
|
||||
DOMAIN_DELETE,
|
||||
DOMAIN_SUSPEND,
|
||||
DOMAIN_UNSUSPEND,
|
||||
EPP_PASSWORD_UPDATE,
|
||||
REGISTRAR_CREATE,
|
||||
REGISTRAR_SECURITY_UPDATE,
|
||||
REGISTRAR_UPDATE,
|
||||
USER_CREATE,
|
||||
USER_DELETE,
|
||||
USER_UPDATE
|
||||
}
|
||||
@Id @IdAllocation @Column Long revisionId;
|
||||
|
||||
/** Autogenerated ID of this event. */
|
||||
@Id
|
||||
@IdAllocation
|
||||
@Column(nullable = false, name = "historyRevisionId")
|
||||
protected Long revisionId;
|
||||
|
||||
/** 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")
|
||||
@Column(nullable = false)
|
||||
@Expose
|
||||
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, name = "historyType")
|
||||
@Column(nullable = false)
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Expose
|
||||
Type type;
|
||||
|
||||
public long getRevisionId() {
|
||||
/** The URL of the action that was used to make the modification. */
|
||||
@Column(nullable = false)
|
||||
String url;
|
||||
|
||||
/** An optional further description of the action. */
|
||||
@Expose String description;
|
||||
|
||||
/** The user that performed the modification. */
|
||||
@JoinColumn(name = "actingUser", referencedColumnName = "emailAddress", nullable = false)
|
||||
@ManyToOne
|
||||
@Expose
|
||||
User actingUser;
|
||||
|
||||
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 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 abstract Builder<? extends ConsoleUpdateHistory, ?> asBuilder();
|
||||
public Builder asBuilder() {
|
||||
return new Builder(clone(this));
|
||||
}
|
||||
|
||||
/** Builder for the immutable ConsoleUpdateHistory. */
|
||||
public abstract static class Builder<
|
||||
T extends ConsoleUpdateHistory, B extends ConsoleUpdateHistory.Builder<?, ?>>
|
||||
extends GenericBuilder<T, B> {
|
||||
public enum Type {
|
||||
DUM_DOWNLOAD,
|
||||
DOMAIN_DELETE,
|
||||
DOMAIN_SUSPEND,
|
||||
DOMAIN_UNSUSPEND,
|
||||
EPP_PASSWORD_UPDATE,
|
||||
REGISTRAR_CREATE,
|
||||
REGISTRAR_CONTACTS_UPDATE,
|
||||
REGISTRAR_SECURITY_UPDATE,
|
||||
REGISTRAR_UPDATE,
|
||||
REGISTRY_LOCK,
|
||||
REGISTRY_UNLOCK,
|
||||
USER_CREATE,
|
||||
USER_DELETE,
|
||||
USER_UPDATE,
|
||||
}
|
||||
|
||||
protected Builder() {}
|
||||
public static final String DESCRIPTION_SEPARATOR = "|";
|
||||
|
||||
protected Builder(T instance) {
|
||||
public static class Builder extends Buildable.Builder<ConsoleUpdateHistory> {
|
||||
public Builder() {}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 @Expose 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,11 +194,59 @@ 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
|
||||
@@ -170,18 +254,56 @@ public class User extends UserBase {
|
||||
return new Builder(clone(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public VKey<User> createVKey() {
|
||||
return VKey.create(User.class, getEmailAddress());
|
||||
}
|
||||
|
||||
/** Builder for constructing immutable {@link User} objects. */
|
||||
public static class Builder extends UserBase.Builder<User, Builder> {
|
||||
public static class Builder extends Buildable.Builder<User> {
|
||||
|
||||
public Builder() {}
|
||||
|
||||
public Builder(User user) {
|
||||
super(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User build() {
|
||||
checkArgumentNotNull(getInstance().emailAddress, "Email address cannot be null");
|
||||
checkArgumentNotNull(getInstance().userRoles, "User roles cannot be null");
|
||||
return super.build();
|
||||
}
|
||||
|
||||
public Builder setEmailAddress(String emailAddress) {
|
||||
getInstance().emailAddress = checkValidEmail(emailAddress);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRegistryLockEmailAddress(@Nullable String registryLockEmailAddress) {
|
||||
getInstance().registryLockEmailAddress =
|
||||
registryLockEmailAddress == null ? null : checkValidEmail(registryLockEmailAddress);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setUserRoles(UserRoles userRoles) {
|
||||
checkArgumentNotNull(userRoles, "User roles cannot be null");
|
||||
getInstance().userRoles = userRoles;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder removeRegistryLockPassword() {
|
||||
getInstance().registryLockPasswordHash = null;
|
||||
getInstance().registryLockPasswordSalt = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder 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 this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())));
|
||||
|
||||
@@ -18,7 +18,7 @@ import static google.registry.util.CollectionUtils.forceEmptyToNull;
|
||||
|
||||
import com.google.common.base.Ascii;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import google.registry.model.Buildable.GenericBuilder;
|
||||
import google.registry.model.Buildable;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.domain.Period;
|
||||
import google.registry.model.domain.fee.Fee;
|
||||
@@ -77,8 +77,7 @@ public class FeeCheckResponseExtensionItemCommandV12 extends ImmutableObject {
|
||||
}
|
||||
|
||||
/** Builder for {@link FeeCheckResponseExtensionItemCommandV12}. */
|
||||
public static class Builder
|
||||
extends GenericBuilder<FeeCheckResponseExtensionItemCommandV12, Builder> {
|
||||
public static class Builder extends Buildable.Builder<FeeCheckResponseExtensionItemCommandV12> {
|
||||
|
||||
public Builder setCommandName(CommandName commandName) {
|
||||
getInstance().commandName = Ascii.toLowerCase(commandName.name());
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
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 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,129 @@ 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 extends Buildable.Builder<RegistrarPoc> {
|
||||
public Builder() {}
|
||||
|
||||
protected Builder(RegistrarPoc instance) {
|
||||
super(instance);
|
||||
}
|
||||
|
||||
/** Build the registrar, nullifying empty fields. */
|
||||
@Override
|
||||
public RegistrarPoc 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 Builder setName(String name) {
|
||||
getInstance().name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setEmailAddress(String emailAddress) {
|
||||
getInstance().emailAddress = emailAddress;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRegistryLockEmailAddress(@Nullable String registryLockEmailAddress) {
|
||||
getInstance().registryLockEmailAddress = registryLockEmailAddress;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setPhoneNumber(String phoneNumber) {
|
||||
getInstance().phoneNumber = phoneNumber;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRegistrarId(String registrarId) {
|
||||
getInstance().registrarId = registrarId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRegistrar(Registrar registrar) {
|
||||
getInstance().registrarId = registrar.getRegistrarId();
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setFaxNumber(String faxNumber) {
|
||||
getInstance().faxNumber = faxNumber;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setTypes(Iterable<Type> types) {
|
||||
getInstance().types = ImmutableSet.copyOf(types);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setVisibleInWhoisAsAdmin(boolean visible) {
|
||||
getInstance().visibleInWhoisAsAdmin = visible;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setVisibleInWhoisAsTech(boolean visible) {
|
||||
getInstance().visibleInWhoisAsTech = visible;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setVisibleInDomainWhoisAsAbuse(boolean visible) {
|
||||
getInstance().visibleInDomainWhoisAsAbuse = visible;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setAllowedToSetRegistryLockPassword(boolean allowedToSetRegistryLockPassword) {
|
||||
if (allowedToSetRegistryLockPassword) {
|
||||
getInstance().registryLockPasswordSalt = null;
|
||||
getInstance().registryLockPasswordHash = null;
|
||||
}
|
||||
getInstance().allowedToSetRegistryLockPassword = allowedToSetRegistryLockPassword;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder 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 this;
|
||||
}
|
||||
}
|
||||
|
||||
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 +467,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// 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.module;
|
||||
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
|
||||
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.Action.GaeService;
|
||||
import google.registry.request.Action.GkeService;
|
||||
import google.registry.request.auth.Auth;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
public class ReadinessProbeAction implements Runnable {
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
private final HttpServletResponse rsp;
|
||||
|
||||
public ReadinessProbeAction(HttpServletResponse rsp) {
|
||||
this.rsp = rsp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the readiness check.
|
||||
*
|
||||
* <p>Performs a simple database query and sets the HTTP response status to OK (200) upon
|
||||
* successful completion. Throws a runtime exception if the database query fails.
|
||||
*/
|
||||
@Override
|
||||
public final void run() {
|
||||
logger.atInfo().log("Performing readiness check database query...");
|
||||
try {
|
||||
tm().transact(() -> tm().query("SELECT version()", Void.class));
|
||||
rsp.setStatus(SC_OK);
|
||||
logger.atInfo().log("Readiness check successful.");
|
||||
} catch (Exception e) {
|
||||
logger.atWarning().withCause(e).log("Readiness check failed:");
|
||||
throw new RuntimeException("Readiness check failed during database query", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Action(
|
||||
service = GaeService.DEFAULT,
|
||||
gkeService = GkeService.CONSOLE,
|
||||
path = ReadinessProbeConsoleAction.PATH,
|
||||
auth = Auth.AUTH_PUBLIC)
|
||||
public static class ReadinessProbeConsoleAction extends ReadinessProbeAction {
|
||||
public static final String PATH = "/ready/console";
|
||||
|
||||
@Inject
|
||||
public ReadinessProbeConsoleAction(HttpServletResponse rsp) {
|
||||
super(rsp);
|
||||
}
|
||||
}
|
||||
|
||||
@Action(
|
||||
service = GaeService.PUBAPI,
|
||||
gkeService = GkeService.PUBAPI,
|
||||
path = ReadinessProbeActionPubApi.PATH,
|
||||
auth = Auth.AUTH_PUBLIC)
|
||||
public static class ReadinessProbeActionPubApi extends ReadinessProbeAction {
|
||||
public static final String PATH = "/ready/pubapi";
|
||||
|
||||
@Inject
|
||||
public ReadinessProbeActionPubApi(HttpServletResponse rsp) {
|
||||
super(rsp);
|
||||
}
|
||||
}
|
||||
|
||||
@Action(
|
||||
service = GaeService.DEFAULT,
|
||||
gkeService = GkeService.FRONTEND,
|
||||
path = ReadinessProbeActionFrontend.PATH,
|
||||
auth = Auth.AUTH_PUBLIC)
|
||||
public static final class ReadinessProbeActionFrontend extends ReadinessProbeAction {
|
||||
public static final String PATH = "/ready/frontend";
|
||||
|
||||
@Inject
|
||||
public ReadinessProbeActionFrontend(HttpServletResponse rsp) {
|
||||
super(rsp);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -58,10 +56,14 @@ import google.registry.flows.TlsCredentials.EppTlsModule;
|
||||
import google.registry.flows.custom.CustomLogicModule;
|
||||
import google.registry.loadtest.LoadTestAction;
|
||||
import google.registry.loadtest.LoadTestModule;
|
||||
import google.registry.module.ReadinessProbeAction.ReadinessProbeActionFrontend;
|
||||
import google.registry.module.ReadinessProbeAction.ReadinessProbeActionPubApi;
|
||||
import google.registry.module.ReadinessProbeAction.ReadinessProbeConsoleAction;
|
||||
import google.registry.monitoring.whitebox.WhiteboxModule;
|
||||
import google.registry.rdap.RdapAutnumAction;
|
||||
import google.registry.rdap.RdapDomainAction;
|
||||
import google.registry.rdap.RdapDomainSearchAction;
|
||||
import google.registry.rdap.RdapEmptyAction;
|
||||
import google.registry.rdap.RdapEntityAction;
|
||||
import google.registry.rdap.RdapEntitySearchAction;
|
||||
import google.registry.rdap.RdapHelpAction;
|
||||
@@ -113,6 +115,7 @@ import google.registry.ui.server.console.ConsoleDomainGetAction;
|
||||
import google.registry.ui.server.console.ConsoleDomainListAction;
|
||||
import google.registry.ui.server.console.ConsoleDumDownloadAction;
|
||||
import google.registry.ui.server.console.ConsoleEppPasswordAction;
|
||||
import google.registry.ui.server.console.ConsoleHistoryDataAction;
|
||||
import google.registry.ui.server.console.ConsoleModule;
|
||||
import google.registry.ui.server.console.ConsoleOteAction;
|
||||
import google.registry.ui.server.console.ConsoleRegistryLockAction;
|
||||
@@ -120,11 +123,13 @@ import google.registry.ui.server.console.ConsoleRegistryLockVerifyAction;
|
||||
import google.registry.ui.server.console.ConsoleUpdateRegistrarAction;
|
||||
import google.registry.ui.server.console.ConsoleUserDataAction;
|
||||
import google.registry.ui.server.console.ConsoleUsersAction;
|
||||
import google.registry.ui.server.console.PasswordResetRequestAction;
|
||||
import google.registry.ui.server.console.PasswordResetVerifyAction;
|
||||
import google.registry.ui.server.console.RegistrarsAction;
|
||||
import google.registry.ui.server.console.domains.ConsoleBulkDomainAction;
|
||||
import google.registry.ui.server.console.settings.ContactAction;
|
||||
import google.registry.ui.server.console.settings.RdapRegistrarFieldsAction;
|
||||
import google.registry.ui.server.console.settings.SecurityAction;
|
||||
import google.registry.ui.server.console.settings.WhoisRegistrarFieldsAction;
|
||||
import google.registry.whois.WhoisAction;
|
||||
import google.registry.whois.WhoisHttpAction;
|
||||
import google.registry.whois.WhoisModule;
|
||||
@@ -136,14 +141,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,
|
||||
@@ -156,7 +160,6 @@ import google.registry.whois.WhoisModule;
|
||||
Spec11Module.class,
|
||||
TmchModule.class,
|
||||
ToolsServerModule.class,
|
||||
VoidDnsWriterModule.class,
|
||||
WhiteboxModule.class,
|
||||
WhoisModule.class,
|
||||
})
|
||||
@@ -183,6 +186,8 @@ interface RequestComponent {
|
||||
|
||||
ConsoleEppPasswordAction consoleEppPasswordAction();
|
||||
|
||||
ConsoleHistoryDataAction consoleHistoryDataAction();
|
||||
|
||||
ConsoleOteAction consoleOteAction();
|
||||
|
||||
ConsoleRegistryLockAction consoleRegistryLockAction();
|
||||
@@ -249,18 +254,30 @@ interface RequestComponent {
|
||||
|
||||
NordnVerifyAction nordnVerifyAction();
|
||||
|
||||
PasswordResetRequestAction passwordResetRequestAction();
|
||||
|
||||
PasswordResetVerifyAction passwordResetVerifyAction();
|
||||
|
||||
PublishDnsUpdatesAction publishDnsUpdatesAction();
|
||||
|
||||
PublishInvoicesAction uploadInvoicesAction();
|
||||
|
||||
PublishSpec11ReportAction publishSpec11ReportAction();
|
||||
|
||||
ReadinessProbeConsoleAction readinessProbeConsoleAction();
|
||||
|
||||
ReadinessProbeActionPubApi readinessProbeActionPubApi();
|
||||
|
||||
ReadinessProbeActionFrontend readinessProbeActionFrontend();
|
||||
|
||||
RdapAutnumAction rdapAutnumAction();
|
||||
|
||||
RdapDomainAction rdapDomainAction();
|
||||
|
||||
RdapDomainSearchAction rdapDomainSearchAction();
|
||||
|
||||
RdapEmptyAction rdapEmptyAction();
|
||||
|
||||
RdapEntityAction rdapEntityAction();
|
||||
|
||||
RdapEntitySearchAction rdapEntitySearchAction();
|
||||
@@ -273,6 +290,8 @@ interface RequestComponent {
|
||||
|
||||
RdapNameserverSearchAction rdapNameserverSearchAction();
|
||||
|
||||
RdapRegistrarFieldsAction rdapRegistrarFieldsAction();
|
||||
|
||||
RdeReportAction rdeReportAction();
|
||||
|
||||
RdeReporter rdeReporter();
|
||||
@@ -324,9 +343,7 @@ interface RequestComponent {
|
||||
WhoisAction whoisAction();
|
||||
|
||||
WhoisHttpAction whoisHttpAction();
|
||||
|
||||
WhoisRegistrarFieldsAction whoisRegistrarFieldsAction();
|
||||
|
||||
|
||||
WipeOutContactHistoryPiiAction wipeOutContactHistoryPiiAction();
|
||||
|
||||
@Subcomponent.Builder
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -21,6 +21,8 @@ import google.registry.dns.DnsModule;
|
||||
import google.registry.flows.EppTlsAction;
|
||||
import google.registry.flows.FlowComponent;
|
||||
import google.registry.flows.TlsCredentials.EppTlsModule;
|
||||
import google.registry.module.ReadinessProbeAction.ReadinessProbeActionFrontend;
|
||||
import google.registry.module.ReadinessProbeAction.ReadinessProbeConsoleAction;
|
||||
import google.registry.monitoring.whitebox.WhiteboxModule;
|
||||
import google.registry.request.RequestComponentBuilder;
|
||||
import google.registry.request.RequestModule;
|
||||
@@ -36,11 +38,13 @@ import google.registry.ui.server.console.ConsoleRegistryLockVerifyAction;
|
||||
import google.registry.ui.server.console.ConsoleUpdateRegistrarAction;
|
||||
import google.registry.ui.server.console.ConsoleUserDataAction;
|
||||
import google.registry.ui.server.console.ConsoleUsersAction;
|
||||
import google.registry.ui.server.console.PasswordResetRequestAction;
|
||||
import google.registry.ui.server.console.PasswordResetVerifyAction;
|
||||
import google.registry.ui.server.console.RegistrarsAction;
|
||||
import google.registry.ui.server.console.domains.ConsoleBulkDomainAction;
|
||||
import google.registry.ui.server.console.settings.ContactAction;
|
||||
import google.registry.ui.server.console.settings.RdapRegistrarFieldsAction;
|
||||
import google.registry.ui.server.console.settings.SecurityAction;
|
||||
import google.registry.ui.server.console.settings.WhoisRegistrarFieldsAction;
|
||||
|
||||
/** Dagger component with per-request lifetime for "default" App Engine module. */
|
||||
@RequestScope
|
||||
@@ -82,12 +86,20 @@ public interface FrontendRequestComponent {
|
||||
|
||||
FlowComponent.Builder flowComponentBuilder();
|
||||
|
||||
PasswordResetRequestAction passwordResetRequestAction();
|
||||
|
||||
PasswordResetVerifyAction passwordResetVerifyAction();
|
||||
|
||||
RdapRegistrarFieldsAction rdapRegistrarFieldsAction();
|
||||
|
||||
ReadinessProbeActionFrontend readinessProbeActionFrontend();
|
||||
|
||||
ReadinessProbeConsoleAction readinessProbeConsoleAction();
|
||||
|
||||
RegistrarsAction registrarsAction();
|
||||
|
||||
SecurityAction securityAction();
|
||||
|
||||
WhoisRegistrarFieldsAction whoisRegistrarFieldsAction();
|
||||
|
||||
@Subcomponent.Builder
|
||||
abstract class Builder implements RequestComponentBuilder<FrontendRequestComponent> {
|
||||
@Override public abstract Builder requestModule(RequestModule requestModule);
|
||||
|
||||
@@ -20,10 +20,12 @@ import google.registry.dns.DnsModule;
|
||||
import google.registry.flows.CheckApiAction;
|
||||
import google.registry.flows.CheckApiAction.CheckApiModule;
|
||||
import google.registry.flows.TlsCredentials.EppTlsModule;
|
||||
import google.registry.module.ReadinessProbeAction.ReadinessProbeActionPubApi;
|
||||
import google.registry.monitoring.whitebox.WhiteboxModule;
|
||||
import google.registry.rdap.RdapAutnumAction;
|
||||
import google.registry.rdap.RdapDomainAction;
|
||||
import google.registry.rdap.RdapDomainSearchAction;
|
||||
import google.registry.rdap.RdapEmptyAction;
|
||||
import google.registry.rdap.RdapEntityAction;
|
||||
import google.registry.rdap.RdapEntitySearchAction;
|
||||
import google.registry.rdap.RdapHelpAction;
|
||||
@@ -55,12 +57,16 @@ public interface PubApiRequestComponent {
|
||||
RdapAutnumAction rdapAutnumAction();
|
||||
RdapDomainAction rdapDomainAction();
|
||||
RdapDomainSearchAction rdapDomainSearchAction();
|
||||
RdapEmptyAction rdapEmptyAction();
|
||||
RdapEntityAction rdapEntityAction();
|
||||
RdapEntitySearchAction rdapEntitySearchAction();
|
||||
RdapHelpAction rdapHelpAction();
|
||||
RdapIpAction rdapDefaultAction();
|
||||
RdapNameserverAction rdapNameserverAction();
|
||||
RdapNameserverSearchAction rdapNameserverSearchAction();
|
||||
|
||||
ReadinessProbeActionPubApi readinessProbeActionPubApi();
|
||||
|
||||
WhoisHttpAction whoisHttpAction();
|
||||
WhoisAction whoisAction();
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -50,7 +55,7 @@ import java.util.Optional;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/**
|
||||
* Base RDAP (new WHOIS) action for all requests.
|
||||
* Base RDAP action for all requests.
|
||||
*
|
||||
* @see <a href="https://tools.ietf.org/html/rfc9082">RFC 9082: Registration Data Access Protocol
|
||||
* (RDAP) Query Format</a>
|
||||
@@ -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;
|
||||
@@ -132,7 +138,7 @@ public abstract class RdapActionBase implements Runnable {
|
||||
// RFC7480 4.2 - servers receiving an RDAP request return an entity with a Content-Type header
|
||||
// containing the RDAP-specific JSON media type.
|
||||
response.setContentType(RESPONSE_MEDIA_TYPE);
|
||||
// RDAP Technical Implementation Guide 1.13 - when responding to RDAP valid requests, we MUST
|
||||
// RDAP Technical Implementation Guide 1.14 - when responding to RDAP valid requests, we MUST
|
||||
// include the Access-Control-Allow-Origin, which MUST be "*" unless otherwise specified.
|
||||
response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, "*");
|
||||
try {
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ import google.registry.request.auth.Auth;
|
||||
import jakarta.inject.Inject;
|
||||
|
||||
/**
|
||||
* RDAP (new WHOIS) action for RDAP autonomous system number requests.
|
||||
* RDAP action for RDAP autonomous system number requests.
|
||||
*
|
||||
* <p>This feature is not implemented because it's only necessary for <i>address</i> registries like
|
||||
* ARIN, not domain registries.
|
||||
|
||||
@@ -41,14 +41,13 @@ final class RdapDataStructures {
|
||||
// Conformance to RFC 9083
|
||||
jsonArray.add("rdap_level_0");
|
||||
|
||||
// Conformance to the RDAP Response Profile V2.1
|
||||
// Conformance to the RDAP Response Profile V2.2 (February 2024)
|
||||
// (see section 1.2)
|
||||
jsonArray.add("icann_rdap_response_profile_1");
|
||||
|
||||
// Conformance to the RDAP Technical Implementation Guide V2.2 (February 2024)
|
||||
// (see section 1.3)
|
||||
jsonArray.add("icann_rdap_response_profile_0");
|
||||
|
||||
// Conformance to the RDAP Technical Implementation Guide V2.1
|
||||
// (see section 1.14)
|
||||
jsonArray.add("icann_rdap_technical_implementation_guide_0");
|
||||
|
||||
jsonArray.add("icann_rdap_technical_implementation_guide_1");
|
||||
return jsonArray;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -36,7 +36,7 @@ import google.registry.request.auth.Auth;
|
||||
import jakarta.inject.Inject;
|
||||
import java.util.Optional;
|
||||
|
||||
/** RDAP (new WHOIS) action for domain requests. */
|
||||
/** RDAP action for domain requests. */
|
||||
@Action(
|
||||
service = GaeService.PUBAPI,
|
||||
path = "/rdap/domain/",
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
@@ -51,6 +51,7 @@ import google.registry.request.Parameter;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.util.NonFinalForTesting;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.persistence.Query;
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import java.net.InetAddress;
|
||||
import java.util.Comparator;
|
||||
@@ -60,17 +61,15 @@ import java.util.stream.Stream;
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
/**
|
||||
* RDAP (new WHOIS) action for domain search requests.
|
||||
* RDAP action for domain search requests.
|
||||
*
|
||||
* <p>All commands and responses conform to the RDAP spec as defined in RFCs 7480 through 7485.
|
||||
* <p>All commands and responses conform to the RDAP spec as defined in STD 95 and its RFCs.
|
||||
*
|
||||
* @see <a href="http://tools.ietf.org/html/rfc9082">RFC 9082: Registration Data Access Protocol
|
||||
* (RDAP) Query Format</a>
|
||||
* @see <a href="http://tools.ietf.org/html/rfc9083">RFC 9083: JSON Responses for the Registration
|
||||
* Data Access Protocol (RDAP)</a>
|
||||
*/
|
||||
// TODO: This isn't required by the RDAP Technical Implementation Guide, and hence should be
|
||||
// deleted, at least until it's actually required.
|
||||
@Action(
|
||||
service = GaeService.PUBAPI,
|
||||
path = "/rdap/domains",
|
||||
@@ -184,7 +183,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 +338,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 +363,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 +380,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
|
||||
@@ -442,7 +441,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
|
||||
replicaTm()
|
||||
.transact(
|
||||
() -> {
|
||||
jakarta.persistence.Query query =
|
||||
Query query =
|
||||
replicaTm()
|
||||
.getEntityManager()
|
||||
.createNativeQuery(queryBuilder.toString())
|
||||
|
||||
54
core/src/main/java/google/registry/rdap/RdapEmptyAction.java
Normal file
54
core/src/main/java/google/registry/rdap/RdapEmptyAction.java
Normal file
@@ -0,0 +1,54 @@
|
||||
// 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.rdap;
|
||||
|
||||
import static google.registry.request.Action.Method.GET;
|
||||
import static google.registry.request.Action.Method.HEAD;
|
||||
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.request.auth.Auth;
|
||||
import jakarta.inject.Inject;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* RDAP action that serves the empty string, redirecting to the help page.
|
||||
*
|
||||
* <p>This isn't technically required, but if someone requests the base url it seems nice to give
|
||||
* them the help response.
|
||||
*/
|
||||
@Action(
|
||||
service = Action.GaeService.PUBAPI,
|
||||
path = "/rdap/",
|
||||
method = {GET, HEAD},
|
||||
auth = Auth.AUTH_PUBLIC)
|
||||
public class RdapEmptyAction implements Runnable {
|
||||
|
||||
private final Response response;
|
||||
|
||||
@Inject
|
||||
public RdapEmptyAction(Response response) {
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
response.sendRedirect(RdapHelpAction.PATH);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user