Compare commits

...
Author SHA1 Message Date
gbrodmanandGitHub daa7ab3bfa Disable primary-contact editing in console (#2745)
This is necessary because we'll use primary-contact emails as a way of
resetting passwords.

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

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

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

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

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

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

See b/315504612 for more details and background
2025-04-23 15:09:37 +00:00
gbrodmanandGitHub 0472dda860 Remove transaction duration logging (#2748)
We suspected this could be a cause of optimistic locking failures
(because long transactions would lead to optimistic locks not being
released) but this didn't end up being the case. Let's remove this to
reduce log spam.
2025-04-22 18:53:21 +00:00
gbrodmanandGitHub 083a9dc8c9 Remove old console history Java classes (#2726)
1. This doesn't remove the SQL tables yet (this is necessary to pass
   tests and also good practice just in case we need or want to look at
history for a little bit)
2. This also removes the Registrar, RegistrarPoc, and User base classes
   that were only necessary because we were saving copies of those
objects in the old history classes.
2025-04-18 22:05:29 +00:00
gbrodmanandGitHub 0153c6284a Add user objects for local test server (#2744)
Also don't try to do anything related to Google admin directory objects
when running the local test server, for obvious reasons
2025-04-18 15:48:06 +00:00
Pavlo TkachandGitHub ca240adfb6 Add new last_poc_verification_date field to Registrar object (#2746) 2025-04-17 19:41:10 +00:00
Pavlo TkachandGitHub b17125ae9a Disable k8s whois routing (#2740) 2025-04-17 15:20:32 +00:00
Pavlo TkachandGitHub dfef733360 Incerase memory request for pubapi and frontend to 1Gi (#2743) 2025-04-11 16:17:43 +00:00
Pavlo TkachandGitHub 04a0659197 Disable console whois (#2741) 2025-04-11 15:32:34 +00:00
Pavlo TkachandGitHub 70010886b1 Increase hikari maximum pool size to 20 (#2742) 2025-04-10 20:51:51 +00:00
gbrodmanandGitHub 3cd50dc929 Only use GKE logs in ICANN reports (#2738)
We no longer need to union GKE+GAE logs since we've moved all production
traffic to GKE only.

For testing, I copied the affected *_test.sql files to Bigquery, removed
all the "-alpha" bits, and changed the dates to 20250301 and 20250331
and ran them to make sure they returned the expected data.
2025-04-09 17:12:02 +00:00
Pavlo TkachandGitHub 03872b508f Exclude prober endoint from sed command canary (#2739) 2025-04-07 21:13:13 +00:00
Pavlo TkachandGitHub 1096f201cd Add GKE readiness probe (#2735) 2025-04-04 21:33:43 +00:00
gbrodmanandGitHub 9dc3215624 Redirect an empty RDAP path to the /help response (#2722)
The behavior when someone hits the plain RDAP base URL isn't specified
by the spec. Currently we just return a plain 404 which isn't
particularly nice or helpful -- so it would probably be nicer to just
redirect to the /help response instead.

tested on alpha,
https://pubapi-dot-domain-registry-alpha.appspot.com/rdap redirects to https://pubapi-dot-domain-registry-alpha.appspot.com/rdap/help
2025-04-03 15:37:23 +00:00
Lai JiangandGitHub af321fb65e Make frontend deployment auto scale (#2736)
Now that we have effective global sessions thanks to #2734, there is no
longer a need to keep the number of pods on the EPP service static.

We are also not vulnerable to random pod restarts. K8s never guarantees
perpetual pod lifetime anyway, and not having to be at its mercy is
certainly a relief.
2025-04-02 18:58:52 +00:00
Lai JiangandGitHub c5132c04be Use pipe as extension URI separator (#2737)
It turns out period can be used in the URI, such as in
"urn:ietf:params:xml:ns:fee-0.12". I don't think pipe is used, at least
not according to EPP URI namespace naming convention.

Ideally we'd use serialization, but using the default serialization runs
the risk of it being platform/JDK dependent, so a new deployment might
not be able to deserialize existing cookies. A custom serializer that
guarantees stability would have been needed.
2025-04-02 13:21:13 +00:00
Lai JiangandGitHub a64dc21f96 make the deploy task deploy to GKE (#2734)
Also always pulls the latest images from repos instead of relying on
local cases. This makes it so that a local docker build is always fresh.
2025-03-31 22:38:53 +00:00
Pavlo TkachandGitHub 0381533a35 Set grace period to 1s for immediate pods restart (#2733) 2025-03-31 19:15:13 +00:00
Lai JiangandGitHub 4999a72d96 Save session data directly in a cookie (#2732) 2025-03-31 16:21:50 +00:00
Pavlo TkachandGitHub 2d072c3844 Update jetty console static files cache policies (#2731) 2025-03-28 19:53:02 +00:00
Pavlo TkachandGitHub c15dec4419 Downgrade node type for pubapi and console, enable bursting for frontend and backend (#2723) 2025-03-28 19:14:33 +00:00
gbrodmanandGitHub 8340125bf4 Remove user FKs from console history tables (#2729)
This, obviously, can mess up user deletion
2025-03-25 20:47:47 +00:00
Pavlo TkachandGitHub 98ba80d94e Remove console security settings timeout (#2728) 2025-03-25 19:36:52 +00:00
gbrodmanandGitHub 967d04efce Include TLD in reserved/registered lists too (#2725)
We already do this for premium terms, but it's nice to do it for the
other list types too

https://b.corp.google.com/issues/390053672
2025-03-24 15:52:12 +00:00
gbrodmanandGitHub 20fd944e83 Remove allocation token custom logic (#2727)
This was added back in early 2018 long ago to enable promotions, but
since then (and for many years) we've added the ability to run
promotions on the tokens themselves, rather than relying on custom Java
classes.

This will make the changes for b/315504612 much easier, as that will
split up token validation into "is this token valid in general?" and "is
this token valid for this domain/action?"
2025-03-21 20:48:54 +00:00
gbrodmanandGitHub daa56e6d85 Bump the number of retries in transaction failures and add skew (#2699)
This can potentially help even more with serializable transaction
failures (optimistic locking exceptions, which are expected to occur
somewhat frequently).

With six attempts, we will sleep at most five times, for
100+200+400+800+1600 ms each, for a total of at most 3.1 seconds (much
less than the EPP maximum which I believe (?) to be 30 seconds.

In addition, we add a 20% skew in an attempt to spread out
possibly-conflicting transaction retries.
2025-03-21 19:47:55 +00:00
gbrodmanandGitHub ed33c7424d Add and use new SimpleConsoleUpdateHistory table (#2712)
This changes the code to only save console histories of this type. We
keep the old Java code (and, necessarily, the corresponding SQL code)
for now because there's no harm in doing so and we want to avoid hastily
deleting too much.
2025-03-21 14:46:16 +00:00
Ben McIlwainandGitHub 04b30f5c04 Fix handling of negative values in monthly transaction reporting (#2704)
The SQL statement was incorrectly flooring to zero one layer too deep, which was
negating all negative transaction report rows (which occur most frequently when
a domain in the autorenew grace period is deleted). I've changed it so that it
now only floors to zero at the report level, which still solves the issue
reported in http://b/290228682 but whose original fix caused the issue
http://b/344645788

This bug was introduced in https://github.com/google/nomulus/pull/2074

I tested this by running the new query against the DB for 2024 Q4 using the
registrar that was having issues and confirmed that the total renewal numbers
for .app now match with the sum total of what we invoiced for the last three
months of 2024.
2025-03-20 21:13:08 +00:00
Lai JiangandGitHub 11702bc940 Revert "Add a redirect for the console bare domain (#2718)" (#2724)
This reverts commit 2a01c12b14.
2025-03-19 22:48:31 +00:00
215 changed files with 8677 additions and 9953 deletions
-1
View File
@@ -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'
+7 -5
View File
@@ -25,7 +25,11 @@ import textwrap
import re
# We should never analyze any generated files
UNIVERSALLY_SKIPPED_PATTERNS = {"/build/", "cloudbuild-caches", "/out/", ".git/", ".gradle/", "/dist/", "/console-alpha/", "/console-crash/", "/console-qa", "/console-sandbox", "/console-production", "karma.conf.js", "polyfills.ts", "test.ts", "/docs/console-endpoints/"}
UNIVERSALLY_SKIPPED_PATTERNS = {"/build/", "cloudbuild-caches", "/out/", ".git/",
".gradle/", "/dist/", "/console-alpha/", "/console-crash/", "/console-qa",
"/console-sandbox", "/console-production", "karma.conf.js", "polyfills.ts",
"test.ts", "/docs/console-endpoints/", "/bin/generated-sources/",
"/bin/generated-test-sources/", "src/main/generated", "src/test/generated"}
# We can't rely on CI to have the Enum package installed so we do this instead.
FORBIDDEN = 1
REQUIRED = 2
@@ -87,11 +91,9 @@ PRESUBMITS = {
PresubmitCheck(
r".*Copyright 20\d{2} The Nomulus Authors\. All Rights Reserved\.",
("java", "js", "soy", "sql", "py", "sh", "gradle", "ts"), {
".git", "/build/", "/bin/generated-sources/", "/bin/generated-test-sources/",
"node_modules/", "LoggerConfig.java", "registrar_bin.",
".git", "/build/", "node_modules/", "LoggerConfig.java", "registrar_bin.",
"registrar_dbg.", "google-java-format-diff.py",
"nomulus.golden.sql", "soyutils_usegoog.js", "javascript/checks.js",
"/src/main/generated", "/src/test/generated"
"nomulus.golden.sql", "soyutils_usegoog.js", "javascript/checks.js"
}, REQUIRED):
"File did not include the license header.",
-7
View File
@@ -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)
+4 -4
View File
@@ -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,
+10 -10
View File
@@ -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,8 @@ 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';
@NgModule({
declarations: [SelectedRegistrarWrapper],
@@ -76,30 +76,30 @@ export class SelectedRegistrarModule {}
ContactDetailsComponent,
DomainListComponent,
EppPasswordEditComponent,
ForceFocusDirective,
HeaderComponent,
HomeComponent,
LocationBackDirective,
ForceFocusDirective,
UserLevelVisibility,
NavigationComponent,
NewRegistrarComponent,
NotificationsComponent,
RdapComponent,
RdapEditComponent,
ReasonDialogComponent,
RegistrarComponent,
RegistrarDetailsComponent,
RegistryLockComponent,
RegistrarSelectorComponent,
RegistryLockComponent,
RegistryLockVerifyComponent,
ResourcesComponent,
ResponseDialogComponent,
SecurityComponent,
SecurityEditComponent,
SettingsComponent,
SettingsContactComponent,
SupportComponent,
TldsComponent,
WhoisComponent,
WhoisEditComponent,
ReasonDialogComponent,
ResponseDialogComponent,
UserLevelVisibility,
],
bootstrap: [AppComponent],
imports: [
@@ -108,8 +108,8 @@ export class SelectedRegistrarModule {}
BrowserModule,
FormsModule,
MaterialModule,
SnackBarModule,
SelectedRegistrarModule,
SnackBarModule,
],
providers: [
BackendService,
@@ -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;
@@ -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',
@@ -24,7 +24,7 @@ export type contactType =
| 'LEGAL'
| 'MARKETING'
| 'TECH'
| 'WHOIS';
| 'RDAP';
type contactTypesToUserFriendlyTypes = { [type in contactType]: string };
@@ -35,7 +35,7 @@ export const contactTypeToTextMap: contactTypesToUserFriendlyTypes = {
LEGAL: 'Legal contact',
MARKETING: 'Marketing contact',
TECH: 'Technical contact',
WHOIS: 'WHOIS-Inquiry contact',
RDAP: 'RDAP-Inquiry contact',
};
type UserFriendlyType = (typeof contactTypeToTextMap)[contactType];
@@ -83,7 +83,7 @@ export class ContactService {
: contactTypeToViewReadyContact({
emailAddress: '',
name: '',
types: ['ADMIN'],
types: ['TECH'],
faxNumber: '',
phoneNumber: '',
registrarId: '',
@@ -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>
@@ -82,6 +82,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 +93,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 +112,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);
@@ -13,7 +13,7 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { switchMap, timeout } from 'rxjs';
import { switchMap } from 'rxjs';
import {
IpAllowListItem,
RegistrarService,
@@ -69,7 +69,6 @@ export class SecurityService {
uiToApiConverter(newSecuritySettings)
)
.pipe(
timeout(2000),
switchMap(() => {
return this.registrarService.loadRegistrars();
})
@@ -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
@@ -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';
@@ -209,12 +209,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
);
}
@@ -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;
@@ -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
@@ -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.",
@@ -1111,12 +1111,6 @@ public final class RegistryConfig {
return config.registryPolicy.whoisCommandFactoryClass;
}
@Provides
@Config("allocationTokenCustomLogicClass")
public static String provideAllocationTokenCustomLogicClass(RegistryConfigSettings config) {
return config.registryPolicy.allocationTokenCustomLogicClass;
}
@Provides
@Config("dnsCountQueryCoordinatorClass")
public static String dnsCountQueryCoordinatorClass(RegistryConfigSettings config) {
@@ -91,7 +91,6 @@ public class RegistryConfigSettings {
public String productName;
public String customLogicFactoryClass;
public String whoisCommandFactoryClass;
public String allocationTokenCustomLogicClass;
public String dnsCountQueryCoordinatorClass;
public int contactAutomaticTransferDays;
public String greetingServerId;
@@ -69,10 +69,6 @@ registryPolicy:
# See whois/WhoisCommandFactory.java
whoisCommandFactoryClass: google.registry.whois.WhoisCommandFactory
# Custom logic class for handling allocation tokens.
# See flows/domain/token/AllocationTokenCustomLogic.java
allocationTokenCustomLogicClass: google.registry.flows.domain.token.AllocationTokenCustomLogic
# Custom logic class for handling DNS query count reporting for ICANN.
# See reporting/icann/DnsCountQueryCoordinator.java
dnsCountQueryCoordinatorClass: google.registry.reporting.icann.DummyDnsCountQueryCoordinator
@@ -247,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());
@@ -78,7 +78,7 @@ public class ExportDomainListsAction implements Runnable {
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 = "registered_domains.txt";
static final String REGISTERED_DOMAINS_FILENAME_FORMAT = "registered_domains_%s.txt";
@Inject Clock clock;
@Inject DriveConnection driveConnection;
@@ -146,7 +146,7 @@ public class ExportDomainListsAction implements Runnable {
} else {
String resultMsg =
driveConnection.createOrUpdateFile(
REGISTERED_DOMAINS_FILENAME,
String.format(REGISTERED_DOMAINS_FILENAME_FORMAT, tldStr),
MediaType.PLAIN_TEXT_UTF_8,
tld.getDriveFolderId(),
domains.getBytes(UTF_8));
@@ -43,7 +43,7 @@ public class ExportReservedTermsAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
static final MediaType EXPORT_MIME_TYPE = MediaType.PLAIN_TEXT_UTF_8;
static final String RESERVED_TERMS_FILENAME = "reserved_terms.txt";
static final String RESERVED_TERMS_FILENAME_FORMAT = "reserved_terms_%s.txt";
@Inject DriveConnection driveConnection;
@Inject ExportUtils exportUtils;
@@ -79,7 +79,7 @@ public class ExportReservedTermsAction implements Runnable {
} else {
resultMsg =
driveConnection.createOrUpdateFile(
RESERVED_TERMS_FILENAME,
String.format(RESERVED_TERMS_FILENAME_FORMAT, tldStr),
EXPORT_MIME_TYPE,
tld.getDriveFolderId(),
exportUtils.exportReservedTerms(tld).getBytes(UTF_8));
@@ -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);
}
@@ -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);
}
}
@@ -43,7 +43,6 @@ import google.registry.flows.domain.DomainTransferQueryFlow;
import google.registry.flows.domain.DomainTransferRejectFlow;
import google.registry.flows.domain.DomainTransferRequestFlow;
import google.registry.flows.domain.DomainUpdateFlow;
import google.registry.flows.domain.token.AllocationTokenModule;
import google.registry.flows.host.HostCheckFlow;
import google.registry.flows.host.HostCreateFlow;
import google.registry.flows.host.HostDeleteFlow;
@@ -59,7 +58,6 @@ import google.registry.model.eppcommon.Trid;
/** Dagger component for flow classes. */
@FlowScope
@Subcomponent(modules = {
AllocationTokenModule.class,
BatchModule.class,
CustomLogicModule.class,
DnsModule.class,
@@ -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();
}
}
@@ -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()));
@@ -133,11 +133,8 @@ import org.joda.time.Duration;
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException}
* @error {@link AllocationTokenFlowUtils.NonexistentAllocationTokenException}
* @error {@link google.registry.flows.exceptions.OnlyToolCanPassMetadataException}
* @error {@link ResourceAlreadyExistsForThisClientException}
* @error {@link ResourceCreateContentionException}
@@ -205,7 +202,6 @@ import org.joda.time.Duration;
* @error {@link DomainFlowUtils.UnexpectedClaimsNoticeException}
* @error {@link DomainFlowUtils.UnsupportedFeeAttributeException}
* @error {@link DomainFlowUtils.UnsupportedMarkTypeException}
* @error {@link DomainPricingLogic.AllocationTokenInvalidForPremiumNameException}
*/
@ReportingSpec(ActivityReportField.DOMAIN_CREATE)
public final class DomainCreateFlow implements MutatingFlow {
@@ -221,7 +217,6 @@ public final class DomainCreateFlow implements MutatingFlow {
@Inject @Superuser boolean isSuperuser;
@Inject DomainHistory.Builder historyBuilder;
@Inject EppResponse.Builder responseBuilder;
@Inject AllocationTokenFlowUtils allocationTokenFlowUtils;
@Inject DomainCreateFlowCustomLogic flowCustomLogic;
@Inject DomainFlowTmchUtils tmchUtils;
@Inject DomainPricingLogic pricingLogic;
@@ -264,25 +259,20 @@ public final class DomainCreateFlow implements MutatingFlow {
}
boolean isSunriseCreate = hasSignedMarks && (tldState == START_DATE_SUNRISE);
Optional<AllocationToken> allocationToken =
allocationTokenFlowUtils.verifyAllocationTokenCreateIfPresent(
command,
tld,
AllocationTokenFlowUtils.loadTokenFromExtensionOrGetDefault(
registrarId,
now,
eppInput.getSingleExtension(AllocationTokenExtension.class));
boolean defaultTokenUsed = false;
if (allocationToken.isEmpty()) {
allocationToken =
DomainFlowUtils.checkForDefaultToken(
tld, command.getDomainName(), CommandName.CREATE, registrarId, now);
if (allocationToken.isPresent()) {
defaultTokenUsed = true;
}
}
eppInput.getSingleExtension(AllocationTokenExtension.class),
tld,
command.getDomainName(),
CommandName.CREATE);
boolean defaultTokenUsed =
allocationToken.map(t -> t.getTokenType().equals(TokenType.DEFAULT_PROMO)).orElse(false);
boolean isAnchorTenant =
isAnchorTenant(
domainName, allocationToken, eppInput.getSingleExtension(MetadataExtension.class));
verifyAnchorTenantValidPeriod(isAnchorTenant, years);
// Superusers can create reserved domains, force creations on domains that require a claims
// notice without specifying a claims key, ignore the registry phase, and override blocks on
// registering premium domains.
@@ -416,7 +406,7 @@ public final class DomainCreateFlow implements MutatingFlow {
entitiesToSave.add(domain, domainHistory);
if (allocationToken.isPresent() && allocationToken.get().getTokenType().isOneTimeUse()) {
entitiesToSave.add(
allocationTokenFlowUtils.redeemToken(
AllocationTokenFlowUtils.redeemToken(
allocationToken.get(), domainHistory.getHistoryEntryId()));
}
if (domain.shouldPublishToDns()) {
@@ -42,7 +42,6 @@ import static google.registry.model.tld.label.ReservationType.RESERVED_FOR_ANCHO
import static google.registry.model.tld.label.ReservationType.RESERVED_FOR_SPECIFIC_USE;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.pricing.PricingEngineProxy.isDomainPremium;
import static google.registry.util.CollectionUtils.isNullOrEmpty;
import static google.registry.util.CollectionUtils.nullToEmpty;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.DateTimeUtils.isAtOrAfter;
@@ -67,7 +66,6 @@ import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import com.google.common.net.InternetDomainName;
import google.registry.flows.EppException;
import google.registry.flows.EppException.AssociationProhibitsOperationException;
import google.registry.flows.EppException.AuthorizationErrorException;
import google.registry.flows.EppException.CommandUseErrorException;
import google.registry.flows.EppException.ObjectDoesNotExistException;
@@ -77,8 +75,6 @@ import google.registry.flows.EppException.ParameterValueSyntaxErrorException;
import google.registry.flows.EppException.RequiredParameterMissingException;
import google.registry.flows.EppException.StatusProhibitsOperationException;
import google.registry.flows.EppException.UnimplementedOptionException;
import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils;
import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException;
import google.registry.model.EppResource;
import google.registry.model.billing.BillingBase.Flag;
@@ -101,7 +97,6 @@ import google.registry.model.domain.fee.BaseFee.FeeType;
import google.registry.model.domain.fee.Credit;
import google.registry.model.domain.fee.Fee;
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem;
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName;
import google.registry.model.domain.fee.FeeQueryResponseExtensionItem;
import google.registry.model.domain.fee.FeeTransformCommandExtension;
import google.registry.model.domain.fee.FeeTransformResponseExtension;
@@ -124,7 +119,7 @@ import google.registry.model.eppoutput.EppResponse.ResponseExtension;
import google.registry.model.host.Host;
import google.registry.model.poll.PollMessage.Autorenew;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarBase.State;
import google.registry.model.registrar.Registrar.State;
import google.registry.model.reporting.DomainTransactionRecord;
import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField;
import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
@@ -1233,52 +1228,6 @@ public class DomainFlowUtils {
.getResultList();
}
/**
* Checks if there is a valid default token to be used for a domain create command.
*
* <p>If there is more than one valid default token for the registration, only the first valid
* token found on the TLD's default token list will be returned.
*/
public static Optional<AllocationToken> checkForDefaultToken(
Tld tld, String domainName, CommandName commandName, String registrarId, DateTime now)
throws EppException {
if (isNullOrEmpty(tld.getDefaultPromoTokens())) {
return Optional.empty();
}
Map<VKey<AllocationToken>, Optional<AllocationToken>> tokens =
AllocationToken.getAll(tld.getDefaultPromoTokens());
ImmutableList<Optional<AllocationToken>> tokenList =
tld.getDefaultPromoTokens().stream()
.map(tokens::get)
.filter(Optional::isPresent)
.collect(toImmutableList());
checkState(
!isNullOrEmpty(tokenList),
"Failure while loading default TLD promotions from the database");
// Check if any of the tokens are valid for this domain registration
for (Optional<AllocationToken> token : tokenList) {
try {
AllocationTokenFlowUtils.validateToken(
InternetDomainName.from(domainName),
token.get(),
commandName,
registrarId,
isDomainPremium(domainName, now),
now);
} catch (AssociationProhibitsOperationException
| StatusProhibitsOperationException
| AllocationTokenInvalidForPremiumNameException e) {
// Allocation token was not valid for this registration, continue to check the next token in
// the list
continue;
}
// Only use the first valid token in the list
return token;
}
// No valid default token found
return Optional.empty();
}
/** Resource linked to this domain does not exist. */
static class LinkedResourcesDoNotExistException extends ObjectDoesNotExistException {
public LinkedResourcesDoNotExistException(Class<?> type, ImmutableSet<String> resourceIds) {
@@ -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);
@@ -1,64 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.flows.domain.token;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.net.InternetDomainName;
import google.registry.flows.EppException;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainCommand;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.tld.Tld;
import org.joda.time.DateTime;
/**
* A no-op base class for allocation token custom logic.
*
* <p>Extend this class and override the hook(s) to perform custom logic.
*/
public class AllocationTokenCustomLogic {
/** Performs additional custom logic for validating a token on a domain create. */
public AllocationToken validateToken(
DomainCommand.Create command,
AllocationToken token,
Tld tld,
String registrarId,
DateTime now)
throws EppException {
// Do nothing.
return token;
}
/** Performs additional custom logic for validating a token on an existing domain. */
public AllocationToken validateToken(
Domain domain, AllocationToken token, Tld tld, String registrarId, DateTime now)
throws EppException {
// Do nothing.
return token;
}
/** Performs additional custom logic for performing domain checks using a token. */
public ImmutableMap<InternetDomainName, String> checkDomainsWithToken(
ImmutableList<InternetDomainName> domainNames,
AllocationToken token,
String registrarId,
DateTime now) {
// Do nothing.
return Maps.toMap(domainNames, k -> "");
}
}
@@ -15,230 +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.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
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 {
private final AllocationTokenCustomLogic tokenCustomLogic;
@Inject
AllocationTokenFlowUtils(AllocationTokenCustomLogic tokenCustomLogic) {
this.tokenCustomLogic = tokenCustomLogic;
}
/**
* 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
ImmutableList.Builder<InternetDomainName> validDomainNames = new ImmutableList.Builder<>();
ImmutableMap.Builder<InternetDomainName, String> resultsBuilder = new ImmutableMap.Builder<>();
for (InternetDomainName domainName : domainNames) {
try {
validateToken(
domainName,
tokenEntity,
CommandName.CREATE,
registrarId,
isDomainPremium(domainName.toString(), now),
now);
validDomainNames.add(domainName);
} catch (EppException e) {
resultsBuilder.put(domainName, e.getMessage());
}
}
// For all valid domain names, run the custom logic and include the results
resultsBuilder.putAll(
tokenCustomLogic.checkDomainsWithToken(
validDomainNames.build(), tokenEntity, registrarId, now));
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(tokenCustomLogic.validateToken(command, tokenEntity, tld, registrarId, now));
}
/** Verifies and returns the allocation token if one is specified, otherwise does nothing. */
public Optional<AllocationToken> verifyAllocationTokenIfPresent(
Domain existingDomain,
Tld tld,
String registrarId,
DateTime now,
CommandName commandName,
Optional<AllocationTokenExtension> extension)
throws EppException {
if (extension.isEmpty()) {
return Optional.empty();
}
AllocationToken tokenEntity = loadToken(extension.get().getAllocationToken());
validateToken(
InternetDomainName.from(existingDomain.getDomainName()),
tokenEntity,
commandName,
registrarId,
isDomainPremium(existingDomain.getDomainName(), now),
now);
return Optional.of(
tokenCustomLogic.validateToken(existingDomain, tokenEntity, tld, registrarId, now));
loadAndValidateToken(extension.get().getAllocationToken(), registrarId, domainName, now));
}
public static void verifyTokenAllowedOnDomain(
Domain domain, Optional<AllocationToken> allocationToken) throws EppException {
/**
* 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,
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;
}
return checkForDefaultToken(tld, domainName, commandName, registrarId, now);
}
/** 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()
@@ -251,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()
@@ -261,7 +133,7 @@ public class AllocationTokenFlowUtils {
BillingRecurrence newBillingRecurrence =
tm().loadByKey(domain.getAutorenewBillingEvent())
.asBuilder()
.setRenewalPriceBehavior(RenewalPriceBehavior.DEFAULT)
.setRenewalPriceBehavior(BillingBase.RenewalPriceBehavior.DEFAULT)
.setRenewalPrice(null)
.build();
@@ -279,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");
}
@@ -315,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");
}
}
@@ -1,33 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.flows.domain.token;
import static google.registry.util.TypeUtils.getClassFromString;
import static google.registry.util.TypeUtils.instantiate;
import dagger.Module;
import dagger.Provides;
import google.registry.config.RegistryConfig.Config;
/** Dagger module for allocation token classes. */
@Module
public class AllocationTokenModule {
@Provides
static AllocationTokenCustomLogic provideAllocationTokenCustomLogic(
@Config("allocationTokenCustomLogicClass") String customClass) {
return instantiate(getClassFromString(customClass, AllocationTokenCustomLogic.class));
}
}
@@ -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;
}
}
}
@@ -1,4 +1,4 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -19,150 +19,146 @@ import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.model.annotations.IdAllocation;
import jakarta.persistence.Access;
import jakarta.persistence.AccessType;
import google.registry.persistence.WithVKey;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.Table;
import java.util.Optional;
import org.joda.time.DateTime;
/**
* A record of a resource that was updated through the console.
*
* <p>This abstract class has several subclasses that (mostly) include the modified resource itself
* so that the entire object history is persisted to SQL.
*/
@Access(AccessType.FIELD)
@MappedSuperclass
public abstract class ConsoleUpdateHistory extends ImmutableObject implements Buildable {
@Entity
@WithVKey(Long.class)
@Table(
indexes = {
@Index(columnList = "actingUser", name = "idx_console_update_history_acting_user"),
@Index(columnList = "type", name = "idx_console_update_history_type"),
@Index(columnList = "modificationTime", name = "idx_console_update_history_modification_time")
})
public class ConsoleUpdateHistory extends ImmutableObject implements Buildable {
public enum Type {
EPP_ACTION,
POC_CREATE,
POC_UPDATE,
POC_DELETE,
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)
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)
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. */
String description;
/** The user that performed the modification. */
@JoinColumn(name = "actingUser", referencedColumnName = "emailAddress", nullable = false)
@ManyToOne
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 {
DOMAIN_DELETE,
DOMAIN_SUSPEND,
DOMAIN_UNSUSPEND,
EPP_PASSWORD_UPDATE,
REGISTRAR_CREATE,
REGISTRAR_SECURITY_UPDATE,
REGISTRAR_UPDATE,
USER_CREATE,
USER_DELETE,
USER_UPDATE
}
protected Builder() {}
public static class Builder extends Buildable.Builder<ConsoleUpdateHistory> {
public Builder() {}
protected Builder(T instance) {
private Builder(ConsoleUpdateHistory instance) {
super(instance);
}
@Override
public T build() {
public ConsoleUpdateHistory build() {
checkArgumentNotNull(getInstance().modificationTime, "Modification time must be specified");
checkArgumentNotNull(getInstance().actingUser, "Acting user must be specified");
checkArgumentNotNull(getInstance().url, "URL must be specified");
checkArgumentNotNull(getInstance().method, "HTTP method must be specified");
checkArgumentNotNull(getInstance().modificationTime, "modificationTime must be specified");
checkArgumentNotNull(getInstance().type, "Console History type must be specified");
checkArgumentNotNull(getInstance().type, "ConsoleUpdateHistory type must be specified");
return super.build();
}
public B setActingUser(User actingUser) {
getInstance().actingUser = actingUser;
return thisCastToDerived();
}
public B setUrl(String url) {
getInstance().url = url;
return thisCastToDerived();
}
public B setMethod(String method) {
getInstance().method = method;
return thisCastToDerived();
}
public B setRequestBody(String requestBody) {
getInstance().requestBody = requestBody;
return thisCastToDerived();
}
public B setModificationTime(DateTime modificationTime) {
public Builder setModificationTime(DateTime modificationTime) {
getInstance().modificationTime = modificationTime;
return thisCastToDerived();
return this;
}
public B setType(Type type) {
public Builder setActingUser(User actingUser) {
getInstance().actingUser = actingUser;
return this;
}
public Builder setUrl(String url) {
getInstance().url = url;
return this;
}
public Builder setMethod(String method) {
getInstance().method = method;
return this;
}
public Builder setDescription(String description) {
getInstance().description = description;
return this;
}
public Builder setType(Type type) {
getInstance().type = type;
return thisCastToDerived();
return this;
}
}
}
@@ -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,4 +1,4 @@
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -15,23 +15,30 @@
package google.registry.model.console;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.io.BaseEncoding.base64;
import static google.registry.model.registrar.Registrar.checkValidEmail;
import static google.registry.tools.server.UpdateUserGroupAction.GROUP_UPDATE_QUEUE;
import static google.registry.util.PasswordUtils.SALT_SUPPLIER;
import static google.registry.util.PasswordUtils.hashPassword;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.cloud.tasks.v2.Task;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import com.google.gson.annotations.Expose;
import google.registry.batch.CloudTasksUtils;
import google.registry.persistence.VKey;
import google.registry.model.Buildable;
import google.registry.model.UpdateAutoTimestampEntity;
import google.registry.request.Action;
import google.registry.tools.IamClient;
import google.registry.tools.ServiceConnection;
import google.registry.tools.server.UpdateUserGroupAction;
import google.registry.tools.server.UpdateUserGroupAction.Mode;
import google.registry.util.PasswordUtils;
import google.registry.util.RegistryEnvironment;
import jakarta.persistence.Access;
import jakarta.persistence.AccessType;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@@ -44,11 +51,33 @@ import javax.annotation.Nullable;
@Embeddable
@Entity
@Table
public class User extends UserBase {
public class User extends UpdateAutoTimestampEntity implements Buildable {
public static final String IAP_SECURED_WEB_APP_USER_ROLE = "roles/iap.httpsResourceAccessor";
private static final long serialVersionUID = 6936728603828566721L;
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/** Email address of the user in question. */
@Id @Expose String emailAddress;
/** Optional external email address to use for registry lock confirmation emails. */
@Column String registryLockEmailAddress;
/** Roles (which grant permissions) associated with this user. */
@Expose
@Column(nullable = false)
UserRoles userRoles;
/**
* A hashed password that exists iff this contact is registry-lock-enabled. The hash is a base64
* encoded SHA256 string.
*/
String registryLockPasswordHash;
/** Randomly generated hash salt. */
String registryLockPasswordSalt;
/**
* Grants the user permission to pass IAP.
*
@@ -113,9 +142,13 @@ public class User extends UserBase {
logger.atInfo().log("Removing %s from group %s", emailAddress, groupEmailAddress.get());
if (cloudTasksUtils != null) {
modifyGroupMembershipAsync(
emailAddress, groupEmailAddress.get(), cloudTasksUtils, Mode.REMOVE);
emailAddress,
groupEmailAddress.get(),
cloudTasksUtils,
UpdateUserGroupAction.Mode.REMOVE);
} else {
modifyGroupMembershipSync(emailAddress, groupEmailAddress.get(), connection, Mode.REMOVE);
modifyGroupMembershipSync(
emailAddress, groupEmailAddress.get(), connection, UpdateUserGroupAction.Mode.REMOVE);
}
}
}
@@ -124,7 +157,7 @@ public class User extends UserBase {
String userEmailAddress,
String groupEmailAddress,
CloudTasksUtils cloudTasksUtils,
Mode mode) {
UpdateUserGroupAction.Mode mode) {
Task task =
cloudTasksUtils.createTask(
UpdateUserGroupAction.class,
@@ -140,7 +173,10 @@ public class User extends UserBase {
}
private static void modifyGroupMembershipSync(
String userEmailAddress, String groupEmailAddress, ServiceConnection connection, Mode mode) {
String userEmailAddress,
String groupEmailAddress,
ServiceConnection connection,
UpdateUserGroupAction.Mode mode) {
try {
connection.sendPostRequest(
UpdateUserGroupAction.PATH,
@@ -158,30 +194,117 @@ public class User extends UserBase {
}
}
@Id
@Override
@Access(AccessType.PROPERTY)
/**
* Sets the user email address.
*
* <p>This should only be used for restoring an object being loaded in a PostLoad method
* (effectively, when it is still under construction by Hibernate). In all other cases, the object
* should be regarded as immutable and changes should go through a Builder.
*
* <p>In addition to this special case use, this method must exist to satisfy Hibernate.
*/
void setEmailAddress(String emailAddress) {
this.emailAddress = emailAddress;
}
public String getEmailAddress() {
return super.getEmailAddress();
return emailAddress;
}
public Optional<String> getRegistryLockEmailAddress() {
return Optional.ofNullable(registryLockEmailAddress);
}
public UserRoles getUserRoles() {
return userRoles;
}
public boolean hasRegistryLockPassword() {
return !isNullOrEmpty(registryLockPasswordHash) && !isNullOrEmpty(registryLockPasswordSalt);
}
public boolean verifyRegistryLockPassword(String registryLockPassword) {
if (isNullOrEmpty(registryLockPassword)
|| isNullOrEmpty(registryLockPasswordSalt)
|| isNullOrEmpty(registryLockPasswordHash)) {
return false;
}
return PasswordUtils.verifyPassword(
registryLockPassword, registryLockPasswordHash, registryLockPasswordSalt);
}
/**
* Whether the user has the registry lock permission on any registrar or globally.
*
* <p>If so, they should be allowed to (re)set their registry lock password.
*/
public boolean hasAnyRegistryLockPermission() {
if (userRoles == null) {
return false;
}
if (userRoles.isAdmin() || userRoles.hasGlobalPermission(ConsolePermission.REGISTRY_LOCK)) {
return true;
}
return userRoles.getRegistrarRoles().values().stream()
.anyMatch(role -> role.hasPermission(ConsolePermission.REGISTRY_LOCK));
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
}
@Override
public VKey<User> createVKey() {
return VKey.create(User.class, getEmailAddress());
public Builder<? extends User, ?> asBuilder() {
return new Builder<>(clone(this));
}
/** Builder for constructing immutable {@link User} objects. */
public static class Builder extends UserBase.Builder<User, Builder> {
public static class Builder<T extends User, B extends Builder<T, B>>
extends GenericBuilder<T, B> {
public Builder() {}
public Builder(User user) {
super(user);
public Builder(T abstractUser) {
super(abstractUser);
}
@Override
public T build() {
checkArgumentNotNull(getInstance().emailAddress, "Email address cannot be null");
checkArgumentNotNull(getInstance().userRoles, "User roles cannot be null");
return super.build();
}
public B setEmailAddress(String emailAddress) {
getInstance().emailAddress = checkValidEmail(emailAddress);
return thisCastToDerived();
}
public B setRegistryLockEmailAddress(@Nullable String registryLockEmailAddress) {
getInstance().registryLockEmailAddress =
registryLockEmailAddress == null ? null : checkValidEmail(registryLockEmailAddress);
return thisCastToDerived();
}
public B setUserRoles(UserRoles userRoles) {
checkArgumentNotNull(userRoles, "User roles cannot be null");
getInstance().userRoles = userRoles;
return thisCastToDerived();
}
public B removeRegistryLockPassword() {
getInstance().registryLockPasswordHash = null;
getInstance().registryLockPasswordSalt = null;
return thisCastToDerived();
}
public B setRegistryLockPassword(String registryLockPassword) {
checkArgument(
getInstance().hasAnyRegistryLockPermission(), "User has no registry lock permission");
checkArgument(
!getInstance().hasRegistryLockPassword(), "User already has a password, remove it first");
checkArgument(
!isNullOrEmpty(registryLockPassword), "Registry lock password was null or empty");
byte[] salt = SALT_SUPPLIER.get();
getInstance().registryLockPasswordSalt = base64().encode(salt);
getInstance().registryLockPasswordHash = hashPassword(registryLockPassword, salt);
return thisCastToDerived();
}
}
}
@@ -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;
}
}
}
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,40 @@
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.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.gson.annotations.Expose;
import google.registry.model.Buildable.GenericBuilder;
import google.registry.model.ImmutableObject;
import google.registry.model.registrar.RegistrarPoc.RegistrarPocId;
import google.registry.model.JsonMapBuilder;
import google.registry.model.Jsonifiable;
import google.registry.model.UnsafeSerializable;
import google.registry.persistence.VKey;
import jakarta.persistence.Access;
import jakarta.persistence.AccessType;
import google.registry.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 +58,244 @@ 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;
}
}
/** 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 String getName() {
return name;
}
@Id
@Access(AccessType.PROPERTY)
@Override
public String getEmailAddress() {
return emailAddress;
}
@Id
@Access(AccessType.PROPERTY)
public String getRegistrarId() {
return registrarId;
public Optional<String> getRegistryLockEmailAddress() {
return Optional.ofNullable(registryLockEmailAddress);
}
public String getPhoneNumber() {
return phoneNumber;
}
public String getFaxNumber() {
return faxNumber;
}
public ImmutableSortedSet<Type> getTypes() {
return nullToEmptyImmutableSortedCopy(types);
}
public boolean getVisibleInWhoisAsAdmin() {
return visibleInWhoisAsAdmin;
}
public boolean getVisibleInWhoisAsTech() {
return visibleInWhoisAsTech;
}
public boolean getVisibleInDomainWhoisAsAbuse() {
return visibleInDomainWhoisAsAbuse;
}
public Builder<? extends RegistrarPoc, ?> asBuilder() {
return new Builder<>(clone(this));
}
public boolean isAllowedToSetRegistryLockPassword() {
return allowedToSetRegistryLockPassword;
}
public boolean isRegistryLockAllowed() {
return !isNullOrEmpty(registryLockPasswordHash) && !isNullOrEmpty(registryLockPasswordSalt);
}
public boolean verifyRegistryLockPassword(String registryLockPassword) {
if (isNullOrEmpty(registryLockPassword)
|| isNullOrEmpty(registryLockPasswordSalt)
|| isNullOrEmpty(registryLockPasswordHash)) {
return false;
}
return PasswordUtils.verifyPassword(
registryLockPassword, registryLockPasswordHash, registryLockPasswordSalt);
}
/**
* Returns a string representation that's human friendly.
*
* <p>The output will look something like this:
*
* <pre>{@code
* Some Person
* person@example.com
* Tel: +1.2125650666
* Types: [ADMIN, WHOIS]
* Visible in WHOIS as Admin contact: Yes
* Visible in WHOIS as Technical contact: No
* Registrar-Console access: Yes
* Login Email Address: person@registry.example
* }</pre>
*/
public String toStringMultilinePlainText() {
StringBuilder result = new StringBuilder(256);
result.append(getName()).append('\n');
result.append(getEmailAddress()).append('\n');
if (phoneNumber != null) {
result.append("Tel: ").append(getPhoneNumber()).append('\n');
}
if (faxNumber != null) {
result.append("Fax: ").append(getFaxNumber()).append('\n');
}
result.append("Types: ").append(getTypes()).append('\n');
result
.append("Visible in registrar WHOIS query as Admin contact: ")
.append(getVisibleInWhoisAsAdmin() ? "Yes" : "No")
.append('\n');
result
.append("Visible in registrar WHOIS query as Technical contact: ")
.append(getVisibleInWhoisAsTech() ? "Yes" : "No")
.append('\n');
result
.append(
"Phone number and email visible in domain WHOIS query as "
+ "Registrar Abuse contact info: ")
.append(getVisibleInDomainWhoisAsAbuse() ? "Yes" : "No")
.append('\n');
return result.toString();
}
@Override
public Map<String, Object> toJsonMap() {
return new JsonMapBuilder()
.put("name", name)
.put("emailAddress", emailAddress)
.put("registryLockEmailAddress", registryLockEmailAddress)
.put("phoneNumber", phoneNumber)
.put("faxNumber", faxNumber)
.put("types", getTypes().stream().map(Object::toString).collect(joining(",")))
.put("visibleInWhoisAsAdmin", visibleInWhoisAsAdmin)
.put("visibleInWhoisAsTech", visibleInWhoisAsTech)
.put("visibleInDomainWhoisAsAbuse", visibleInDomainWhoisAsAbuse)
.put("allowedToSetRegistryLockPassword", allowedToSetRegistryLockPassword)
.put("registryLockAllowed", isRegistryLockAllowed())
.build();
}
@Override
@@ -57,9 +303,124 @@ public class RegistrarPoc extends RegistrarPocBase {
return VKey.create(RegistrarPoc.class, new RegistrarPocId(emailAddress, registrarId));
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
/**
* These methods set the email address and registrar ID
*
* <p>This should only be used for restoring the fields of an object being loaded in a PostLoad
* method (effectively, when it is still under construction by Hibernate). In all other cases, the
* object should be regarded as immutable and changes should go through a Builder.
*
* <p>In addition to this special case use, this method must exist to satisfy Hibernate.
*/
public void setEmailAddress(String emailAddress) {
this.emailAddress = emailAddress;
}
public void setRegistrarId(String registrarId) {
this.registrarId = registrarId;
}
/** A builder for constructing a {@link RegistrarPoc}, since it is immutable. */
public static class Builder<T extends RegistrarPoc, B extends Builder<T, B>>
extends GenericBuilder<T, B> {
public Builder() {}
protected Builder(T instance) {
super(instance);
}
/** Build the registrar, nullifying empty fields. */
@Override
public T build() {
checkNotNull(getInstance().registrarId, "Registrar ID cannot be null");
checkValidEmail(getInstance().emailAddress);
// Check allowedToSetRegistryLockPassword here because if we want to allow the user to set
// a registry lock password, we must also set up the correct registry lock email concurrently
// or beforehand.
if (getInstance().allowedToSetRegistryLockPassword) {
checkArgument(
!isNullOrEmpty(getInstance().registryLockEmailAddress),
"Registry lock email must not be null if allowing registry lock access");
}
return cloneEmptyToNull(super.build());
}
public B setName(String name) {
getInstance().name = name;
return thisCastToDerived();
}
public B setEmailAddress(String emailAddress) {
getInstance().emailAddress = emailAddress;
return thisCastToDerived();
}
public B setRegistryLockEmailAddress(@Nullable String registryLockEmailAddress) {
getInstance().registryLockEmailAddress = registryLockEmailAddress;
return thisCastToDerived();
}
public B setPhoneNumber(String phoneNumber) {
getInstance().phoneNumber = phoneNumber;
return thisCastToDerived();
}
public B setRegistrarId(String registrarId) {
getInstance().registrarId = registrarId;
return thisCastToDerived();
}
public B setRegistrar(Registrar registrar) {
getInstance().registrarId = registrar.getRegistrarId();
return thisCastToDerived();
}
public B setFaxNumber(String faxNumber) {
getInstance().faxNumber = faxNumber;
return thisCastToDerived();
}
public B setTypes(Iterable<Type> types) {
getInstance().types = ImmutableSet.copyOf(types);
return thisCastToDerived();
}
public B setVisibleInWhoisAsAdmin(boolean visible) {
getInstance().visibleInWhoisAsAdmin = visible;
return thisCastToDerived();
}
public B setVisibleInWhoisAsTech(boolean visible) {
getInstance().visibleInWhoisAsTech = visible;
return thisCastToDerived();
}
public B setVisibleInDomainWhoisAsAbuse(boolean visible) {
getInstance().visibleInDomainWhoisAsAbuse = visible;
return thisCastToDerived();
}
public B setAllowedToSetRegistryLockPassword(boolean allowedToSetRegistryLockPassword) {
if (allowedToSetRegistryLockPassword) {
getInstance().registryLockPasswordSalt = null;
getInstance().registryLockPasswordHash = null;
}
getInstance().allowedToSetRegistryLockPassword = allowedToSetRegistryLockPassword;
return thisCastToDerived();
}
public B setRegistryLockPassword(String registryLockPassword) {
checkArgument(
getInstance().allowedToSetRegistryLockPassword,
"Not allowed to set registry lock password for this contact");
checkArgument(
!isNullOrEmpty(registryLockPassword), "Registry lock password was null or empty");
byte[] salt = SALT_SUPPLIER.get();
getInstance().registryLockPasswordSalt = base64().encode(salt);
getInstance().registryLockPasswordHash = hashPassword(registryLockPassword, salt);
getInstance().allowedToSetRegistryLockPassword = false;
return thisCastToDerived();
}
}
/** Class to represent the composite primary key for {@link RegistrarPoc} entity. */
@@ -90,13 +451,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);
}
}
}
@@ -58,10 +58,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;
@@ -123,8 +127,8 @@ import google.registry.ui.server.console.ConsoleUsersAction;
import google.registry.ui.server.console.RegistrarsAction;
import google.registry.ui.server.console.domains.ConsoleBulkDomainAction;
import google.registry.ui.server.console.settings.ContactAction;
import google.registry.ui.server.console.settings.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;
@@ -255,12 +259,20 @@ interface RequestComponent {
PublishSpec11ReportAction publishSpec11ReportAction();
ReadinessProbeConsoleAction readinessProbeConsoleAction();
ReadinessProbeActionPubApi readinessProbeActionPubApi();
ReadinessProbeActionFrontend readinessProbeActionFrontend();
RdapAutnumAction rdapAutnumAction();
RdapDomainAction rdapDomainAction();
RdapDomainSearchAction rdapDomainSearchAction();
RdapEmptyAction rdapEmptyAction();
RdapEntityAction rdapEntityAction();
RdapEntitySearchAction rdapEntitySearchAction();
@@ -325,7 +337,7 @@ interface RequestComponent {
WhoisHttpAction whoisHttpAction();
WhoisRegistrarFieldsAction whoisRegistrarFieldsAction();
RdapRegistrarFieldsAction rdapRegistrarFieldsAction();
WipeOutContactHistoryPiiAction wipeOutContactHistoryPiiAction();
@@ -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;
@@ -39,8 +41,8 @@ import google.registry.ui.server.console.ConsoleUsersAction;
import google.registry.ui.server.console.RegistrarsAction;
import google.registry.ui.server.console.domains.ConsoleBulkDomainAction;
import google.registry.ui.server.console.settings.ContactAction;
import google.registry.ui.server.console.settings.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,11 +84,15 @@ public interface FrontendRequestComponent {
FlowComponent.Builder flowComponentBuilder();
ReadinessProbeActionFrontend readinessProbeActionFrontend();
ReadinessProbeConsoleAction readinessProbeConsoleAction();
RegistrarsAction registrarsAction();
SecurityAction securityAction();
WhoisRegistrarFieldsAction whoisRegistrarFieldsAction();
RdapRegistrarFieldsAction rdapRegistrarFieldsAction();
@Subcomponent.Builder
abstract class Builder implements RequestComponentBuilder<FrontendRequestComponent> {
@@ -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();
@@ -86,7 +86,7 @@ import org.joda.time.DateTime;
public class JpaTransactionManagerImpl implements JpaTransactionManager {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final Retrier retrier = new Retrier(new SystemSleeper(), 3);
private static final Retrier retrier = new Retrier(new SystemSleeper(), 6);
private static final String NESTED_TRANSACTION_MESSAGE =
"Nested transaction detected. Try refactoring to avoid nested transactions. If unachievable,"
+ " use reTransact() in nested transactions";
@@ -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.
@@ -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);
}
}
}
@@ -31,12 +31,14 @@ import java.util.Optional;
/** RDAP (new WHOIS) action for help requests. */
@Action(
service = GaeService.PUBAPI,
path = "/rdap/help",
path = RdapHelpAction.PATH,
method = {GET, HEAD},
isPrefix = true,
auth = Auth.AUTH_PUBLIC)
public class RdapHelpAction extends RdapActionBase {
public static final String PATH = "/rdap/help";
/** The help path for the RDAP terms of service. */
public static final String TOS_PATH = "/tos";
@@ -19,9 +19,8 @@ import static com.google.common.base.Preconditions.checkState;
import com.google.common.collect.ImmutableMap;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.Registrar.State;
import google.registry.model.registrar.RegistrarAddress;
import google.registry.model.registrar.RegistrarBase;
import google.registry.model.registrar.RegistrarBase.State;
import google.registry.xjc.contact.XjcContactE164Type;
import google.registry.xjc.rderegistrar.XjcRdeRegistrar;
import google.registry.xjc.rderegistrar.XjcRdeRegistrarAddrType;
@@ -41,7 +40,7 @@ final class RegistrarToXjcConverter {
private static final String UNKNOWN_CC = "US";
/** A conversion map between internal Registrar states and external RDE states. */
private static final ImmutableMap<RegistrarBase.State, XjcRdeRegistrarStatusType>
private static final ImmutableMap<Registrar.State, XjcRdeRegistrarStatusType>
REGISTRAR_STATUS_CONVERSIONS =
ImmutableMap.of(
State.ACTIVE, XjcRdeRegistrarStatusType.OK,
@@ -85,9 +85,6 @@ public final class ActivityReportingQueryBuilder implements QueryBuilder {
String monthlyLogsQuery =
SqlTemplate.create(getQueryFromFile(MONTHLY_LOGS + ".sql"))
.put("PROJECT_ID", projectId)
.put("APPENGINE_LOGS_DATA_SET", "appengine_logs")
.put("GKE_LOGS_DATA_SET", "gke_logs")
.put("REQUEST_TABLE", "appengine_googleapis_com_request_log_")
.put("FIRST_DAY_OF_MONTH", logTableFormatter.print(firstDayOfMonth))
.put("LAST_DAY_OF_MONTH", logTableFormatter.print(lastDayOfMonth))
.build();
@@ -96,9 +93,6 @@ public final class ActivityReportingQueryBuilder implements QueryBuilder {
String eppQuery =
SqlTemplate.create(getQueryFromFile(EPP_METRICS + ".sql"))
.put("PROJECT_ID", projectId)
.put("APPENGINE_LOGS_DATA_SET", "appengine_logs")
.put("GKE_LOGS_DATA_SET", "gke_logs")
.put("APP_LOGS_TABLE", "_var_log_app_")
.put("FIRST_DAY_OF_MONTH", logTableFormatter.print(firstDayOfMonth))
.put("LAST_DAY_OF_MONTH", logTableFormatter.print(lastDayOfMonth))
.build();
@@ -112,9 +112,6 @@ public final class TransactionsReportingQueryBuilder implements QueryBuilder {
String attemptedAddsQuery =
SqlTemplate.create(getQueryFromFile(ATTEMPTED_ADDS + ".sql"))
.put("PROJECT_ID", projectId)
.put("APPENGINE_LOGS_DATA_SET", "appengine_logs")
.put("GKE_LOGS_DATA_SET", "gke_logs")
.put("APP_LOGS_TABLE", "_var_log_app_")
.put("FIRST_DAY_OF_MONTH", logTableFormatter.print(earliestReportTime))
.put("LAST_DAY_OF_MONTH", logTableFormatter.print(latestReportTime))
.build();
@@ -143,8 +143,11 @@ public class RequestHandler<C> {
GkeService service = Action.ServiceGetter.get(route.get().action());
String expectedDomain = RegistryConfig.getServiceUrl(service).getHost();
String actualDomain = req.getServerName();
// If the hostname is "localhost", it must have come from the sidecar proxy.
if (!Objects.equals("localhost", actualDomain)
// If the request doesn't come from GKE readiness prober
String maybeUserAgent = Optional.ofNullable(req.getHeader("User-Agent")).orElse("");
if (!maybeUserAgent.startsWith("kube-probe")
// If the hostname is "localhost", it must have come from the sidecar proxy.
&& !Objects.equals("localhost", actualDomain)
&& !Objects.equals(actualDomain, expectedDomain)) {
logger.atWarning().log(
"Actual domain %s does not match expected domain %s", actualDomain, expectedDomain);
@@ -27,7 +27,7 @@ import google.registry.config.RegistryConfig.Config;
import google.registry.groups.GroupsConnection;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarBase.State;
import google.registry.model.registrar.Registrar.State;
import jakarta.inject.Inject;
import java.util.Optional;
import javax.annotation.concurrent.Immutable;
@@ -30,7 +30,6 @@ import com.google.common.collect.ImmutableSet;
import google.registry.flows.certs.CertificateChecker;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarAddress;
import google.registry.model.registrar.RegistrarBase;
import google.registry.tools.params.KeyValueMapParameter.CurrencyUnitToStringMap;
import google.registry.tools.params.OptionalLongParameter;
import google.registry.tools.params.OptionalPhoneNumberParameter;
@@ -61,11 +60,11 @@ abstract class CreateOrUpdateRegistrarCommand extends MutatingCommand {
List<String> mainParameters;
@Parameter(names = "--registrar_type", description = "Type of the registrar")
RegistrarBase.Type registrarType;
Registrar.Type registrarType;
@Nullable
@Parameter(names = "--registrar_state", description = "Initial state of the registrar")
RegistrarBase.State registrarState;
Registrar.State registrarState;
@Parameter(
names = "--allowed_tlds",
@@ -18,7 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Strings.emptyToNull;
import static com.google.common.collect.Iterables.getOnlyElement;
import static google.registry.model.registrar.RegistrarBase.State.ACTIVE;
import static google.registry.model.registrar.Registrar.State.ACTIVE;
import static google.registry.tools.RegistryToolEnvironment.PRODUCTION;
import static google.registry.tools.RegistryToolEnvironment.SANDBOX;
import static google.registry.tools.RegistryToolEnvironment.UNITTEST;
@@ -29,7 +29,6 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.registrar.RegistrarPocBase;
import google.registry.tools.params.OptionalPhoneNumberParameter;
import google.registry.tools.params.PathParameter;
import google.registry.tools.params.StringListParameter;
@@ -153,7 +152,7 @@ final class RegistrarPocCommand extends MutatingCommand {
private static final ImmutableSet<Mode> MODES_REQUIRING_CONTACT_SYNC =
ImmutableSet.of(Mode.CREATE, Mode.UPDATE, Mode.DELETE);
@Nullable private ImmutableSet<RegistrarPocBase.Type> contactTypes;
@Nullable private ImmutableSet<RegistrarPoc.Type> contactTypes;
@Override
protected void init() throws Exception {
@@ -169,7 +168,7 @@ final class RegistrarPocCommand extends MutatingCommand {
} else {
contactTypes =
contactTypeNames.stream()
.map(Enums.stringConverter(RegistrarPocBase.Type.class))
.map(Enums.stringConverter(RegistrarPoc.Type.class))
.collect(toImmutableSet());
}
ImmutableSet<RegistrarPoc> contacts = registrar.getContacts();
@@ -24,7 +24,7 @@ import google.registry.config.RegistryConfig.Config;
import google.registry.groups.GroupsConnection;
import google.registry.groups.GroupsConnection.Role;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPocBase;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.request.Action;
import google.registry.request.Action.GaeService;
import google.registry.request.HttpException.BadRequestException;
@@ -65,7 +65,7 @@ public class CreateGroupsAction implements Runnable {
if (registrar == null) {
return;
}
List<RegistrarPocBase.Type> types = asList(RegistrarPocBase.Type.values());
List<RegistrarPoc.Type> types = asList(RegistrarPoc.Type.values());
// Concurrently create the groups for each RegistrarContact.Type, collecting the results from
// each call (which are either an Exception if it failed, or absent() if it succeeded).
List<Optional<Exception>> results =
@@ -29,7 +29,6 @@ import com.google.re2j.Pattern;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarAddress;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.registrar.RegistrarPocBase;
import google.registry.ui.forms.FormException;
import google.registry.ui.forms.FormField;
import google.registry.ui.forms.FormFieldException;
@@ -201,10 +200,10 @@ public final class RegistrarFormFields {
public static final FormField<String, String> CONTACT_REGISTRY_LOCK_PASSWORD_FIELD =
FormFields.NAME.asBuilderNamed("registryLockPassword").build();
public static final FormField<String, Set<RegistrarPocBase.Type>> CONTACT_TYPES =
public static final FormField<String, Set<RegistrarPoc.Type>> CONTACT_TYPES =
FormField.named("types")
.uppercased()
.asEnum(RegistrarPocBase.Type.class)
.asEnum(RegistrarPoc.Type.class)
.asSet(Splitter.on(',').omitEmptyStrings().trimResults())
.build();
@@ -41,7 +41,6 @@ import google.registry.model.console.ConsoleUpdateHistory;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.registrar.RegistrarPocBase;
import google.registry.request.HttpException;
import google.registry.security.XsrfTokenManager;
import google.registry.util.DiffUtils;
@@ -218,7 +217,7 @@ public abstract class ConsoleApiAction implements Runnable {
consoleApiParams.authResult().userIdForLogging(),
DiffUtils.prettyPrintDiffedMap(diffs, null)),
contacts.stream()
.filter(c -> c.getTypes().contains(RegistrarPocBase.Type.ADMIN))
.filter(c -> c.getTypes().contains(RegistrarPoc.Type.ADMIN))
.map(RegistrarPoc::getEmailAddress)
.collect(toImmutableList()));
}
@@ -262,7 +261,7 @@ public abstract class ConsoleApiAction implements Runnable {
}
}
protected void finishAndPersistConsoleUpdateHistory(ConsoleUpdateHistory.Builder<?, ?> builder) {
protected void finishAndPersistConsoleUpdateHistory(ConsoleUpdateHistory.Builder builder) {
builder.setActingUser(consoleApiParams.authResult().user().get());
builder.setUrl(consoleApiParams.request().getRequestURI());
builder.setMethod(consoleApiParams.request().getMethod());
@@ -28,7 +28,6 @@ import com.google.gson.annotations.Expose;
import google.registry.flows.EppException.AuthenticationErrorException;
import google.registry.flows.PasswordOnlyTransportCredentials;
import google.registry.model.console.ConsoleUpdateHistory;
import google.registry.model.console.RegistrarUpdateHistory;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.request.Action;
@@ -107,14 +106,10 @@ public class ConsoleEppPasswordAction extends ConsoleApiAction {
Registrar updatedRegistrar =
registrar.asBuilder().setPassword(eppRequestBody.newPassword()).build();
tm().put(updatedRegistrar);
EppPasswordData sanitizedData =
new EppPasswordData(
eppRequestBody.registrarId, "********", "••••••••", "••••••••");
finishAndPersistConsoleUpdateHistory(
new RegistrarUpdateHistory.Builder()
.setType(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE)
.setRegistrar(updatedRegistrar)
.setRequestBody(consoleApiParams.gson().toJson(sanitizedData)));
new ConsoleUpdateHistory.Builder()
.setType(ConsoleUpdateHistory.Type.EPP_PASSWORD_UPDATE)
.setDescription(registrar.getRegistrarId()));
sendExternalUpdates(
ImmutableMap.of("password", new DiffUtils.DiffPair("********", "••••••••")),
registrar,
@@ -35,7 +35,6 @@ import google.registry.model.OteStats.StatType;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarBase;
import google.registry.request.Action;
import google.registry.request.Action.GkeService;
import google.registry.request.Parameter;
@@ -140,7 +139,7 @@ public class ConsoleOteAction extends ConsoleApiAction {
SC_BAD_REQUEST);
return;
}
if (!RegistrarBase.Type.OTE.equals(registrar.get().getType())) {
if (!Registrar.Type.OTE.equals(registrar.get().getType())) {
setFailedResponse(
String.format("Registrar with ID %s is not an OT&E registrar", registrarId),
SC_BAD_REQUEST);
@@ -24,7 +24,6 @@ import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.ConsoleUpdateHistory;
import google.registry.model.console.RegistrarUpdateHistory;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.request.Action;
@@ -102,10 +101,9 @@ public class ConsoleUpdateRegistrarAction extends ConsoleApiAction {
tm().put(updatedRegistrar);
finishAndPersistConsoleUpdateHistory(
new RegistrarUpdateHistory.Builder()
new ConsoleUpdateHistory.Builder()
.setType(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE)
.setRegistrar(updatedRegistrar)
.setRequestBody(consoleApiParams.gson().toJson(registrarParam)));
.setDescription(updatedRegistrar.getRegistrarId()));
sendExternalUpdatesIfNecessary(
EmailInfo.create(
existingRegistrar.get(),
@@ -165,7 +165,7 @@ public class ConsoleUsersAction extends ConsoleApiAction {
User updatedUser = updateUserRegistrarRoles(email, registrarId, null);
// User has no registrars assigned
if (updatedUser.getUserRoles().getRegistrarRoles().size() == 0) {
if (updatedUser.getUserRoles().getRegistrarRoles().isEmpty()) {
try {
directory.users().delete(email).execute();
} catch (IOException e) {
@@ -28,11 +28,9 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.ConsoleUpdateHistory;
import google.registry.model.console.RegistrarUpdateHistory;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarBase;
import google.registry.model.registrar.RegistrarBase.State;
import google.registry.model.registrar.Registrar.State;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.request.Action;
import google.registry.request.Action.GaeService;
@@ -55,8 +53,8 @@ import java.util.Optional;
public class RegistrarsAction extends ConsoleApiAction {
private static final int PASSWORD_LENGTH = 16;
private static final int PASSCODE_LENGTH = 5;
private static final ImmutableList<RegistrarBase.Type> allowedRegistrarTypes =
ImmutableList.of(Registrar.Type.REAL, RegistrarBase.Type.OTE);
private static final ImmutableList<Registrar.Type> allowedRegistrarTypes =
ImmutableList.of(Registrar.Type.REAL, Registrar.Type.OTE);
private static final String SQL_TEMPLATE =
"""
SELECT * FROM "Registrar"
@@ -174,10 +172,9 @@ public class RegistrarsAction extends ConsoleApiAction {
registrar.getRegistrarId());
tm().putAll(registrar, contact);
finishAndPersistConsoleUpdateHistory(
new RegistrarUpdateHistory.Builder()
.setType(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE)
.setRegistrar(registrar)
.setRequestBody(consoleApiParams.gson().toJson(registrar)));
new ConsoleUpdateHistory.Builder()
.setType(ConsoleUpdateHistory.Type.REGISTRAR_CREATE)
.setDescription(registrar.getRegistrarId()));
});
}
@@ -15,6 +15,7 @@
package google.registry.ui.server.console.domains;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -25,6 +26,7 @@ import google.registry.flows.EppController;
import google.registry.flows.EppRequestSource;
import google.registry.flows.PasswordOnlyTransportCredentials;
import google.registry.flows.StatelessRequestSessionMetadata;
import google.registry.model.console.ConsoleUpdateHistory;
import google.registry.model.console.User;
import google.registry.model.eppcommon.ProtocolDefinition;
import google.registry.model.eppoutput.EppOutput;
@@ -93,11 +95,29 @@ public class ConsoleBulkDomainAction extends ConsoleApiAction {
domainList.domainList.stream()
.collect(
toImmutableMap(d -> d, d -> executeEpp(actionType.getXmlContentsToRun(d), user)));
handleHistoryAdditions(result, actionType);
// Front end should parse situations where only some commands worked
consoleApiParams.response().setPayload(consoleApiParams.gson().toJson(result));
consoleApiParams.response().setStatus(SC_OK);
}
private void handleHistoryAdditions(
ImmutableMap<String, ConsoleEppOutput> result, ConsoleDomainActionType actionType) {
if (result.values().stream().noneMatch(ConsoleEppOutput::isSuccess)) {
return;
}
tm().transact(
() ->
result.entrySet().stream()
.filter(e -> e.getValue().isSuccess())
.forEach(
e ->
finishAndPersistConsoleUpdateHistory(
new ConsoleUpdateHistory.Builder()
.setDescription(e.getKey())
.setType(actionType.getConsoleUpdateHistoryType()))));
}
private ConsoleEppOutput executeEpp(String xml, User user) {
return ConsoleEppOutput.fromEppOutput(
eppController.handleEppCommand(
@@ -115,5 +135,9 @@ public class ConsoleBulkDomainAction extends ConsoleApiAction {
Result result = eppOutput.getResponse().getResult();
return new ConsoleEppOutput(result.getMsg(), result.getCode().code);
}
boolean isSuccess() {
return responseCode < 2000;
}
}
}
@@ -16,6 +16,7 @@ package google.registry.ui.server.console.domains;
import com.google.gson.JsonElement;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.ConsoleUpdateHistory;
/** An action that will run a delete EPP command on the given domain. */
public class ConsoleBulkDomainDeleteActionType extends ConsoleDomainActionType {
@@ -54,4 +55,9 @@ public class ConsoleBulkDomainDeleteActionType extends ConsoleDomainActionType {
public ConsolePermission getNecessaryPermission() {
return ConsolePermission.EXECUTE_EPP_COMMANDS;
}
@Override
public ConsoleUpdateHistory.Type getConsoleUpdateHistoryType() {
return ConsoleUpdateHistory.Type.DOMAIN_DELETE;
}
}
@@ -16,6 +16,7 @@ package google.registry.ui.server.console.domains;
import com.google.gson.JsonElement;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.ConsoleUpdateHistory;
/** An action that will suspend the given domain, assigning all 5 server*Prohibited statuses. */
public class ConsoleBulkDomainSuspendActionType extends ConsoleDomainActionType {
@@ -64,4 +65,9 @@ public class ConsoleBulkDomainSuspendActionType extends ConsoleDomainActionType
public ConsolePermission getNecessaryPermission() {
return ConsolePermission.SUSPEND_DOMAIN;
}
@Override
public ConsoleUpdateHistory.Type getConsoleUpdateHistoryType() {
return ConsoleUpdateHistory.Type.DOMAIN_SUSPEND;
}
}
@@ -16,6 +16,7 @@ package google.registry.ui.server.console.domains;
import com.google.gson.JsonElement;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.ConsoleUpdateHistory;
/** An action that will unsuspend the given domain, removing all 5 server*Prohibited statuses. */
public class ConsoleBulkDomainUnsuspendActionType extends ConsoleDomainActionType {
@@ -64,4 +65,9 @@ public class ConsoleBulkDomainUnsuspendActionType extends ConsoleDomainActionTyp
public ConsolePermission getNecessaryPermission() {
return ConsolePermission.SUSPEND_DOMAIN;
}
@Override
public ConsoleUpdateHistory.Type getConsoleUpdateHistoryType() {
return ConsoleUpdateHistory.Type.DOMAIN_UNSUSPEND;
}
}
@@ -18,6 +18,7 @@ import com.google.common.escape.Escaper;
import com.google.common.xml.XmlEscapers;
import com.google.gson.JsonElement;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.ConsoleUpdateHistory;
/**
* A type of EPP action to perform on domain(s), run by the {@link ConsoleBulkDomainAction}.
@@ -68,6 +69,9 @@ public abstract class ConsoleDomainActionType {
/** Returns the permission necessary to successfully perform this action. */
public abstract ConsolePermission getNecessaryPermission();
/** Returns the type of history / audit logging object to save. */
public abstract ConsoleUpdateHistory.Type getConsoleUpdateHistoryType();
/** Returns the XML template contents for this action. */
protected abstract String getXmlTemplate();
@@ -32,7 +32,7 @@ import google.registry.model.console.ConsolePermission;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.registrar.RegistrarPocBase.Type;
import google.registry.model.registrar.RegistrarPoc.Type;
import google.registry.persistence.transaction.QueryComposer.Comparator;
import google.registry.request.Action;
import google.registry.request.Action.GaeService;
@@ -169,6 +169,7 @@ public class ContactAction extends ConsoleApiAction {
throw new ContactRequirementException(t);
}
}
enforcePrimaryContactRestrictions(oldContactsByType, newContactsByType);
ensurePhoneNumberNotRemovedForContactTypes(oldContactsByType, newContactsByType, Type.TECH);
Optional<RegistrarPoc> domainWhoisAbuseContact =
getDomainWhoisVisibleAbuseContact(updatedContacts);
@@ -187,6 +188,23 @@ public class ContactAction extends ConsoleApiAction {
checkContactRegistryLockRequirements(existingContacts, updatedContacts);
}
private static void enforcePrimaryContactRestrictions(
Multimap<Type, RegistrarPoc> oldContactsByType,
Multimap<Type, RegistrarPoc> newContactsByType) {
ImmutableSet<String> oldAdminEmails =
oldContactsByType.get(Type.ADMIN).stream()
.map(RegistrarPoc::getEmailAddress)
.collect(toImmutableSet());
ImmutableSet<String> newAdminEmails =
newContactsByType.get(Type.ADMIN).stream()
.map(RegistrarPoc::getEmailAddress)
.collect(toImmutableSet());
if (!newAdminEmails.containsAll(oldAdminEmails)) {
throw new ContactRequirementException(
"Cannot remove or change the email address of primary contacts");
}
}
private static void checkContactRegistryLockRequirements(
ImmutableSet<RegistrarPoc> existingContacts, ImmutableSet<RegistrarPoc> updatedContacts) {
// Any contact(s) with new passwords must be allowed to set them
@@ -17,13 +17,11 @@ package google.registry.ui.server.console.settings;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.POST;
import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.ConsoleUpdateHistory;
import google.registry.model.console.RegistrarUpdateHistory;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.request.Action;
@@ -36,7 +34,6 @@ import google.registry.request.auth.AuthenticatedRegistrarAccessor.RegistrarAcce
import google.registry.ui.server.console.ConsoleApiAction;
import google.registry.ui.server.console.ConsoleApiParams;
import jakarta.inject.Inject;
import java.util.Objects;
import java.util.Optional;
/**
@@ -48,17 +45,17 @@ import java.util.Optional;
@Action(
service = GaeService.DEFAULT,
gkeService = GkeService.CONSOLE,
path = WhoisRegistrarFieldsAction.PATH,
path = RdapRegistrarFieldsAction.PATH,
method = {POST},
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public class WhoisRegistrarFieldsAction extends ConsoleApiAction {
public class RdapRegistrarFieldsAction extends ConsoleApiAction {
static final String PATH = "/console-api/settings/whois-fields";
static final String PATH = "/console-api/settings/rdap-fields";
private final AuthenticatedRegistrarAccessor registrarAccessor;
private final Optional<Registrar> registrar;
@Inject
public WhoisRegistrarFieldsAction(
public RdapRegistrarFieldsAction(
ConsoleApiParams consoleApiParams,
AuthenticatedRegistrarAccessor registrarAccessor,
@Parameter("registrar") Optional<Registrar> registrar) {
@@ -72,10 +69,10 @@ public class WhoisRegistrarFieldsAction extends ConsoleApiAction {
checkArgument(registrar.isPresent(), "'registrar' parameter is not present");
checkPermission(
user, registrar.get().getRegistrarId(), ConsolePermission.EDIT_REGISTRAR_DETAILS);
tm().transact(() -> loadAndModifyRegistrar(registrar.get(), user));
tm().transact(() -> loadAndModifyRegistrar(registrar.get()));
}
private void loadAndModifyRegistrar(Registrar providedRegistrar, User user) {
private void loadAndModifyRegistrar(Registrar providedRegistrar) {
Registrar savedRegistrar;
try {
// reload to make sure the object has all the correct fields
@@ -85,32 +82,19 @@ public class WhoisRegistrarFieldsAction extends ConsoleApiAction {
return;
}
// icannReferralEmail can't be updated by partners, only by global users with
// EDIT_REGISTRAR_DETAILS permission
if (!Objects.equals(
savedRegistrar.getIcannReferralEmail(), providedRegistrar.getIcannReferralEmail())
&& !user.getUserRoles().hasGlobalPermission(ConsolePermission.EDIT_REGISTRAR_DETAILS)) {
setFailedResponse(
"Icann Referral Email update is not permitted for this user", SC_BAD_REQUEST);
}
Registrar newRegistrar =
savedRegistrar
.asBuilder()
.setWhoisServer(providedRegistrar.getWhoisServer())
.setUrl(providedRegistrar.getUrl())
.setLocalizedAddress(providedRegistrar.getLocalizedAddress())
.setPhoneNumber(providedRegistrar.getPhoneNumber())
.setFaxNumber(providedRegistrar.getFaxNumber())
.setIcannReferralEmail(providedRegistrar.getIcannReferralEmail())
.setEmailAddress(providedRegistrar.getEmailAddress())
.build();
tm().put(newRegistrar);
finishAndPersistConsoleUpdateHistory(
new RegistrarUpdateHistory.Builder()
new ConsoleUpdateHistory.Builder()
.setType(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE)
.setRegistrar(newRegistrar)
.setRequestBody(consoleApiParams.gson().toJson(registrar.get())));
.setDescription(newRegistrar.getRegistrarId()));
sendExternalUpdatesIfNecessary(
EmailInfo.create(
savedRegistrar,
@@ -26,7 +26,6 @@ import google.registry.flows.certs.CertificateChecker;
import google.registry.flows.certs.CertificateChecker.InsecureCertificateException;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.ConsoleUpdateHistory;
import google.registry.model.console.RegistrarUpdateHistory;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.request.Action;
@@ -120,10 +119,9 @@ public class SecurityAction extends ConsoleApiAction {
Registrar updatedRegistrar = updatedRegistrarBuilder.build();
tm().put(updatedRegistrar);
finishAndPersistConsoleUpdateHistory(
new RegistrarUpdateHistory.Builder()
.setType(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE)
.setRegistrar(updatedRegistrar)
.setRequestBody(consoleApiParams.gson().toJson(registrar.get())));
new ConsoleUpdateHistory.Builder()
.setType(ConsoleUpdateHistory.Type.REGISTRAR_SECURITY_UPDATE)
.setDescription(registrarId));
sendExternalUpdatesIfNecessary(
EmailInfo.create(savedRegistrar, updatedRegistrar, ImmutableSet.of(), ImmutableSet.of()));
@@ -47,11 +47,8 @@
<class>google.registry.model.billing.BillingRecurrence</class>
<class>google.registry.model.common.Cursor</class>
<class>google.registry.model.common.DnsRefreshRequest</class>
<class>google.registry.model.console.ConsoleEppActionHistory</class>
<class>google.registry.model.console.RegistrarPocUpdateHistory</class>
<class>google.registry.model.console.RegistrarUpdateHistory</class>
<class>google.registry.model.console.ConsoleUpdateHistory</class>
<class>google.registry.model.console.User</class>
<class>google.registry.model.console.UserUpdateHistory</class>
<class>google.registry.model.contact.ContactHistory</class>
<class>google.registry.model.contact.Contact</class>
<class>google.registry.model.domain.Domain</class>
@@ -16,7 +16,7 @@
-- Determine the number of attempted adds each registrar made.
-- Since the specification requests all 'attempted' adds, we regex the
-- monthly App Engine logs, searching for all create commands and associating
-- monthly GKE logs, searching for all create commands and associating
-- them with their corresponding registrars.
-- Example log generated by FlowReporter in App Engine and GKE logs:
@@ -27,7 +27,7 @@
-- ,"targetId":"","targetIds":[],"tld":"",
-- "tlds":[],"icannActivityReportField":""}
-- This outer select just converts the registrar's clientId to their name.
-- This outer select just converts the registrar's ID to their name.
SELECT
tld,
registrar_table.registrar_name AS registrar_name,
@@ -38,34 +38,20 @@ FROM (
JSON_EXTRACT_SCALAR(json, '$.tld') AS tld,
JSON_EXTRACT_SCALAR(json, '$.clientId') AS clientId,
COUNT(json) AS count
FROM ((
-- Extract JSON metadata package from monthly logs
FROM (
-- Extract JSON metadata package from monthly logs
SELECT
REGEXP_EXTRACT(textPayload, r'FLOW-LOG-SIGNATURE-METADATA: (.*)\n?$') AS json
REGEXP_EXTRACT(jsonPayload.message, r'FLOW-LOG-SIGNATURE-METADATA: (.*)\n?$') AS json
FROM
`%PROJECT_ID%.%APPENGINE_LOGS_DATA_SET%.%APP_LOGS_TABLE%*`
`%PROJECT_ID%.gke_logs.stderr_*`
WHERE
_TABLE_SUFFIX BETWEEN '%FIRST_DAY_OF_MONTH%'
AND '%LAST_DAY_OF_MONTH%'
AND STARTS_WITH(textPayload, "FLOW-LOG-SIGNATURE-METADATA")
AND STARTS_WITH(jsonPayload.message, "FLOW-LOG-SIGNATURE-METADATA")
-- Look for domain creates
AND REGEXP_CONTAINS(textPayload, r'"commandType":"create","resourceType":"domain"')
AND REGEXP_CONTAINS(jsonPayload.message, r'"commandType":"create","resourceType":"domain"')
-- Filter prober data
AND NOT REGEXP_CONTAINS(textPayload, r'"prober-[a-z]{2}-((any)|(canary))"'))
UNION ALL (
-- Extract JSON metadata package from monthly logs
SELECT
REGEXP_EXTRACT(jsonPayload.message, r'FLOW-LOG-SIGNATURE-METADATA: (.*)\n?$') AS json
FROM
`%PROJECT_ID%.%GKE_LOGS_DATA_SET%.stderr_*`
WHERE
_TABLE_SUFFIX BETWEEN '%FIRST_DAY_OF_MONTH%'
AND '%LAST_DAY_OF_MONTH%'
AND STARTS_WITH(jsonPayload.message, "FLOW-LOG-SIGNATURE-METADATA")
-- Look for domain creates
AND REGEXP_CONTAINS(jsonPayload.message, r'"commandType":"create","resourceType":"domain"')
-- Filter prober data
AND NOT REGEXP_CONTAINS(jsonPayload.message, r'"prober-[a-z]{2}-((any)|(canary))"')))
AND NOT REGEXP_CONTAINS(jsonPayload.message, r'"prober-[a-z]{2}-((any)|(canary))"'))
GROUP BY
tld,
clientId ) AS logs_table
@@ -15,7 +15,7 @@
-- Query FlowReporter JSON log messages and calculate SRS metrics.
-- We use ugly regex's over the monthly appengine logs to determine how many
-- We use ugly regexes over the monthly GKE logs to determine how many
-- EPP requests we received for each command. For example:
-- {"commandType":"check"...,"targetIds":["ais.a.how"],
-- "tld":"","tlds":["a.how"],"icannActivityReportField":"srs-dom-check"}
@@ -35,30 +35,15 @@ FROM (
JSON_EXTRACT_SCALAR(json,
'$.icannActivityReportField') AS activityReportField
FROM (
-- For reasons that I don't understand, if I directly select the three columns
-- from the union, BigQuery complains about column number mismatch, so I have to
-- make a temporary union table and select on it.
SELECT
*
FROM (
SELECT
-- Extract the logged JSON payload.
REGEXP_EXTRACT(textPayload, r'FLOW-LOG-SIGNATURE-METADATA: (.*)\n?$')
AS json
FROM `%PROJECT_ID%.%APPENGINE_LOGS_DATA_SET%.%APP_LOGS_TABLE%*`
WHERE
STARTS_WITH(textPayload, "FLOW-LOG-SIGNATURE-METADATA")
AND _TABLE_SUFFIX BETWEEN '%FIRST_DAY_OF_MONTH%' AND '%LAST_DAY_OF_MONTH%')
UNION ALL (
SELECT
-- Extract the logged JSON payload.
REGEXP_EXTRACT(jsonPayload.message, r'FLOW-LOG-SIGNATURE-METADATA: (.*)\n?$')
AS json
FROM `%PROJECT_ID%.%GKE_LOGS_DATA_SET%.stderr_*`
WHERE
STARTS_WITH(jsonPayload.message, "FLOW-LOG-SIGNATURE-METADATA")
AND _TABLE_SUFFIX BETWEEN '%FIRST_DAY_OF_MONTH%' AND '%LAST_DAY_OF_MONTH%')
)) AS regexes
-- Extract the logged JSON payload.
REGEXP_EXTRACT(jsonPayload.message, r'FLOW-LOG-SIGNATURE-METADATA: (.*)\n?$')
AS json
FROM `%PROJECT_ID%.gke_logs.stderr_*`
WHERE
STARTS_WITH(jsonPayload.message, "FLOW-LOG-SIGNATURE-METADATA")
AND _TABLE_SUFFIX BETWEEN '%FIRST_DAY_OF_MONTH%' AND '%LAST_DAY_OF_MONTH%')
) AS regexes
JOIN
-- Unnest the JSON-parsed tlds.
UNNEST(regexes.tlds) AS tld
@@ -13,7 +13,7 @@
-- See the License for the specific language governing permissions and
-- limitations under the License.
-- Query to fetch AppEngine and GKE request logs for the report month.
-- Query to fetch GKE request logs for the report month.
-- START_OF_MONTH and END_OF_MONTH should be in YYYYMM01 format.
@@ -23,13 +23,6 @@ FROM (
SELECT
jsonPayload.httrequest.requesturl AS requestPath
FROM
`%PROJECT_ID%.%GKE_LOGS_DATA_SET%.stderr_*`
WHERE
_TABLE_SUFFIX BETWEEN '%FIRST_DAY_OF_MONTH%' AND '%LAST_DAY_OF_MONTH%')
UNION ALL (
SELECT
protoPayload.resource AS requestPath
FROM
`%PROJECT_ID%.%APPENGINE_LOGS_DATA_SET%.%REQUEST_TABLE%*`
`%PROJECT_ID%.gke_logs.stderr_*`
WHERE
_TABLE_SUFFIX BETWEEN '%FIRST_DAY_OF_MONTH%' AND '%LAST_DAY_OF_MONTH%')
@@ -32,7 +32,16 @@ FROM (
WHEN field = 'TRANSFER_NACKED' THEN 'TRANSFER_GAINING_NACKED'
ELSE field
END AS metricName,
SUM(amount) AS metricValue
-- See b/290228682, there are edge cases in which the net_renew would be negative when
-- a domain is cancelled by superusers during renew grace period. The correct thing
-- to do is attribute the cancellation to the owning registrar, but that would require
-- changing the owing registrar of the the corresponding cancellation DomainHistory,
-- which has cascading effects that we don't want to deal with. As such we simply
-- floor the number here to zero to prevent any negative value from appearing, which
-- should have negligible impact as the edge cage happens very rarely, more specifically
-- when a cancellation happens during grace period by a registrar other than the the
-- owning one. All the numbers here should be non-negative to pass ICANN validation.
GREATEST(SUM(amount), 0) AS metricValue
FROM (
SELECT
CASE
@@ -47,16 +56,7 @@ FROM (
END AS clientId,
tld,
report_field AS field,
-- See b/290228682, there are edge cases in which the net_renew would be negative when
-- a domain is cancelled by superusers during renew grace period. The correct thing
-- to do is attribute the cancellation to the owning registrar, but that would require
-- changing the owing registrar of the the corresponding cancellation DomainHistory,
-- which has cascading effects that we don't want to deal with. As such we simply
-- floor the number here to zero to prevent any negative value from appearing, which
-- should have negligible impact as the edge cage happens very rarely, more specifically
-- when a cancellation happens during grace period by a registrar other than the the
-- owning one. All the numbers here should be positive to pass ICANN validation.
GREATEST(report_amount, 0) AS amount,
report_amount AS amount,
reporting_time AS reportingTime
FROM EXTERNAL_QUERY("projects/%PROJECT_ID%/locations/us/connections/%PROJECT_ID%-sql",
''' SELECT history_type, history_other_registrar_id, history_registrar_id, domain_repo_id, history_revision_id FROM "DomainHistory";''') AS dh
@@ -36,8 +36,7 @@ import google.registry.groups.GmailClient;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarAddress;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.registrar.RegistrarPocBase;
import google.registry.model.registrar.RegistrarPocBase.Type;
import google.registry.model.registrar.RegistrarPoc.Type;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.FakeClock;
@@ -57,13 +56,13 @@ class SendExpiringCertificateNotificationEmailActionTest {
private static final String EXPIRATION_WARNING_EMAIL_BODY_TEXT =
"""
Dear %1$s,
Dear %1$s,
We would like to inform you that your %2$s certificate will expire at %3$s.
Kind update your account using the following steps:
1. Navigate to support and login using your %4$s@registry.example credentials.
2. Click Settings -> Privacy on the top left corner.
3. Click Edit and enter certificate string. 3. Click SaveRegards,Example Registry""";
We would like to inform you that your %2$s certificate will expire at %3$s.
Kind update your account using the following steps:
1. Navigate to support and login using your %4$s@registry.example credentials.
2. Click Settings -> Privacy on the top left corner.
3. Click Edit and enter certificate string. 3. Click SaveRegards,Example Registry""";
private static final String EXPIRATION_WARNING_EMAIL_SUBJECT_TEXT = "Expiration Warning Email";
@@ -220,7 +219,7 @@ class SendExpiringCertificateNotificationEmailActionTest {
.setEmailAddress("will@example-registrar.tld")
.setPhoneNumber("+1.3105551213")
.setFaxNumber("+1.3105551213")
.setTypes(ImmutableSet.of(RegistrarPocBase.Type.TECH))
.setTypes(ImmutableSet.of(RegistrarPoc.Type.TECH))
.setVisibleInWhoisAsAdmin(true)
.setVisibleInWhoisAsTech(false)
.build());
@@ -510,7 +509,7 @@ class SendExpiringCertificateNotificationEmailActionTest {
.setEmailAddress("jd@example-registrar.tld")
.setPhoneNumber("+1.3105551213")
.setFaxNumber("+1.3105551213")
.setTypes(ImmutableSet.of(RegistrarPocBase.Type.TECH))
.setTypes(ImmutableSet.of(RegistrarPoc.Type.TECH))
.setVisibleInWhoisAsAdmin(true)
.setVisibleInWhoisAsTech(false)
.build(),
@@ -520,7 +519,7 @@ class SendExpiringCertificateNotificationEmailActionTest {
.setEmailAddress("js@example-registrar.tld")
.setPhoneNumber("+1.1111111111")
.setFaxNumber("+1.1111111111")
.setTypes(ImmutableSet.of(RegistrarPocBase.Type.TECH))
.setTypes(ImmutableSet.of(RegistrarPoc.Type.TECH))
.build(),
new RegistrarPoc.Builder()
.setRegistrar(registrar)
@@ -528,7 +527,7 @@ class SendExpiringCertificateNotificationEmailActionTest {
.setEmailAddress("will@example-registrar.tld")
.setPhoneNumber("+1.3105551213")
.setFaxNumber("+1.3105551213")
.setTypes(ImmutableSet.of(RegistrarPocBase.Type.TECH))
.setTypes(ImmutableSet.of(RegistrarPoc.Type.TECH))
.setVisibleInWhoisAsAdmin(true)
.setVisibleInWhoisAsTech(false)
.build(),
@@ -538,7 +537,7 @@ class SendExpiringCertificateNotificationEmailActionTest {
.setEmailAddress("mike@example-registrar.tld")
.setPhoneNumber("+1.1111111111")
.setFaxNumber("+1.1111111111")
.setTypes(ImmutableSet.of(RegistrarPocBase.Type.ADMIN))
.setTypes(ImmutableSet.of(RegistrarPoc.Type.ADMIN))
.build(),
new RegistrarPoc.Builder()
.setRegistrar(registrar)
@@ -546,7 +545,7 @@ class SendExpiringCertificateNotificationEmailActionTest {
.setEmailAddress("john@example-registrar.tld")
.setPhoneNumber("+1.3105551215")
.setFaxNumber("+1.3105551216")
.setTypes(ImmutableSet.of(RegistrarPocBase.Type.ADMIN))
.setTypes(ImmutableSet.of(RegistrarPoc.Type.ADMIN))
.setVisibleInWhoisAsTech(true)
.build());
persistSimpleResources(contacts);
@@ -700,7 +699,7 @@ class SendExpiringCertificateNotificationEmailActionTest {
/** Returns persisted sample contacts with a customized contact email type. */
private static ImmutableList<RegistrarPoc> persistSampleContacts(
Registrar registrar, RegistrarPocBase.Type emailType) {
Registrar registrar, RegistrarPoc.Type emailType) {
return persistSimpleResources(
ImmutableList.of(
new RegistrarPoc.Builder()
@@ -70,7 +70,7 @@ import google.registry.model.rde.RdeMode;
import google.registry.model.rde.RdeRevision;
import google.registry.model.rde.RdeRevision.RdeRevisionId;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarBase.State;
import google.registry.model.registrar.Registrar.State;
import google.registry.model.reporting.DomainTransactionRecord;
import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField;
import google.registry.model.reporting.HistoryEntry;
@@ -388,22 +388,22 @@ public class RdePipelineTest {
assertThat(domainFrags.stream().findFirst().get().xml().strip())
.isEqualTo(
"""
<rdeDomain:domain>
<rdeDomain:name>cat.fun</rdeDomain:name>
<rdeDomain:roid>15-FUN</rdeDomain:roid>
<rdeDomain:uName>cat.fun</rdeDomain:uName>
<rdeDomain:status s="ok"/>
<rdeDomain:contact type="admin">contact456</rdeDomain:contact>
<rdeDomain:contact type="tech">contact456</rdeDomain:contact>
<rdeDomain:ns>
<domain:hostObj>ns1.external.tld</domain:hostObj>
<domain:hostObj>ns1.hello.soy</domain:hostObj>
</rdeDomain:ns>
<rdeDomain:clID>TheRegistrar</rdeDomain:clID>
<rdeDomain:crRr>TheRegistrar</rdeDomain:crRr>
<rdeDomain:crDate>1970-01-01T00:00:00Z</rdeDomain:crDate>
<rdeDomain:exDate>294247-01-10T04:00:54Z</rdeDomain:exDate>
</rdeDomain:domain>""");
<rdeDomain:domain>
<rdeDomain:name>cat.fun</rdeDomain:name>
<rdeDomain:roid>15-FUN</rdeDomain:roid>
<rdeDomain:uName>cat.fun</rdeDomain:uName>
<rdeDomain:status s="ok"/>
<rdeDomain:contact type="admin">contact456</rdeDomain:contact>
<rdeDomain:contact type="tech">contact456</rdeDomain:contact>
<rdeDomain:ns>
<domain:hostObj>ns1.external.tld</domain:hostObj>
<domain:hostObj>ns1.hello.soy</domain:hostObj>
</rdeDomain:ns>
<rdeDomain:clID>TheRegistrar</rdeDomain:clID>
<rdeDomain:crRr>TheRegistrar</rdeDomain:crRr>
<rdeDomain:crDate>1970-01-01T00:00:00Z</rdeDomain:crDate>
<rdeDomain:exDate>294247-01-10T04:00:54Z</rdeDomain:exDate>
</rdeDomain:domain>""");
}
if (kv.getKey().mode().equals(FULL)) {
// Contact fragments for hello.soy.
@@ -425,23 +425,23 @@ public class RdePipelineTest {
assertThat(domainFrags.stream().findFirst().get().xml().strip())
.isEqualTo(
"""
<rdeDomain:domain>
<rdeDomain:name>hello.soy</rdeDomain:name>
<rdeDomain:roid>E-SOY</rdeDomain:roid>
<rdeDomain:uName>hello.soy</rdeDomain:uName>
<rdeDomain:status s="ok"/>
<rdeDomain:registrant>contact1234</rdeDomain:registrant>
<rdeDomain:contact type="admin">contact789</rdeDomain:contact>
<rdeDomain:contact type="tech">contact1234</rdeDomain:contact>
<rdeDomain:ns>
<domain:hostObj>ns1.external.tld</domain:hostObj>
<domain:hostObj>ns1.lol.cat</domain:hostObj>
</rdeDomain:ns>
<rdeDomain:clID>TheRegistrar</rdeDomain:clID>
<rdeDomain:crRr>TheRegistrar</rdeDomain:crRr>
<rdeDomain:crDate>1970-01-01T00:00:00Z</rdeDomain:crDate>
<rdeDomain:exDate>294247-01-10T04:00:54Z</rdeDomain:exDate>
</rdeDomain:domain>""");
<rdeDomain:domain>
<rdeDomain:name>hello.soy</rdeDomain:name>
<rdeDomain:roid>E-SOY</rdeDomain:roid>
<rdeDomain:uName>hello.soy</rdeDomain:uName>
<rdeDomain:status s="ok"/>
<rdeDomain:registrant>contact1234</rdeDomain:registrant>
<rdeDomain:contact type="admin">contact789</rdeDomain:contact>
<rdeDomain:contact type="tech">contact1234</rdeDomain:contact>
<rdeDomain:ns>
<domain:hostObj>ns1.external.tld</domain:hostObj>
<domain:hostObj>ns1.lol.cat</domain:hostObj>
</rdeDomain:ns>
<rdeDomain:clID>TheRegistrar</rdeDomain:clID>
<rdeDomain:crRr>TheRegistrar</rdeDomain:crRr>
<rdeDomain:crDate>1970-01-01T00:00:00Z</rdeDomain:crDate>
<rdeDomain:exDate>294247-01-10T04:00:54Z</rdeDomain:exDate>
</rdeDomain:domain>""");
} else {
// Contact fragments for cat.fun.
assertThat(
@@ -471,20 +471,20 @@ public class RdePipelineTest {
assertThat(domainFrags.stream().findFirst().get().xml().strip())
.isEqualTo(
"""
<rdeDomain:domain>
<rdeDomain:name>hello.soy</rdeDomain:name>
<rdeDomain:roid>E-SOY</rdeDomain:roid>
<rdeDomain:uName>hello.soy</rdeDomain:uName>
<rdeDomain:status s="ok"/>
<rdeDomain:ns>
<domain:hostObj>ns1.external.tld</domain:hostObj>
<domain:hostObj>ns1.lol.cat</domain:hostObj>
</rdeDomain:ns>
<rdeDomain:clID>TheRegistrar</rdeDomain:clID>
<rdeDomain:crRr>TheRegistrar</rdeDomain:crRr>
<rdeDomain:crDate>1970-01-01T00:00:00Z</rdeDomain:crDate>
<rdeDomain:exDate>294247-01-10T04:00:54Z</rdeDomain:exDate>
</rdeDomain:domain>""");
<rdeDomain:domain>
<rdeDomain:name>hello.soy</rdeDomain:name>
<rdeDomain:roid>E-SOY</rdeDomain:roid>
<rdeDomain:uName>hello.soy</rdeDomain:uName>
<rdeDomain:status s="ok"/>
<rdeDomain:ns>
<domain:hostObj>ns1.external.tld</domain:hostObj>
<domain:hostObj>ns1.lol.cat</domain:hostObj>
</rdeDomain:ns>
<rdeDomain:clID>TheRegistrar</rdeDomain:clID>
<rdeDomain:crRr>TheRegistrar</rdeDomain:crRr>
<rdeDomain:crDate>1970-01-01T00:00:00Z</rdeDomain:crDate>
<rdeDomain:exDate>294247-01-10T04:00:54Z</rdeDomain:exDate>
</rdeDomain:domain>""");
}
});
return null;

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