1
0
mirror of https://github.com/google/nomulus synced 2026-07-20 06:52:27 +00:00

Compare commits

...

26 Commits

Author SHA1 Message Date
sarahcaseybot 74f0a8dd7b Add nomulus tool command for FeatureFlags (#2480)
* Add registryTool commands for FeatureFlags

* Fix merge conflicts

* Add required parameters and inject mapper

* Use optionals in cache to negative cahe missing objects

* Fix spelling

* Change back to bulk load in cache

* Add FeatureName enum

* Change variable name

* Use FeatureName in main parameter
2024-07-09 20:05:15 +00:00
gbrodman 092e3dca47 Add a renewal cost for ATs when renewal is SPECIFIED (#2484)
Note: this is not used yet
2024-07-09 18:39:48 +00:00
gbrodman b8a6ac72dd Add a reg-lock verification action to the new console (#2467)
The front end will have a (hidden) page that passes the verification
code to this API endpoint and displays the result.
2024-07-08 21:25:22 +00:00
Ben McIlwain b602aac09a Make all contacts nullable in the data model (#2490)
This doesn't yet allow them to be absent in EPP flows, but it should make the
code not break if they happen to be null in the database. This is a follow-up to
PR #2477, which ends up being a bit easier because whereas the registrant is
used in more parts of the codebase, the other contact types (admin, technical,
billing) are really only used in RDE, WHOIS, and RDAP, and because they were
already being used as a collection anyway, the handling for if that collection
contains fewer elements or is empty happened to already be mostly correct.
2024-07-03 21:42:20 +00:00
Lai Jiang d86c002132 Create Users when setting up OT&E and Production registrars (#2488) 2024-07-03 18:31:07 +00:00
gbrodman 54c5a9450d Add RegistrationBehavior.NONPREMIUM_CREATE (#2481)
When using this token (which must be tied to a particular domain), the
first year price (and only the first year price, i.e. the creation
price) will be the standard price for this TLD. Future years (i.e.
renewals) will continue at the normal premium price.
2024-06-26 20:01:32 +00:00
gbrodman 0f0097c15c Wait to load domain list until a registrar is selected (#2485)
This isn't the worst thing in the world but it does result in a bad
request to the server otherwise, and log/error spam. So, only load the
domains list if we have a registrar selected.
2024-06-25 18:39:53 +00:00
Ben McIlwain c9437d8c72 Make registrant nullable on domains (#2477)
This is the first step in migrating to the minimum registration data set. Note
that our database model already permits null domain registrants, so this just
makes the code accept it as well. Note that I haven't changed any requirements
in EPP flows yet; a later step will be to check the migration schedule and then
not require the registrant to be present if in a suitable state.

This does potentially affect the output of WHOIS/RDAP, but that's a NOOP so long
as EPP commands and other tools continue to enforce the requirement of a
registrant.
2024-06-20 15:22:38 +00:00
Weimin Yu 19819444af Set upper bound of netty version (#2482)
A new alpha version is introduced and breaks our tests.
2024-06-17 19:43:36 +00:00
Pavlo Tkach 15df3aea44 Update Angular 17 -> 18 (#2479) 2024-06-14 23:09:34 +00:00
Weimin Yu d000a5dff8 Use replica db for non-mutating epp flows (#2478)
* Use replica db for non-mutating epp flows

* Add a test
2024-06-13 23:18:56 +00:00
sarahcaseybot 34694b4aef Add the FeatureFlag entity (#2464)
* Add FeatureFlag entity

* Add converter

* Add loading cache

* Add more tests

* Fix NPE in cache

* small fixes
2024-06-12 16:44:08 +00:00
dependabot[bot] 7ce7b23450 Bump braces (#2476)
Bumps the npm_and_yarn group with 1 update in the /console-webapp directory: [braces](https://github.com/micromatch/braces).


Updates `braces` from 3.0.2 to 3.0.3
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-11 19:19:16 +00:00
Lai Jiang a5d1469281 Upgrade to Gradle 8.8 (#2475) 2024-06-10 14:56:10 +00:00
Pavlo Tkach a90a85afae Fix domain page "not found" layout issue (#2474) 2024-06-08 11:36:23 +00:00
Weimin Yu 6e68876a14 Use replica for whois/rdap (#2470) 2024-06-07 17:03:55 +00:00
Lai Jiang 11231703d5 Upgrade to jakarta mail (#2473) 2024-06-07 15:28:36 +00:00
gbrodman b77a219e19 Move domain-list search+download outside of loading bar (#2457)
This means that they'll stick around even while we're loading domains
from the server.

https://b.corp.google.com/issues/343213150
2024-06-06 20:35:20 +00:00
Pavlo Tkach bd8e6354b5 Add new registrar screen to the console (#2469) 2024-06-07 00:21:53 +00:00
Weimin Yu 361094f537 BSA check in DomainCheckFlow should check TLD (#2472)
Should not block labels if the tld is not enrolled with BSA.
2024-06-06 19:30:36 +00:00
sarahcaseybot d53177e44c Add domain creates to the load testing client (#2458)
* Add domain creates to the load testing client

* Update contact create
2024-06-06 17:30:12 +00:00
sarahcaseybot e73f646e1f Add FeatureFlag table to the database (#2463)
* Add FeatureFlag table to the database

* Change status to hstore
2024-06-06 17:17:11 +00:00
Lai Jiang 1a5dfb0ac2 Upgrade schemacrawler (#2471) 2024-06-06 14:51:13 +00:00
Lai Jiang 49cb1875d1 Upgrade dependencies (#2468) 2024-06-05 15:50:42 +00:00
gbrodman 61eee45ad0 Add renewalPrice fields to AllocationToken in SQL (#2462)
This is an optional field (will be required when the renewal price
behavior is SPECIFIED). This will allow us to set arbitrary renewal
prices for domains as part of one-off negotiations.

https://b.corp.google.com/issues/332928676
2024-06-03 19:50:58 +00:00
Weimin Yu e99a18f54f Pass log trace_id to TimeLimiter task (#2466)
Code executed by TimeLimiter is in another thread. Pass on the log
trace_id if exists.
2024-06-03 19:38:17 +00:00
230 changed files with 12235 additions and 7804 deletions
+1 -1
View File
@@ -24,7 +24,7 @@ com.google.guava:failureaccess:1.0.2=compileClasspath,deploy_jar,runtimeClasspat
com.google.guava:guava-parent:32.1.1-jre=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.guava:guava:31.0.1-jre=checkstyle
com.google.guava:guava:32.1.1-jre=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.guava:guava:33.2.0-jre=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.google.guava:guava:33.2.1-jre=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=checkstyle,compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.google.inject:guice:5.1.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.j2objc:j2objc-annotations:1.3=checkstyle
+10 -7
View File
@@ -15,12 +15,16 @@
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/console-webapp",
"outputPath": {
"base": "staged/dist/",
"browser": ""
},
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"polyfills": [
"src/polyfills.ts"
],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
@@ -34,7 +38,8 @@
"stylePreprocessorOptions": {
"includePaths": ["node_modules/"]
},
"scripts": []
"scripts": [],
"browser": "src/main.ts"
},
"configurations": {
"production": {
@@ -59,9 +64,7 @@
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
+2280 -1631
View File
File diff suppressed because it is too large Load Diff
+21 -21
View File
@@ -4,7 +4,7 @@
"scripts": {
"ng": "ng",
"start": "ng serve --proxy-config dev-proxy.config.json",
"build": "ng build --base-href=/console/ --output-path=staged/dist",
"build": "ng build --base-href=/console/",
"build:local": "ng build --base-href=/default/console/",
"watch": "ng build --watch --configuration development",
"test": "ng test --browsers=ChromeHeadless --watch=false",
@@ -16,29 +16,29 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^17.3.5",
"@angular/cdk": "^17.3.5",
"@angular/common": "^17.3.5",
"@angular/compiler": "^17.3.5",
"@angular/core": "^17.3.5",
"@angular/forms": "^17.3.5",
"@angular/material": "^17.3.5",
"@angular/platform-browser": "^17.3.5",
"@angular/platform-browser-dynamic": "^17.3.5",
"@angular/router": "^17.3.5",
"@angular/animations": "^18.0.2",
"@angular/cdk": "^18.0.2",
"@angular/common": "^18.0.2",
"@angular/compiler": "^18.0.2",
"@angular/core": "^18.0.2",
"@angular/forms": "^18.0.2",
"@angular/material": "^18.0.2",
"@angular/platform-browser": "^18.0.2",
"@angular/platform-browser-dynamic": "^18.0.2",
"@angular/router": "^18.0.2",
"rxjs": "~7.5.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.3.5",
"@angular-eslint/builder": "17.3.0",
"@angular-eslint/eslint-plugin": "17.3.0",
"@angular-eslint/eslint-plugin-template": "17.3.0",
"@angular-eslint/schematics": "17.3.0",
"@angular-eslint/template-parser": "17.3.0",
"@angular/cli": "~17.3.5",
"@angular/compiler-cli": "^17.3.5",
"@angular-devkit/build-angular": "^18.0.3",
"@angular-eslint/builder": "18.0.1",
"@angular-eslint/eslint-plugin": "18.0.1",
"@angular-eslint/eslint-plugin-template": "18.0.1",
"@angular-eslint/schematics": "18.0.1",
"@angular-eslint/template-parser": "18.0.1",
"@angular/cli": "~18.0.3",
"@angular/compiler-cli": "^18.0.2",
"@types/jasmine": "~4.0.0",
"@types/node": "^18.11.18",
"@typescript-eslint/eslint-plugin": "^7.2.0",
@@ -52,6 +52,6 @@
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.0.0",
"prettier": "2.8.7",
"typescript": "~5.2.2"
"typescript": "~5.4.5"
}
}
}
+9 -9
View File
@@ -12,25 +12,25 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MaterialModule } from './material.module';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { BackendService } from './shared/services/backend.service';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
RouterTestingModule,
MaterialModule,
BrowserAnimationsModule,
],
providers: [BackendService],
declarations: [AppComponent],
imports: [RouterTestingModule, MaterialModule, BrowserAnimationsModule],
providers: [
BackendService,
provideHttpClient(),
provideHttpClientTesting(),
],
}).compileComponents();
});
+5 -3
View File
@@ -23,13 +23,14 @@ import { MaterialModule } from './material.module';
import { BackendService } from './shared/services/backend.service';
import { HttpClientModule } from '@angular/common/http';
import { provideHttpClient } from '@angular/common/http';
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
import { BillingInfoComponent } from './billingInfo/billingInfo.component';
import { DomainListComponent } from './domains/domainList.component';
import { HeaderComponent } from './header/header.component';
import { HomeComponent } from './home/home.component';
import { NavigationComponent } from './navigation/navigation.component';
import NewRegistrarComponent from './registrar/newRegistrar.component';
import { RegistrarDetailsComponent } from './registrar/registrarDetails.component';
import { RegistrarSelectorComponent } from './registrar/registrarSelector.component';
import { RegistrarComponent } from './registrar/registrarsTable.component';
@@ -63,6 +64,7 @@ import { TldsComponent } from './tlds/tlds.component';
HomeComponent,
LocationBackDirective,
NavigationComponent,
NewRegistrarComponent,
NotificationsComponent,
RegistrarComponent,
RegistrarDetailsComponent,
@@ -78,12 +80,12 @@ import { TldsComponent } from './tlds/tlds.component';
WhoisComponent,
WhoisEditComponent,
],
bootstrap: [AppComponent],
imports: [
AppRoutingModule,
BrowserAnimationsModule,
BrowserModule,
FormsModule,
HttpClientModule,
MaterialModule,
SnackBarModule,
],
@@ -98,7 +100,7 @@ import { TldsComponent } from './tlds/tlds.component';
subscriptSizing: 'dynamic',
},
},
provideHttpClient(),
],
bootstrap: [AppComponent],
})
export class AppModule {}
@@ -1,7 +1,8 @@
<app-selected-registrar-wrapper>
<div class="console-domains">
<div class="console-app-domains">
<h1 class="mat-headline-4">Domains</h1>
@if (totalResults === 0) {
@if (!isLoading && totalResults == 0) {
<div class="console-app__empty-domains">
<h1>
<mat-icon class="console-app__empty-domains-icon secondary-text"
@@ -10,76 +11,85 @@
</h1>
<h1>No domains found</h1>
</div>
} @else if(isLoading) {
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
} @else {
<a
mat-stroked-button
color="primary"
href="/console-api/dum-download?registrarId={{
registrarService.registrarId()
}}"
class="console-app-domains__download"
>
<mat-icon>download</mat-icon>
Download domains (.csv)
</a>
<mat-form-field class="console-app__domains-filter">
<mat-label>Filter</mat-label>
<input
type="search"
matInput
[(ngModel)]="searchTerm"
(ngModelChange)="sendInput()"
#input
/>
</mat-form-field>
<mat-table
[dataSource]="dataSource"
class="mat-elevation-z0"
class="console-app__domains-table"
>
<ng-container matColumnDef="domainName">
<mat-header-cell *matHeaderCellDef>Domain Name</mat-header-cell>
<mat-cell *matCellDef="let element">{{ element.domainName }}</mat-cell>
</ng-container>
<div class="console-app__domains-table-parent">
@if (isLoading) {
<div class="console-app__domains-spinner">
<mat-spinner />
</div>
} @else {
<a
mat-stroked-button
color="primary"
href="/console-api/dum-download?registrarId={{
registrarService.registrarId()
}}"
class="console-app-domains__download"
>
<mat-icon>download</mat-icon>
Download domains (.csv)
</a>
<mat-form-field class="console-app__domains-filter">
<mat-label>Filter</mat-label>
<input
type="search"
matInput
[(ngModel)]="searchTerm"
(ngModelChange)="sendInput()"
[disabled]="isLoading"
#input
/>
</mat-form-field>
}
<mat-table
[dataSource]="dataSource"
class="mat-elevation-z0"
class="console-app__domains-table"
>
<ng-container matColumnDef="domainName">
<mat-header-cell *matHeaderCellDef>Domain Name</mat-header-cell>
<mat-cell *matCellDef="let element">{{
element.domainName
}}</mat-cell>
</ng-container>
<ng-container matColumnDef="creationTime">
<mat-header-cell *matHeaderCellDef>Creation Time</mat-header-cell>
<mat-cell *matCellDef="let element">
{{ element.creationTime.creationTime }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="creationTime">
<mat-header-cell *matHeaderCellDef>Creation Time</mat-header-cell>
<mat-cell *matCellDef="let element">
{{ element.creationTime.creationTime }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="registrationExpirationTime">
<mat-header-cell *matHeaderCellDef>Expiration Time</mat-header-cell>
<mat-cell *matCellDef="let element">
{{ element.registrationExpirationTime }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="registrationExpirationTime">
<mat-header-cell *matHeaderCellDef>Expiration Time</mat-header-cell>
<mat-cell *matCellDef="let element">
{{ element.registrationExpirationTime }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="statuses">
<mat-header-cell *matHeaderCellDef>Statuses</mat-header-cell>
<mat-cell *matCellDef="let element">{{ element.statuses }}</mat-cell>
</ng-container>
<ng-container matColumnDef="statuses">
<mat-header-cell *matHeaderCellDef>Statuses</mat-header-cell>
<mat-cell *matCellDef="let element">{{ element.statuses }}</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
<!-- Row shown when there is no matching data. -->
<mat-row *matNoDataRow>
<mat-cell colspan="4">No domains found</mat-cell>
</mat-row>
</mat-table>
<mat-paginator
[length]="totalResults"
[pageIndex]="pageNumber"
[pageSize]="resultsPerPage"
[pageSizeOptions]="[10, 25, 50, 100, 500]"
(page)="onPageChange($event)"
aria-label="Select page of domain results"
showFirstLastButtons
></mat-paginator>
<!-- Row shown when there is no matching data. -->
<mat-row *matNoDataRow>
<mat-cell colspan="4">No domains found</mat-cell>
</mat-row>
</mat-table>
<mat-paginator
[length]="totalResults"
[pageIndex]="pageNumber"
[pageSize]="resultsPerPage"
[pageSizeOptions]="[10, 25, 50, 100, 500]"
(page)="onPageChange($event)"
aria-label="Select page of domain results"
showFirstLastButtons
></mat-paginator>
</div>
}
</div>
</app-selected-registrar-wrapper>
@@ -12,13 +12,10 @@
}
}
&-domains {
position: relative;
&__download {
position: absolute;
top: 0;
right: 0;
}
&-domains__download {
position: absolute;
top: -55px;
right: 0;
}
&__domains {
@@ -31,10 +28,26 @@
width: 100%;
}
&__domains-table-parent {
position: relative;
width: fit-content;
min-width: 100%;
}
&__domains-table {
min-width: $min-width !important;
}
&__domains-spinner {
align-items: center;
display: flex;
justify-content: center;
position: absolute;
height: 100%;
width: 100%;
background: rgba(255, 255, 255, 0.6);
}
.mat-mdc-paginator {
min-width: $min-width !important;
}
@@ -14,11 +14,12 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DomainListComponent } from './domainList.component';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MaterialModule } from '../material.module';
import { BackendService } from '../shared/services/backend.service';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { DomainListComponent } from './domainList.component';
describe('DomainListComponent', () => {
let component: DomainListComponent;
@@ -27,12 +28,12 @@ describe('DomainListComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DomainListComponent],
imports: [
HttpClientTestingModule,
MaterialModule,
BrowserAnimationsModule,
imports: [MaterialModule, BrowserAnimationsModule],
providers: [
BackendService,
provideHttpClient(),
provideHttpClientTesting(),
],
providers: [BackendService],
}).compileComponents();
fixture = TestBed.createComponent(DomainListComponent);
@@ -58,9 +58,9 @@ export class DomainListComponent {
private _snackBar: MatSnackBar
) {
effect(() => {
this.pageNumber = 0;
this.totalResults = 0;
if (this.registrarService.registrarId()) {
this.pageNumber = 0;
this.totalResults = 0;
this.reloadData();
}
});
@@ -0,0 +1,182 @@
<div class="console-new-registrar">
<button
mat-icon-button
aria-label="Back to registrars list"
(click)="goBack()"
>
<mat-icon>arrow_back</mat-icon>
</button>
<div class="spacer"></div>
<h1>Create a registrar</h1>
<form (ngSubmit)="save($event)" #form>
<h2>General</h2>
<section>
<mat-form-field appearance="outline">
<mat-label>Registrar Name: </mat-label>
<input
matInput
[required]="true"
[(ngModel)]="newRegistrar.registrarName"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</section>
<section>
<mat-form-field appearance="outline">
<mat-label>Registrar ID: </mat-label>
<input
matInput
[required]="true"
[(ngModel)]="newRegistrar.registrarId"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</section>
<section>
<mat-form-field appearance="outline">
<mat-label>Registrar email address: </mat-label>
<input
matInput
type="email"
[required]="true"
[(ngModel)]="newRegistrar.emailAddress"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</section>
<section>
<mat-form-field appearance="outline">
<mat-label>Billing Accounts: </mat-label>
<textarea
matInput
required="true"
placeholder="USD=billing-id-for-usd
JPY=billing-id-for-yen"
[ngModel]="billingAccountMap"
(ngModelChange)="onBillingAccountMapChange($event)"
[ngModelOptions]="{ standalone: true }"
></textarea>
</mat-form-field>
</section>
<section>
<mat-form-field appearance="outline">
<mat-label>IANA ID: </mat-label>
<input
matInput
[required]="true"
[(ngModel)]="newRegistrar.ianaIdentifier"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</section>
<section>
<mat-form-field appearance="outline">
<mat-label>ICANN referral email: </mat-label>
<input
matInput
[required]="true"
type="email"
[(ngModel)]="newRegistrar.icannReferralEmail"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</section>
<section>
<mat-form-field appearance="outline">
<mat-label>Drive ID: </mat-label>
<input
matInput
[required]="true"
[(ngModel)]="newRegistrar.driveFolderId"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</section>
<h2>Contact Info</h2>
<section>
<mat-form-field appearance="outline">
<mat-label>Street address (Line 1): </mat-label>
<input
matInput
[required]="true"
[(ngModel)]="localizedAddressStreet.line1"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</section>
<section>
<mat-form-field appearance="outline">
<mat-label>Street address (Line 2)</mat-label>
<input
matInput
[required]="false"
[(ngModel)]="localizedAddressStreet.line2"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</section>
<section>
<mat-form-field appearance="outline">
<mat-label>Street address (Line 3)</mat-label>
<input
matInput
[required]="false"
[(ngModel)]="localizedAddressStreet.line3"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</section>
<section>
<mat-form-field appearance="outline">
<mat-label>City: </mat-label>
<input
matInput
[required]="true"
[(ngModel)]="newRegistrar.localizedAddress.city"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</section>
<section>
<mat-form-field appearance="outline">
<mat-label>State/Region: </mat-label>
<input
matInput
[required]="false"
[(ngModel)]="newRegistrar.localizedAddress.state"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</section>
<section>
<mat-form-field appearance="outline">
<mat-label>ZIP/Postal Code: </mat-label>
<input
matInput
[required]="true"
[(ngModel)]="newRegistrar.localizedAddress.zip"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</section>
<section>
<mat-form-field appearance="outline">
<mat-label>Country Code (e.g. US): </mat-label>
<input
matInput
[required]="true"
[(ngModel)]="newRegistrar.localizedAddress.countryCode"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</section>
<button
class="console-new-registrar__submit"
mat-flat-button
color="primary"
type="submit"
>
Save
</button>
</form>
</div>
@@ -0,0 +1,20 @@
.console-new-registrar {
max-width: 616px;
h2 {
margin: 40px 0 25px 0 !important;
}
section {
margin-bottom: 20px;
}
mat-form-field {
display: block;
width: 100%;
}
&__submit {
margin: 30px 0;
}
}
@@ -0,0 +1,99 @@
// 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.
import { HttpErrorResponse } from '@angular/common/http';
import {
Component,
ElementRef,
ViewChild,
ViewEncapsulation,
} from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Registrar, RegistrarService } from './registrar.service';
interface LocalizedAddressStreet {
line1: string;
line2: string;
line3: string;
}
@Component({
selector: 'app-new-registrar',
templateUrl: './newRegistrar.component.html',
styleUrls: ['./newRegistrar.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export default class NewRegistrarComponent {
protected newRegistrar: Registrar;
protected localizedAddressStreet: LocalizedAddressStreet;
protected billingAccountMap: String = '';
@ViewChild('form') form!: ElementRef;
constructor(
private registrarService: RegistrarService,
private _snackBar: MatSnackBar
) {
this.newRegistrar = {
registrarId: '',
url: '',
whoisServer: '',
registrarName: '',
icannReferralEmail: '',
localizedAddress: {
city: '',
state: '',
zip: '',
countryCode: '',
},
};
this.localizedAddressStreet = {
line1: '',
line2: '',
line3: '',
};
}
onBillingAccountMapChange(val: String) {
const billingAccountMap: { [key: string]: string } = {};
this.newRegistrar.billingAccountMap = val.split('\n').reduce((acc, val) => {
const [currency, billingCode] = val.split('=');
acc[currency] = billingCode;
return acc;
}, billingAccountMap);
}
save(e: SubmitEvent) {
e.preventDefault();
if (this.form.nativeElement.checkValidity()) {
const { line1, line2, line3 } = this.localizedAddressStreet;
this.newRegistrar.localizedAddress.street = [line1, line2, line3].filter(
(v) => !!v
);
this.registrarService.createRegistrar(this.newRegistrar).subscribe({
complete: () => {
this.goBack();
},
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error);
},
});
} else {
this.form.nativeElement.reportValidity();
}
}
goBack() {
this.registrarService.inNewRegistrarMode.set(false);
}
}
@@ -14,18 +14,24 @@
import { TestBed } from '@angular/core/testing';
import { RegistrarService } from './registrar.service';
import { BackendService } from '../shared/services/backend.service';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { MatSnackBar } from '@angular/material/snack-bar';
import { BackendService } from '../shared/services/backend.service';
import { RegistrarService } from './registrar.service';
describe('RegistrarService', () => {
let service: RegistrarService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [BackendService, MatSnackBar],
imports: [],
providers: [
BackendService,
MatSnackBar,
provideHttpClient(),
provideHttpClientTesting(),
],
});
service = TestBed.inject(RegistrarService);
});
@@ -13,7 +13,7 @@
// limitations under the License.
import { Injectable, computed, signal } from '@angular/core';
import { Observable, tap } from 'rxjs';
import { Observable, switchMap, tap } from 'rxjs';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router';
@@ -85,14 +85,21 @@ export class RegistrarService implements GlobalLoader {
this.registrars().find((r) => r.registrarId === this.registrarId())
);
inNewRegistrarMode = signal(false);
registrarsLoaded: Promise<void>;
constructor(
private backend: BackendService,
private globalLoader: GlobalLoaderService,
private _snackBar: MatSnackBar,
private router: Router
) {
this.loadRegistrars().subscribe((r) => {
this.globalLoader.stopGlobalLoader(this);
this.registrarsLoaded = new Promise((resolve) => {
this.loadRegistrars().subscribe((r) => {
this.globalLoader.stopGlobalLoader(this);
resolve();
});
});
this.globalLoader.startGlobalLoader(this);
}
@@ -118,19 +125,23 @@ export class RegistrarService implements GlobalLoader {
);
}
saveRegistrar(registrar: Registrar) {
return this.backend.postRegistrar(registrar).pipe(
tap((registrar) => {
if (registrar) {
this.registrars.set(
this.registrars().map((r) => {
if (r.registrarId === registrar.registrarId) {
return registrar;
}
return r;
})
);
}
createRegistrar(registrar: Registrar) {
return this.backend
.createRegistrar(registrar)
.pipe(switchMap((_) => this.loadRegistrars()));
}
updateRegistrar(updatedRegistrar: Registrar) {
return this.backend.updateRegistrar(updatedRegistrar).pipe(
tap(() => {
this.registrars.set(
this.registrars().map((r) => {
if (r.registrarId === updatedRegistrar.registrarId) {
return updatedRegistrar;
}
return r;
})
);
})
);
}
@@ -42,28 +42,32 @@ export class RegistrarDetailsComponent implements OnInit {
) {}
ngOnInit(): void {
this.subscription = this.route.paramMap.subscribe((params: ParamMap) => {
this.registrarInEdit = structuredClone(
this.registrarService
.registrars()
.filter((r) => r.registrarId === params.get('id'))[0]
);
if (!this.registrarInEdit) {
this._snackBar.open(
`Registrar with id ${params.get('id')} is not available`
this.registrarService.registrarsLoaded.then(() => {
this.subscription = this.route.paramMap.subscribe((params: ParamMap) => {
this.registrarInEdit = structuredClone(
this.registrarService
.registrars()
.filter((r) => r.registrarId === params.get('id'))[0]
);
this.registrarNotFound = true;
} else {
this.registrarNotFound = false;
}
if (!this.registrarInEdit) {
this._snackBar.open(
`Registrar with id ${params.get('id')} is not available`
);
this.registrarNotFound = true;
} else {
this.registrarNotFound = false;
}
});
});
}
addTLD(e: MatChipInputEvent) {
this.registrarInEdit.allowedTlds = this.registrarInEdit.allowedTlds || [];
this.removeTLD(e.value); // Prevent dups
this.registrarInEdit.allowedTlds = this.registrarInEdit.allowedTlds?.concat(
[e.value.toLowerCase()]
);
this.registrarInEdit.allowedTlds = [
...this.registrarInEdit.allowedTlds,
e.value.toLowerCase(),
];
}
removeTLD(tld: string) {
@@ -73,7 +77,7 @@ export class RegistrarDetailsComponent implements OnInit {
}
saveAndClose() {
this.registrarService.saveRegistrar(this.registrarInEdit).subscribe({
this.registrarService.updateRegistrar(this.registrarInEdit).subscribe({
complete: () => {
this.router.navigate([RegistrarComponent.PATH], {
queryParamsHandling: 'merge',
@@ -1,9 +1,5 @@
<div class="console-app__registrar">
<mat-form-field
class="example-full-width"
class="mat-form-field-density-5"
appearance="outline"
>
<mat-form-field class="example-full-width" appearance="outline">
<mat-label>Registrar</mat-label>
<input
type="text"
@@ -14,11 +14,12 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RegistrarSelectorComponent } from './registrarSelector.component';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MaterialModule } from '../material.module';
import { BackendService } from '../shared/services/backend.service';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RegistrarSelectorComponent } from './registrarSelector.component';
describe('RegistrarSelectorComponent', () => {
let component: RegistrarSelectorComponent;
@@ -26,13 +27,13 @@ describe('RegistrarSelectorComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
MaterialModule,
BrowserAnimationsModule,
],
providers: [BackendService],
declarations: [RegistrarSelectorComponent],
imports: [MaterialModule, BrowserAnimationsModule],
providers: [
BackendService,
provideHttpClient(),
provideHttpClientTesting(),
],
}).compileComponents();
fixture = TestBed.createComponent(RegistrarSelectorComponent);
@@ -1,5 +1,19 @@
@if(registrarService.inNewRegistrarMode()) {
<app-new-registrar />
} @else {
<div class="console-app__registrars">
<h1 class="mat-headline-4">Registrars</h1>
<div class="console-app__registrars-header">
<h1 class="mat-headline-4">Registrars</h1>
<button
mat-flat-button
color="primary"
(click)="openNewRegistrar()"
aria-label="Add new registrar"
>
<mat-icon>add</mat-icon>
Add new registrar
</button>
</div>
<mat-form-field class="console-app__registrars-filter">
<mat-label>Search</mat-label>
<input
@@ -36,3 +50,5 @@
showFirstLastButtons
></mat-paginator>
</div>
}
@@ -15,6 +15,11 @@
min-width: $min-width !important;
}
&__registrars-header {
display: flex;
justify-content: space-between;
}
.mat-mdc-paginator {
min-width: $min-width !important;
}
@@ -14,12 +14,13 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RegistrarComponent } from './registrarsTable.component';
import { BackendService } from '../shared/services/backend.service';
import { ActivatedRoute } from '@angular/router';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ActivatedRoute } from '@angular/router';
import { MaterialModule } from '../material.module';
import { BackendService } from '../shared/services/backend.service';
import { RegistrarComponent } from './registrarsTable.component';
describe('RegistrarComponent', () => {
let component: RegistrarComponent;
@@ -28,14 +29,12 @@ describe('RegistrarComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [RegistrarComponent],
imports: [
HttpClientTestingModule,
MaterialModule,
BrowserAnimationsModule,
],
imports: [MaterialModule, BrowserAnimationsModule],
providers: [
BackendService,
{ provide: ActivatedRoute, useValue: {} as ActivatedRoute },
provideHttpClient(),
provideHttpClientTesting(),
],
}).compileComponents();
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, ViewChild, ViewEncapsulation } from '@angular/core';
import { Component, effect, ViewChild, ViewEncapsulation } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
@@ -93,6 +93,9 @@ export class RegistrarComponent {
this.dataSource = new MatTableDataSource<Registrar>(
registrarService.registrars()
);
effect(() => {
this.dataSource.data = registrarService.registrars();
});
}
ngAfterViewInit() {
@@ -111,4 +114,8 @@ export class RegistrarComponent {
// TODO: consider filteing out only by registrar name
this.dataSource.filter = filterValue.trim().toLowerCase();
}
openNewRegistrar() {
this.registrarService.inNewRegistrarMode.set(true);
}
}
@@ -14,10 +14,11 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import ContactComponent from './contact.component';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { MaterialModule } from 'src/app/material.module';
import { BackendService } from 'src/app/shared/services/backend.service';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import ContactComponent from './contact.component';
describe('ContactComponent', () => {
let component: ContactComponent;
@@ -26,8 +27,12 @@ describe('ContactComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ContactComponent],
imports: [HttpClientTestingModule, MaterialModule],
providers: [BackendService],
imports: [MaterialModule],
providers: [
BackendService,
provideHttpClient(),
provideHttpClientTesting(),
],
}).compileComponents();
fixture = TestBed.createComponent(ContactComponent);
component = fixture.componentInstance;
@@ -26,6 +26,6 @@
mat-form-field {
display: block;
width: 100%;
margin-bottom: 30px;
margin-bottom: 20px;
}
}
@@ -14,18 +14,19 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import SecurityComponent from './security.component';
import { SecurityService, apiToUiConverter } from './security.service';
import { BackendService } from 'src/app/shared/services/backend.service';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { MaterialModule } from 'src/app/material.module';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { FormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { of } from 'rxjs';
import { FormsModule } from '@angular/forms';
import { MaterialModule } from 'src/app/material.module';
import {
Registrar,
RegistrarService,
} from 'src/app/registrar/registrar.service';
import { BackendService } from 'src/app/shared/services/backend.service';
import SecurityComponent from './security.component';
import { SecurityService, apiToUiConverter } from './security.service';
describe('SecurityComponent', () => {
let component: SecurityComponent;
@@ -50,16 +51,13 @@ describe('SecurityComponent', () => {
} as RegistrarService;
await TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
MaterialModule,
BrowserAnimationsModule,
FormsModule,
],
declarations: [SecurityComponent],
imports: [MaterialModule, BrowserAnimationsModule, FormsModule],
providers: [
BackendService,
{ provide: RegistrarService, useValue: dummyRegistrarService },
provideHttpClient(),
provideHttpClientTesting(),
],
})
.overrideComponent(SecurityComponent, {
@@ -14,6 +14,11 @@
import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { MatSnackBar } from '@angular/material/snack-bar';
import { BackendService } from 'src/app/shared/services/backend.service';
import SecurityComponent from './security.component';
import {
SecurityService,
SecuritySettings,
@@ -21,10 +26,6 @@ import {
apiToUiConverter,
uiToApiConverter,
} from './security.service';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import SecurityComponent from './security.component';
import { BackendService } from 'src/app/shared/services/backend.service';
import { MatSnackBar } from '@angular/material/snack-bar';
describe('SecurityService', () => {
const uiMockData: SecuritySettings = {
@@ -42,9 +43,15 @@ describe('SecurityService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
declarations: [SecurityComponent],
providers: [MatSnackBar, SecurityService, BackendService],
imports: [],
providers: [
MatSnackBar,
SecurityService,
BackendService,
provideHttpClient(),
provideHttpClientTesting(),
],
});
service = TestBed.inject(SecurityService);
});
@@ -29,7 +29,6 @@
>Security</a
>
</nav>
<mat-divider></mat-divider>
<mat-tab-nav-panel #tabPanel>
<router-outlet></router-outlet>
</mat-tab-nav-panel>
@@ -13,9 +13,6 @@
// limitations under the License.
.console-settings {
> mat-divider {
margin-bottom: 40px;
}
.mdc-tab {
&.active-link {
border-bottom: 2px solid var(--primary);
@@ -24,4 +21,7 @@
}
}
}
nav {
margin-bottom: 40px;
}
}
@@ -5,7 +5,7 @@
display: flex;
align-items: center;
gap: 1rem;
margin: 20px 0;
margin-bottom: 20px;
button {
flex-shrink: 0;
}
@@ -14,12 +14,13 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import WhoisComponent from './whois.component';
import { MaterialModule } from 'src/app/material.module';
import { BackendService } from 'src/app/shared/services/backend.service';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { RegistrarService } from 'src/app/registrar/registrar.service';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
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';
describe('WhoisComponent', () => {
let component: WhoisComponent;
@@ -28,14 +29,12 @@ describe('WhoisComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [WhoisComponent],
imports: [
HttpClientTestingModule,
MaterialModule,
BrowserAnimationsModule,
],
imports: [MaterialModule, BrowserAnimationsModule],
providers: [
BackendService,
{ provide: RegistrarService, useValue: { registrar: {} } },
provideHttpClient(),
provideHttpClientTesting(),
],
}).compileComponents();
@@ -110,7 +110,13 @@ export class BackendService {
.pipe(catchError((err) => this.errorCatcher<Registrar[]>(err)));
}
postRegistrar(registrar: Registrar): Observable<Registrar> {
createRegistrar(registrar: Registrar): Observable<Registrar> {
return this.http
.post<Registrar>('/console-api/registrars', registrar)
.pipe(catchError((err) => this.errorCatcher<Registrar>(err)));
}
updateRegistrar(registrar: Registrar): Observable<Registrar> {
return this.http
.post<Registrar>('/console-api/registrar', registrar)
.pipe(catchError((err) => this.errorCatcher<Registrar>(err)));
+3 -3
View File
@@ -30,14 +30,14 @@ body {
.console-app {
.mat-headline-4 {
margin-top: 6px;
margin-bottom: 24px;
margin-top: 6px !important;
margin-bottom: 24px !important;
}
.spacer {
flex: 1;
}
h1 {
margin-bottom: 24px;
margin-bottom: 24px !important;
}
a {
text-decoration: none;
+52 -85
View File
@@ -4,63 +4,19 @@
$secondary-color: #80868b;
$border-color: #dadce0;
$hue-undefined: "undefined";
$blue-palette: (
50: #e8f0fe,
100: #d2e3fc,
200: #aecbfa,
300: #8ab4f8,
400: #669df6,
500: #4285f4,
600: #1a73e8,
700: #1967d2,
800: #185abc,
900: #174ea6,
A100: $hue-undefined,
A200: $hue-undefined,
A400: $hue-undefined,
A700: $hue-undefined,
contrast: (
50: #174ea6,
100: #174ea6,
200: #174ea6,
300: #174ea6,
400: #174ea6,
500: white,
600: white,
700: white,
800: white,
900: white,
A100: $hue-undefined,
A200: $hue-undefined,
A400: $hue-undefined,
A700: $hue-undefined,
),
);
@function rem($valueInPixels, $rootbase: 16px) {
@return math.div($valueInPixels, $rootbase) * 1rem;
}
/** Copied from docs section **/
// Include the common styles for Angular Material. We include this here so that you only
// have to load a single css file for Angular Material in your app.
// Be sure that you only ever include this mixin once!
@include mat.core();
// The warn palette is optional (defaults to red).
$theme-warn: mat.define-palette(mat.$red-palette);
/**
** Application specific section - Global styles and mixins
**/
$theme-primary: mat.define-palette($blue-palette);
$typographyConfig: mat.define-typography-config(
$typographyConfig: mat.m2-define-typography-config(
$headline-1:
mat.define-typography-level(
mat.m2-define-typography-level(
rem(29px),
rem(36px),
500,
@@ -68,7 +24,7 @@ $typographyConfig: mat.define-typography-config(
normal
),
$headline-4:
mat.define-typography-level(
mat.m2-define-typography-level(
rem(28px),
rem(36px),
500,
@@ -76,7 +32,7 @@ $typographyConfig: mat.define-typography-config(
normal
),
$headline-5:
mat.define-typography-level(
mat.m2-define-typography-level(
rem(20px),
rem(28px),
400,
@@ -84,9 +40,15 @@ $typographyConfig: mat.define-typography-config(
normal
),
$headline-6:
mat.define-typography-level(rem(16px), rem(2px), 500, "Google Sans", normal),
mat.m2-define-typography-level(
rem(16px),
rem(2px),
500,
"Google Sans",
normal
),
$body-1:
mat.define-typography-level(
mat.m2-define-typography-level(
rem(16px),
rem(24px),
400,
@@ -94,7 +56,7 @@ $typographyConfig: mat.define-typography-config(
normal
),
$body-2:
mat.define-typography-level(
mat.m2-define-typography-level(
rem(14px),
rem(20px),
400,
@@ -102,7 +64,7 @@ $typographyConfig: mat.define-typography-config(
normal
),
$caption:
mat.define-typography-level(
mat.m2-define-typography-level(
rem(14px),
rem(24px),
400,
@@ -110,37 +72,13 @@ $typographyConfig: mat.define-typography-config(
0.15px
),
$overline:
mat.define-typography-level(rem(14px), rem(20px), 500, "Google Sans", 0.5px),
);
@include mat.typography-hierarchy($typographyConfig);
@mixin form-field-density($density) {
$field-typography: mat.define-typography-config(
$body-1: mat.define-typography-level(12px, 24px, 400),
);
@include mat.typography-level($field-typography, "body-1");
@include mat.form-field-density($density);
}
// Define lowest possible density class to be used in application
// In the same manner -1...-5 classes can be defined
.mat-form-field-density-5 {
@include form-field-density(-5);
}
/**
** Light theme
**/
$light-theme: mat.define-light-theme(
(
color: (
primary: $theme-primary,
accent: $theme-primary,
warn: $theme-warn,
mat.m2-define-typography-level(
rem(14px),
rem(20px),
500,
"Google Sans",
0.5px
),
density: 0,
typography: $typographyConfig,
)
);
// Access and define a class with secondary color exposed
@@ -165,12 +103,41 @@ mat-row:hover {
:root {
--text: #5f6368;
--primary: #{mat.get-color-from-palette($blue-palette, 500)};
--lightest: #{mat.get-color-from-palette($blue-palette, 100)};
--primary: #4285f4;
--lightest: #d2e3fc;
--light-highlight: #e8eaed;
--lightest-highlight: #f8f9fa;
--secondary: #{$secondary-color};
--border: #{$border-color};
--mat-tree-node-text-font: "Google Sans Text";
--mat-tree-node-text-size: 0.95rem;
--mat-sidenav-container-width: 280px;
}
@include mat.all-component-themes($light-theme);
$theme: mat.define-theme(
(
color: (
theme-type: light,
primary: mat.$blue-palette,
),
typography: (
plain-family: "Google Sans",
brand-family: "Google Sans Text",
bold-weight: 600,
),
)
);
html {
@include mat.all-component-themes($theme);
@include mat.typography-hierarchy($typographyConfig);
@include mat.form-field-theme(
mat.define-theme(
(
density: (
scale: -3,
),
)
)
);
}
+1 -1
View File
@@ -5,6 +5,7 @@
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
@@ -12,7 +13,6 @@
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
+2 -5
View File
@@ -135,14 +135,12 @@ dependencies {
implementation deps['com.google.api.grpc:proto-google-common-protos']
implementation deps['com.google.api.grpc:proto-google-cloud-secretmanager-v1']
implementation deps['com.google.api-client:google-api-client']
implementation deps['com.google.api-client:google-api-client-appengine']
implementation deps['com.google.api-client:google-api-client-servlet']
implementation deps['com.google.monitoring-client:metrics']
implementation deps['com.google.monitoring-client:stackdriver']
implementation deps['com.google.api-client:google-api-client-java6']
implementation deps['com.google.api.grpc:proto-google-cloud-tasks-v2']
implementation deps['com.google.apis:google-api-services-admin-directory']
implementation deps['com.google.apis:google-api-services-appengine']
implementation deps['com.google.apis:google-api-services-bigquery']
implementation deps['com.google.apis:google-api-services-dataflow']
implementation deps['com.google.apis:google-api-services-dns']
@@ -176,12 +174,10 @@ dependencies {
implementation deps['com.google.cloud:google-cloud-storage']
implementation deps['com.google.cloud:google-cloud-tasks']
implementation deps['com.google.http-client:google-http-client']
implementation deps['com.google.http-client:google-http-client-appengine']
implementation deps['com.google.http-client:google-http-client-jackson2']
implementation deps['com.google.oauth-client:google-oauth-client']
implementation deps['com.google.oauth-client:google-oauth-client-java6']
implementation deps['com.google.oauth-client:google-oauth-client-jetty']
implementation deps['com.google.oauth-client:google-oauth-client-appengine']
implementation deps['com.google.oauth-client:google-oauth-client-servlet']
implementation deps['com.google.protobuf:protobuf-java']
implementation deps['com.google.re2j:re2j']
@@ -194,8 +190,8 @@ dependencies {
testRuntimeOnly deps['guru.nidi:graphviz-java-all-j2v8']
testImplementation deps['io.github.classgraph:classgraph']
testRuntimeOnly deps['io.github.java-diff-utils:java-diff-utils']
implementation deps['javax.mail:mail']
implementation deps['jakarta.inject:jakarta.inject-api']
implementation deps['jakarta.mail:jakarta.mail-api']
implementation deps['javax.persistence:javax.persistence-api']
implementation deps['jakarta.servlet:jakarta.servlet-api']
implementation deps['javax.xml.bind:jaxb-api']
@@ -247,6 +243,7 @@ dependencies {
implementation deps['us.fatehi:schemacrawler-api']
implementation deps['us.fatehi:schemacrawler-diagram']
implementation deps['us.fatehi:schemacrawler-tools']
implementation deps['us.fatehi:schemacrawler-postgresql']
implementation deps['xerces:xmlParserAPIs']
implementation deps['org.ogce:xpp3']
// Known issue: nebula-lint misses inherited dependency.
+40 -39
View File
@@ -40,11 +40,10 @@ com.github.jnr:jnr-unixsocket:0.38.22=compileClasspath,deploy_jar,nonprodCompile
com.github.jnr:jnr-x86asm:1.0.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,errorprone,nonprodAnnotationProcessor,testAnnotationProcessor
com.google.android:annotations:4.1.1.4=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
com.google.api-client:google-api-client-appengine:2.6.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api-client:google-api-client-jackson2:2.0.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api-client:google-api-client-java6:2.1.4=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api-client:google-api-client-servlet:2.6.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api-client:google-api-client:1.35.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api-client:google-api-client:2.6.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:gapic-google-cloud-storage-v2:2.32.1-alpha=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath
com.google.api.grpc:gapic-google-cloud-storage-v2:2.39.0-alpha=testCompileClasspath,testRuntimeClasspath
com.google.api.grpc:grpc-google-cloud-bigquerystorage-v1:3.4.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
@@ -88,12 +87,11 @@ com.google.api:gax-grpc:2.48.1=compileClasspath,deploy_jar,nonprodCompileClasspa
com.google.api:gax-httpjson:2.48.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.api:gax:2.48.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-admin-directory:directory_v1-rev20240509-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-appengine:v1-rev20240415-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-bigquery:v2-rev20240229-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-bigquery:v2-rev20240423-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-cloudresourcemanager:v1-rev20240310-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-dataflow:v1b3-rev20240218-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-dns:v2beta1-rev99-1.25.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-drive:v2-rev393-1.25.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-dataflow:v1b3-rev20240430-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-dns:v1-rev20240419-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-drive:v3-rev20240521-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-gmail:v1-rev20240520-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-groupssettings:v1-rev20210624-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-healthcare:v1-rev20240130-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
@@ -112,7 +110,8 @@ com.google.auto.service:auto-service-annotations:1.1.1=annotationProcessor,compi
com.google.auto.service:auto-service:1.1.1=annotationProcessor
com.google.auto.value:auto-value-annotations:1.10.4=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.auto.value:auto-value-annotations:1.9=annotationProcessor,errorprone,nonprodAnnotationProcessor,testAnnotationProcessor
com.google.auto.value:auto-value:1.10.4=annotationProcessor,compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
com.google.auto.value:auto-value:1.10.4=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.auto.value:auto-value:1.11.0=annotationProcessor,testAnnotationProcessor
com.google.auto:auto-common:1.2.1=annotationProcessor,errorprone,nonprodAnnotationProcessor,testAnnotationProcessor
com.google.closure-stylesheets:closure-stylesheets:1.5.0=css
com.google.cloud.bigdataoss:gcsio:2.2.16=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
@@ -170,19 +169,19 @@ com.google.googlejavaformat:google-java-format:1.5=annotationProcessor,testAnnot
com.google.guava:failureaccess:1.0.1=checkstyle,errorprone,nonprodAnnotationProcessor,soy
com.google.guava:failureaccess:1.0.2=annotationProcessor,compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
com.google.guava:guava-parent:32.1.1-jre=errorprone,nonprodAnnotationProcessor
com.google.guava:guava-testlib:33.2.0-jre=testCompileClasspath,testRuntimeClasspath
com.google.guava:guava-testlib:33.2.1-jre=testCompileClasspath,testRuntimeClasspath
com.google.guava:guava:20.0=css
com.google.guava:guava:31.0.1-jre=checkstyle,soy
com.google.guava:guava:32.1.1-jre=errorprone,nonprodAnnotationProcessor
com.google.guava:guava:33.0.0-jre=annotationProcessor,testAnnotationProcessor
com.google.guava:guava:33.2.0-android=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath
com.google.guava:guava:33.2.0-jre=testCompileClasspath,testRuntimeClasspath
com.google.guava:guava:33.2.1-jre=testCompileClasspath,testRuntimeClasspath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,checkstyle,compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,soy,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
com.google.gwt:gwt-user:2.10.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.http-client:google-http-client-apache-v2:1.43.3=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath
com.google.http-client:google-http-client-apache-v2:1.44.1=testCompileClasspath,testRuntimeClasspath
com.google.http-client:google-http-client-appengine:1.44.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.http-client:google-http-client-gson:1.44.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.http-client:google-http-client-apache-v2:1.44.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.http-client:google-http-client-appengine:1.43.3=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath
com.google.http-client:google-http-client-appengine:1.44.1=testCompileClasspath,testRuntimeClasspath
com.google.http-client:google-http-client-gson:1.44.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.http-client:google-http-client-jackson2:1.44.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.http-client:google-http-client-protobuf:1.44.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.http-client:google-http-client:1.44.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
@@ -200,7 +199,6 @@ com.google.jsinterop:jsinterop-annotations:2.0.0=compileClasspath,deploy_jar,non
com.google.monitoring-client:contrib:1.0.7=testCompileClasspath,testRuntimeClasspath
com.google.monitoring-client:metrics:1.0.7=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.monitoring-client:stackdriver:1.0.7=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.oauth-client:google-oauth-client-appengine:1.36.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.oauth-client:google-oauth-client-java6:1.36.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.oauth-client:google-oauth-client-jetty:1.36.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.oauth-client:google-oauth-client-servlet:1.36.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
@@ -313,20 +311,19 @@ io.opentelemetry:opentelemetry-context:1.36.0=compileClasspath,deploy_jar,nonpro
io.outfoxx:swiftpoet:1.3.1=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.perfmark:perfmark-api:0.27.0=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
jakarta-regexp:jakarta-regexp:1.4=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.activation:jakarta.activation-api:2.1.3=jaxb
jakarta.activation:jakarta.activation-api:2.1.3=compileClasspath,deploy_jar,jaxb,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.inject:jakarta.inject-api:1.0.5=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.inject:jakarta.inject-api:2.0.1=soy
jakarta.mail:jakarta.mail-api:2.1.3=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.servlet:jakarta.servlet-api:6.0.0=testCompileClasspath,testRuntimeClasspath
jakarta.servlet:jakarta.servlet-api:6.1.0-M2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath
jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=jaxb
javacc:javacc:4.1=css
javax.activation:activation:1.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
javax.activation:javax.activation-api:1.2.0=compileClasspath,deploy_jar,jaxb,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
javax.annotation:javax.annotation-api:1.3.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
javax.annotation:jsr250-api:1.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,soy,testCompileClasspath,testRuntimeClasspath
javax.inject:javax.inject:1=annotationProcessor,compileClasspath,deploy_jar,errorprone,nonprodAnnotationProcessor,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
javax.jdo:jdo2-api:2.3-20090302111651=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
javax.mail:mail:1.5.0-b01=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
javax.persistence:javax.persistence-api:2.2=annotationProcessor,compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
javax.validation:validation-api:1.0.0.GA=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
javax.xml.bind:jaxb-api:2.3.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
@@ -398,20 +395,22 @@ org.checkerframework:checker-qual:3.42.0=compileClasspath,deploy_jar,nonprodComp
org.codehaus.mojo:animal-sniffer-annotations:1.23=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.conscrypt:conscrypt-openjdk-uber:2.5.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.easymock:easymock:3.0=css
org.eclipse.angus:angus-activation:2.0.2=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.eclipse.angus:jakarta.mail:2.0.3=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.eclipse.collections:eclipse-collections-api:11.1.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.collections:eclipse-collections:11.1.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.ee10:jetty-ee10-servlet:12.0.9=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.ee10:jetty-ee10-webapp:12.0.9=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-ee:12.0.9=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-http:12.0.9=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-io:12.0.9=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-security:12.0.9=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-server:12.0.9=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-session:12.0.9=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-util:12.0.9=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-xml:12.0.9=testCompileClasspath,testRuntimeClasspath
org.flywaydb:flyway-core:10.13.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.flywaydb:flyway-database-postgresql:10.13.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.ee10:jetty-ee10-servlet:12.0.10=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty.ee10:jetty-ee10-webapp:12.0.10=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-ee:12.0.10=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-http:12.0.10=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-io:12.0.10=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-security:12.0.10=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-server:12.0.10=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-session:12.0.10=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-util:12.0.10=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-xml:12.0.10=testCompileClasspath,testRuntimeClasspath
org.flywaydb:flyway-core:10.14.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.flywaydb:flyway-database-postgresql:10.14.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.glassfish.jaxb:jaxb-runtime:2.3.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.glassfish.jaxb:txw2:2.3.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.gwtproject:gwt-user:2.10.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
@@ -489,7 +488,7 @@ org.ow2.asm:asm:7.0=soy
org.ow2.asm:asm:9.2=compileClasspath,nonprodCompileClasspath
org.ow2.asm:asm:9.6=deploy_jar,jacocoAnt,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.pcollections:pcollections:3.1.4=annotationProcessor,errorprone,nonprodAnnotationProcessor,testAnnotationProcessor
org.postgresql:postgresql:42.7.3=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.postgresql:postgresql:42.7.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.reflections:reflections:0.10.2=checkstyle
org.rnorth.duct-tape:duct-tape:1.0.8=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.seleniumhq.selenium:selenium-api:3.141.59=testCompileClasspath,testRuntimeClasspath
@@ -504,9 +503,8 @@ org.seleniumhq.selenium:selenium-safari-driver:3.141.59=testCompileClasspath,tes
org.seleniumhq.selenium:selenium-support:3.141.59=testCompileClasspath,testRuntimeClasspath
org.slf4j:jcl-over-slf4j:1.7.32=testCompileClasspath,testRuntimeClasspath
org.slf4j:jul-to-slf4j:1.7.30=testRuntimeClasspath
org.slf4j:slf4j-api:2.0.13=compileClasspath,nonprodCompileClasspath,nonprodRuntimeClasspath,testCompileClasspath
org.slf4j:slf4j-api:2.1.0-alpha1=deploy_jar,runtimeClasspath,testRuntimeClasspath
org.slf4j:slf4j-jdk14:2.1.0-alpha1=deploy_jar,runtimeClasspath,testRuntimeClasspath
org.slf4j:slf4j-api:2.0.13=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.slf4j:slf4j-jdk14:2.0.12=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.snakeyaml:snakeyaml-engine:2.1=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.testcontainers:database-commons:1.19.8=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.testcontainers:jdbc:1.19.8=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
@@ -519,10 +517,13 @@ org.w3c.css:sac:1.3=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodR
org.webjars.npm:viz.js-graphviz-java:2.1.3=testRuntimeClasspath
org.xerial.snappy:snappy-java:1.1.10.4=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.yaml:snakeyaml:2.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-api:16.10.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-diagram:16.10.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-tools:16.10.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-utility:16.10.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler:16.10.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-api:16.21.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-diagram:16.21.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-loader:16.21.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-postgresql:16.21.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-text:16.21.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-tools:16.21.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-utility:16.21.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler:16.21.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
xerces:xmlParserAPIs:2.6.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
empty=devtool,nomulus_test
@@ -41,10 +41,10 @@ import google.registry.request.auth.Auth;
import google.registry.tools.DomainLockUtils;
import google.registry.util.DateTimeUtils;
import google.registry.util.EmailMessage;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
import java.util.Optional;
import javax.inject.Inject;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import org.joda.time.Duration;
/** Task that re-locks a previously-Registry-Locked domain after a predetermined period of time. */
@@ -38,10 +38,10 @@ import google.registry.request.Action;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.util.EmailMessage;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
import java.util.Optional;
import javax.inject.Inject;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.joda.time.format.DateTimeFormat;
@@ -466,13 +466,10 @@ public class RdePipeline implements Serializable {
// Contacts and hosts are only deposited in RDE, not BRDA.
if (pendingDeposit.mode() == RdeMode.FULL) {
HashSet<Serializable> contacts = new HashSet<>();
contacts.add(domain.getAdminContact().getKey());
contacts.add(domain.getTechContact().getKey());
contacts.add(domain.getRegistrant().getKey());
// Billing contact is not mandatory.
if (domain.getBillingContact() != null) {
contacts.add(domain.getBillingContact().getKey());
}
domain.getAdminContact().ifPresent(c -> contacts.add(c.getKey()));
domain.getTechContact().ifPresent(c -> contacts.add(c.getKey()));
domain.getRegistrant().ifPresent(c -> contacts.add(c.getKey()));
domain.getBillingContact().ifPresent(c -> contacts.add(c.getKey()));
referencedContactCounter.inc(contacts.size());
contacts.forEach(
contactRepoId ->
@@ -17,8 +17,8 @@ package google.registry.bsa;
import google.registry.config.RegistryConfig.Config;
import google.registry.groups.GmailClient;
import google.registry.util.EmailMessage;
import jakarta.mail.internet.InternetAddress;
import javax.inject.Inject;
import javax.mail.internet.InternetAddress;
/** Sends BSA-related email notifications. */
class BsaEmailSender {
@@ -39,6 +39,8 @@ import google.registry.model.common.DnsRefreshRequest;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.util.RegistryEnvironment;
import google.registry.util.YamlUtils;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.net.URI;
@@ -54,8 +56,6 @@ import javax.annotation.Nullable;
import javax.inject.Named;
import javax.inject.Qualifier;
import javax.inject.Singleton;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import org.joda.time.DateTime;
import org.joda.time.DateTimeConstants;
import org.joda.time.Duration;
@@ -63,11 +63,11 @@ import google.registry.request.lock.LockHandler;
import google.registry.util.Clock;
import google.registry.util.DomainNameUtils;
import google.registry.util.EmailMessage;
import jakarta.mail.internet.InternetAddress;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import javax.inject.Inject;
import javax.mail.internet.InternetAddress;
import org.joda.time.DateTime;
import org.joda.time.Duration;
@@ -401,7 +401,7 @@ public class CloudDnsWriter extends BaseDnsWriter {
if (err == null || err.getErrors().size() > 1) {
throw e;
}
String errorReason = err.getErrors().get(0).getReason();
String errorReason = err.getErrors().getFirst().getReason();
if (RETRYABLE_EXCEPTION_REASONS.contains(errorReason)) {
throw new ZoneStateException(errorReason);
@@ -96,6 +96,14 @@
<max-retry-duration>3600s</max-retry-duration>
</queue>
<!-- Queue for tasks that update membership in the console user group. -->
<queue>
<name>console-user-group-update</name>
<max-dispatches-per-second>1</max-dispatches-per-second>
<max-concurrent-dispatches>1</max-concurrent-dispatches>
<max-retry-duration>3600s</max-retry-duration>
</queue>
<!-- Queue for infrequent cron tasks (i.e. hourly or less often) that should retry three times on failure. -->
<queue>
<name>retryable-cron-tasks</name>
@@ -15,6 +15,7 @@
package google.registry.flows;
import static com.google.common.base.Preconditions.checkState;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.base.Strings;
@@ -37,6 +38,7 @@ import google.registry.model.host.HostHistory;
import google.registry.model.reporting.HistoryEntry;
import google.registry.persistence.IsolationLevel;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import google.registry.persistence.transaction.JpaTransactionManager;
import java.lang.annotation.Documented;
import java.util.Optional;
import javax.inject.Qualifier;
@@ -191,6 +193,16 @@ public class FlowModule {
}
}
@Provides
@FlowScope
static JpaTransactionManager provideJpaTm(Class<? extends Flow> flowClass) {
if (MutatingFlow.class.isAssignableFrom(flowClass)) {
return tm();
} else {
return replicaTm();
}
}
@Provides
@FlowScope
static ResourceCommand provideResourceCommand(EppInput eppInput) {
@@ -14,7 +14,6 @@
package google.registry.flows;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.xml.XmlTransformer.prettyPrint;
import com.google.common.flogger.FluentLogger;
@@ -28,6 +27,7 @@ import google.registry.model.eppcommon.Trid;
import google.registry.model.eppoutput.EppOutput;
import google.registry.monitoring.whitebox.EppMetric;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import google.registry.persistence.transaction.JpaTransactionManager;
import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Provider;
@@ -52,6 +52,8 @@ public class FlowRunner {
@Inject SessionMetadata sessionMetadata;
@Inject Trid trid;
@Inject FlowReporter flowReporter;
@Inject JpaTransactionManager jpaTransactionManager;
@Inject FlowRunner() {}
/** Runs the EPP flow, and records metrics on the given builder. */
@@ -77,26 +79,24 @@ public class FlowRunner {
return EppOutput.create(flowProvider.get().run());
}
try {
// TODO(mcilwain/weiminyu): Use transactReadOnly() here for TransactionalFlow and transact()
// for MutatingFlow.
return tm().transact(
isolationLevelOverride.orElse(null),
() -> {
try {
EppOutput output = EppOutput.create(flowProvider.get().run());
if (isDryRun) {
throw new DryRunException(output);
}
if (flowClass.equals(LoginFlow.class)) {
// In LoginFlow, registrarId isn't known until after the flow executes, so save
// it then.
eppMetricBuilder.setRegistrarId(sessionMetadata.getRegistrarId());
}
return output;
} catch (EppException e) {
throw new EppRuntimeException(e);
}
});
return jpaTransactionManager.transact(
isolationLevelOverride.orElse(null),
() -> {
try {
EppOutput output = EppOutput.create(flowProvider.get().run());
if (isDryRun) {
throw new DryRunException(output);
}
if (flowClass.equals(LoginFlow.class)) {
// In LoginFlow, registrarId isn't known until after the flow executes, so save
// it then.
eppMetricBuilder.setRegistrarId(sessionMetadata.getRegistrarId());
}
return output;
} catch (EppException e) {
throw new EppRuntimeException(e);
}
});
} catch (DryRunException e) {
return e.output;
} catch (EppRuntimeException e) {
@@ -15,7 +15,6 @@
package google.registry.flows;
import static com.google.common.base.Preconditions.checkState;
import static google.registry.model.IdService.allocateId;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.xml.ValidationMode.LENIENT;
import static google.registry.xml.ValidationMode.STRICT;
@@ -103,7 +102,7 @@ public final class FlowUtils {
}
public static HistoryEntryId createHistoryEntryId(EppResource parent) {
return new HistoryEntryId(parent.getRepoId(), allocateId());
return new HistoryEntryId(parent.getRepoId(), tm().allocateId());
}
/** Registrar is not logged in. */
@@ -19,7 +19,6 @@ import static google.registry.flows.ResourceFlowUtils.verifyResourceDoesNotExist
import static google.registry.flows.contact.ContactFlowUtils.validateAsciiPostalInfo;
import static google.registry.flows.contact.ContactFlowUtils.validateContactAgainstPolicy;
import static google.registry.model.EppResourceUtils.createRepoId;
import static google.registry.model.IdService.allocateId;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet;
@@ -79,7 +78,7 @@ public final class ContactCreateFlow implements MutatingFlow {
.setAuthInfo(command.getAuthInfo())
.setCreationRegistrarId(registrarId)
.setPersistedCurrentSponsorRegistrarId(registrarId)
.setRepoId(createRepoId(allocateId(), roidSuffix))
.setRepoId(createRepoId(tm().allocateId(), roidSuffix))
.setFaxNumber(command.getFax())
.setVoiceNumber(command.getVoice())
.setDisclose(command.getDisclose())
@@ -33,6 +33,7 @@ import static google.registry.flows.domain.DomainFlowUtils.validateDomainName;
import static google.registry.flows.domain.DomainFlowUtils.validateDomainNameWithIdnTables;
import static google.registry.flows.domain.DomainFlowUtils.verifyNotInPredelegation;
import static google.registry.model.tld.Tld.TldState.START_DATE_SUNRISE;
import static google.registry.model.tld.Tld.isEnrolledWithBsa;
import static google.registry.model.tld.label.ReservationType.getTypeOfHighestSeverity;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
@@ -188,7 +189,7 @@ public final class DomainCheckFlow implements TransactionalFlow {
ImmutableSet<InternetDomainName> bsaBlockedDomainNames =
existingDomains.size() == parsedDomains.size()
? ImmutableSet.of()
: getBsaBlockedDomains(parsedDomains.values());
: getBsaBlockedDomains(parsedDomains.values(), now);
Optional<AllocationTokenExtension> allocationTokenExtension =
eppInput.getSingleExtension(AllocationTokenExtension.class);
Optional<AllocationTokenDomainCheckResults> tokenDomainCheckResults =
@@ -441,9 +442,11 @@ public final class DomainCheckFlow implements TransactionalFlow {
}
static ImmutableSet<InternetDomainName> getBsaBlockedDomains(
ImmutableCollection<InternetDomainName> parsedDomains) {
ImmutableCollection<InternetDomainName> parsedDomains, DateTime now) {
Map<String, ImmutableList<InternetDomainName>> labelToDomainNames =
parsedDomains.stream()
.filter(
parsedDomain -> isEnrolledWithBsa(Tld.get(parsedDomain.parent().toString()), now))
.collect(
Collectors.groupingBy(
parsedDomain -> parsedDomain.parts().get(0), toImmutableList()));
@@ -46,7 +46,6 @@ import static google.registry.flows.domain.DomainFlowUtils.verifyPremiumNameIsNo
import static google.registry.flows.domain.DomainFlowUtils.verifyRegistrarIsActive;
import static google.registry.flows.domain.DomainFlowUtils.verifyUnitIsYears;
import static google.registry.model.EppResourceUtils.createDomainRepoId;
import static google.registry.model.IdService.allocateId;
import static google.registry.model.eppcommon.StatusValue.SERVER_HOLD;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_CREATE;
import static google.registry.model.tld.Tld.TldState.GENERAL_AVAILABILITY;
@@ -345,8 +344,8 @@ public final class DomainCreateFlow implements MutatingFlow {
Optional<SecDnsCreateExtension> secDnsCreate =
validateSecDnsExtension(eppInput.getSingleExtension(SecDnsCreateExtension.class));
DateTime registrationExpirationTime = leapSafeAddYears(now, years);
String repoId = createDomainRepoId(allocateId(), tld.getTldStr());
long historyRevisionId = allocateId();
String repoId = createDomainRepoId(tm().allocateId(), tld.getTldStr());
long historyRevisionId = tm().allocateId();
HistoryEntryId domainHistoryId = new HistoryEntryId(repoId, historyRevisionId);
historyBuilder.setRevisionId(historyRevisionId);
// Bill for the create.
@@ -412,11 +412,13 @@ public class DomainFlowUtils {
/** Verify that no linked resources have disallowed statuses. */
static void verifyNotInPendingDelete(
Set<DesignatedContact> contacts, VKey<Contact> registrant, Set<VKey<Host>> nameservers)
Set<DesignatedContact> contacts,
Optional<VKey<Contact>> registrant,
Set<VKey<Host>> nameservers)
throws EppException {
ImmutableList.Builder<VKey<? extends EppResource>> keysToLoad = new ImmutableList.Builder<>();
contacts.stream().map(DesignatedContact::getContactKey).forEach(keysToLoad::add);
Optional.ofNullable(registrant).ifPresent(keysToLoad::add);
registrant.ifPresent(keysToLoad::add);
keysToLoad.addAll(nameservers);
verifyNotInPendingDelete(EppResource.loadCached(keysToLoad.build()).values());
}
@@ -480,9 +482,10 @@ public class DomainFlowUtils {
}
static void validateRequiredContactsPresent(
@Nullable VKey<Contact> registrant, Set<DesignatedContact> contacts)
Optional<VKey<Contact>> registrant, Set<DesignatedContact> contacts)
throws RequiredParameterMissingException {
if (registrant == null) {
// TODO: Check minimum reg data set migration schedule here and don't throw when any are empty.
if (registrant.isEmpty()) {
throw new MissingRegistrantException();
}
@@ -498,14 +501,14 @@ public class DomainFlowUtils {
}
}
static void validateRegistrantAllowedOnTld(String tld, String registrantContactId)
static void validateRegistrantAllowedOnTld(String tld, Optional<String> registrantContactId)
throws RegistrantNotAllowedException {
ImmutableSet<String> allowedRegistrants = Tld.get(tld).getAllowedRegistrantContactIds();
// Empty allow list or null registrantContactId are ignored.
if (registrantContactId != null
if (registrantContactId.isPresent()
&& !allowedRegistrants.isEmpty()
&& !allowedRegistrants.contains(registrantContactId)) {
throw new RegistrantNotAllowedException(registrantContactId);
&& !allowedRegistrants.contains(registrantContactId.get())) {
throw new RegistrantNotAllowedException(registrantContactId.get());
}
}
@@ -119,8 +119,11 @@ public final class DomainInfoFlow implements TransactionalFlow {
.setCreationTime(domain.getCreationTime())
.setLastEppUpdateTime(domain.getLastEppUpdateTime())
.setRegistrationExpirationTime(domain.getRegistrationExpirationTime())
.setLastTransferTime(domain.getLastTransferTime())
.setRegistrant(tm().loadByKey(domain.getRegistrant()).getContactId());
.setLastTransferTime(domain.getLastTransferTime());
domain
.getRegistrant()
.ifPresent(r -> infoBuilder.setRegistrant(tm().loadByKey(r).getContactId()));
// If authInfo is non-null, then the caller is authorized to see the full information since we
// will have already verified the authInfo is valid.
if (registrarId.equals(domain.getCurrentSponsorRegistrarId()) || authInfo.isPresent()) {
@@ -85,6 +85,15 @@ public final class DomainPricingLogic {
createFee = Fee.create(zeroInCurrency(currency), FeeType.CREATE, false);
} else {
DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime);
if (allocationToken.isPresent()
&& allocationToken
.get()
.getRegistrationBehavior()
.equals(RegistrationBehavior.NONPREMIUM_CREATE)) {
domainPrices =
DomainPrices.create(
false, tld.getCreateBillingCost(dateTime), domainPrices.getRenewCost());
}
Money domainCreateCost =
getDomainCreateCostWithDiscount(domainPrices, years, allocationToken);
// Apply a sunrise discount if configured and applicable
@@ -278,7 +278,7 @@ public final class DomainUpdateFlow implements MutatingFlow {
.removeStatusValues(remove.getStatusValues())
.removeContacts(remove.getContacts())
.addContacts(add.getContacts())
.setRegistrant(firstNonNull(change.getRegistrant(), domain.getRegistrant()))
.setRegistrant(change.getRegistrant().or(domain::getRegistrant))
.setAuthInfo(firstNonNull(change.getAuthInfo(), domain.getAuthInfo()));
if (!add.getNameservers().isEmpty()) {
@@ -301,7 +301,10 @@ public final class DomainUpdateFlow implements MutatingFlow {
}
private static void validateRegistrantIsntBeingRemoved(Change change) throws EppException {
if (change.getRegistrantContactId() != null && change.getRegistrantContactId().isEmpty()) {
// TODO(mcilwain): Make this check the minimum registration data set migration schedule
// and not require presence of a registrant in later stages.
if (change.getRegistrantContactId().isPresent()
&& change.getRegistrantContactId().get().isEmpty()) {
throw new MissingRegistrantException();
}
}
@@ -22,7 +22,6 @@ import static google.registry.flows.host.HostFlowUtils.validateHostName;
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainNotInPendingDelete;
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainOwnership;
import static google.registry.model.EppResourceUtils.createRepoId;
import static google.registry.model.IdService.allocateId;
import static google.registry.model.reporting.HistoryEntry.Type.HOST_CREATE;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.CollectionUtils.isNullOrEmpty;
@@ -123,7 +122,7 @@ public final class HostCreateFlow implements MutatingFlow {
.setPersistedCurrentSponsorRegistrarId(registrarId)
.setHostName(targetId)
.setInetAddresses(command.getInetAddresses())
.setRepoId(createRepoId(allocateId(), roidSuffix))
.setRepoId(createRepoId(tm().allocateId(), roidSuffix))
.setSuperordinateDomain(superordinateDomain.map(Domain::createVKey).orElse(null))
.build();
historyBuilder.setType(HOST_CREATE).setHost(newHost);
@@ -28,22 +28,22 @@ import google.registry.config.RegistryConfig.Config;
import google.registry.util.EmailMessage;
import google.registry.util.EmailMessage.Attachment;
import google.registry.util.Retrier;
import jakarta.mail.Address;
import jakarta.mail.BodyPart;
import jakarta.mail.Message.RecipientType;
import jakarta.mail.MessagingException;
import jakarta.mail.Multipart;
import jakarta.mail.Session;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeBodyPart;
import jakarta.mail.internet.MimeMessage;
import jakarta.mail.internet.MimeMultipart;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Properties;
import java.util.function.Predicate;
import javax.inject.Inject;
import javax.mail.Address;
import javax.mail.BodyPart;
import javax.mail.Message.RecipientType;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Session;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
/** Sends {@link EmailMessage EmailMessages} through Google Workspace using {@link Gmail}. */
public final class GmailClient {
@@ -16,8 +16,8 @@ package google.registry.model;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static google.registry.model.IdService.allocateId;
import static google.registry.model.ModelUtils.getAllFields;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import google.registry.model.annotations.IdAllocation;
import google.registry.util.TypeUtils.TypeInstantiator;
@@ -65,7 +65,7 @@ public interface Buildable {
&& !idField.getType().equals(String.class)
&& Optional.ofNullable((Long) ModelUtils.getFieldValue(instance, idField))
.orElse(0L) == 0) {
ModelUtils.setFieldValue(instance, idField, allocateId());
ModelUtils.setFieldValue(instance, idField, tm().reTransact(tm()::allocateId));
}
return instance;
} finally {
@@ -31,6 +31,7 @@ import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature;
import com.google.common.collect.ImmutableSortedSet;
import google.registry.model.common.FeatureFlag.FeatureStatus;
import google.registry.model.common.TimedTransitionProperty;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.tld.Tld.TldState;
@@ -363,6 +364,33 @@ public class EntityYamlUtils {
}
}
/** A custom JSON deserializer for a {@link TimedTransitionProperty} of {@link FeatureStatus}. */
public static class TimedTransitionPropertyFeatureStatusDeserializer
extends StdDeserializer<TimedTransitionProperty<FeatureStatus>> {
public TimedTransitionPropertyFeatureStatusDeserializer() {
this(null);
}
public TimedTransitionPropertyFeatureStatusDeserializer(
Class<TimedTransitionProperty<FeatureStatus>> t) {
super(t);
}
@Override
public TimedTransitionProperty<FeatureStatus> deserialize(
JsonParser jp, DeserializationContext context) throws IOException {
SortedMap<String, String> valueMap = jp.readValueAs(SortedMap.class);
return TimedTransitionProperty.fromValueMap(
valueMap.keySet().stream()
.collect(
toImmutableSortedMap(
natural(),
DateTime::parse,
key -> FeatureStatus.valueOf(valueMap.get(key)))));
}
}
/** A custom JSON deserializer for a {@link CreateAutoTimestamp}. */
public static class CreateAutoTimestampDeserializer extends StdDeserializer<CreateAutoTimestamp> {
@@ -17,6 +17,7 @@ package google.registry.model;
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 com.google.common.collect.ImmutableMap.toImmutableMap;
import static google.registry.model.tld.Tld.TldState.GENERAL_AVAILABILITY;
import static google.registry.model.tld.Tld.TldState.START_DATE_SUNRISE;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
@@ -28,19 +29,28 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import google.registry.batch.CloudTasksUtils;
import google.registry.model.console.RegistrarRole;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.model.console.UserRoles;
import google.registry.model.pricing.StaticPremiumListPricingEngine;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarAddress;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.tld.Tld;
import google.registry.model.tld.Tld.TldState;
import google.registry.model.tld.Tld.TldType;
import google.registry.model.tld.label.PremiumList;
import google.registry.model.tld.label.PremiumListDao;
import google.registry.persistence.VKey;
import google.registry.tools.IamClient;
import google.registry.util.CidrAddressBlock;
import google.registry.util.RegistryEnvironment;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.regex.Pattern;
@@ -75,8 +85,8 @@ public final class OteAccountBuilder {
* Validation regex for registrar base client IDs (3-14 lowercase alphanumeric characters).
*
* <p>The base client ID is appended with numbers to create four different test registrar accounts
* (e.g. reg-1, reg-3, reg-4, reg-5). Registrar client IDs are of type clIDType in eppcom.xsd
* which is limited to 16 characters, hence the limit of 14 here to account for the dash and
* (e.g., reg-1, reg-3, reg-4, reg-5). Registrar client IDs are of type clIDType in eppcom.xsd
* that is limited to 16 characters, hence the limit of 14 here to account for the dash and
* numbers.
*
* <p>The base client ID is also used to generate the OT&E TLDs, hence the restriction to
@@ -113,7 +123,7 @@ public final class OteAccountBuilder {
* The default billing account map applied to all OT&amp;E registrars.
*
* <p>This contains dummy values for USD and JPY so that OT&amp;E registrars can be granted access
* to all existing TLDs in sandbox. Note that OT&amp;E is only on sandbox and thus these dummy
* to all existing TLDs in sandbox. Note that OT&amp;E is only on sandbox, and thus these dummy
* values will never be used in production (the only environment where real invoicing takes
* place).
*/
@@ -124,7 +134,7 @@ public final class OteAccountBuilder {
private final Tld sunriseTld;
private final Tld gaTld;
private final Tld eapTld;
private final ImmutableList.Builder<RegistrarPoc> contactsBuilder = new ImmutableList.Builder<>();
private final List<User> users = new ArrayList<>();
private ImmutableList<Registrar> registrars;
private boolean replaceExisting = false;
@@ -172,16 +182,28 @@ public final class OteAccountBuilder {
}
/**
* Adds a RegistrarContact with Web Console access.
* Adds a {@link User} with Web Console access.
*
* <p>NOTE: can be called more than once, adding multiple contacts. Each contact will have access
* to all OT&amp;E Registrars.
* <p>NOTE: can be called more than once, adding multiple users. Each user will have access to all
* OT&amp;E Registrars.
*
* @param email the contact/login email that will have web-console access to all the Registrars.
* Must be from "our G Suite domain".
* @param email the login email that will have web-console access to all the Registrars. Must be
* from "our Google Workspace domain".
*/
public OteAccountBuilder addContact(String email) {
registrars.forEach(registrar -> contactsBuilder.add(createRegistrarContact(email, registrar)));
public OteAccountBuilder addUser(String email) {
users.add(
new User.Builder()
.setEmailAddress(email)
.setUserRoles(
new UserRoles.Builder()
.setRegistrarRoles(
registrars.stream()
.collect(
toImmutableMap(
Registrar::getRegistrarId,
registrar -> RegistrarRole.ACCOUNT_MANAGER)))
.build())
.build());
return this;
}
@@ -217,7 +239,7 @@ public final class OteAccountBuilder {
return transformRegistrars(builder -> builder.setClientCertificate(asciiCert, now));
}
/** Sets the IP allow list to all the OT&amp;E Registrars. */
/** Sets the IP allowlist to all the OT&amp;E Registrars. */
public OteAccountBuilder setIpAllowList(Collection<String> ipAllowList) {
ImmutableList<CidrAddressBlock> ipAddressAllowList =
ipAllowList.stream().map(CidrAddressBlock::create).collect(toImmutableList());
@@ -237,18 +259,37 @@ public final class OteAccountBuilder {
}
/**
* Return map from the OT&amp;E registrarIds we will create to the new TLDs they will have access
* to.
* Return the map from the OT&amp;E registrarIds we will create to the new TLDs they will have
* access to.
*/
public ImmutableMap<String, String> getRegistrarIdToTldMap() {
return registrarIdToTld;
}
/** Grants the users permission to pass IAP. */
public void grantIapPermission(
Optional<String> groupEmailAddress, CloudTasksUtils cloudTasksUtils, IamClient iamClient) {
for (User user : users) {
User.grantIapPermission(
user.getEmailAddress(), groupEmailAddress, cloudTasksUtils, iamClient);
}
}
/** Saves all the OT&amp;E entities we created. */
private void saveAllEntities() {
// use ImmutableObject instead of Registry so that the Key generation doesn't break
ImmutableList<Tld> registries = ImmutableList.of(sunriseTld, gaTld, eapTld);
ImmutableList<RegistrarPoc> contacts = contactsBuilder.build();
Map<String, User> existingUsers = new HashMap<>();
users.forEach(
user ->
UserDao.loadUser(user.getEmailAddress())
.ifPresent(
existingUser ->
existingUsers.put(existingUser.getEmailAddress(), existingUser)));
if (!replaceExisting) {
checkState(existingUsers.isEmpty(), "Found existing users: %s", existingUsers);
}
tm().transact(
() -> {
@@ -256,8 +297,7 @@ public final class OteAccountBuilder {
ImmutableList<VKey<? extends ImmutableObject>> keys =
Streams.concat(
registries.stream().map(tld -> Tld.createVKey(tld.getTldStr())),
registrars.stream().map(Registrar::createVKey),
contacts.stream().map(RegistrarPoc::createVKey))
registrars.stream().map(Registrar::createVKey))
.collect(toImmutableList());
ImmutableMap<VKey<? extends ImmutableObject>, ImmutableObject> existingObjects =
tm().loadByKeysIfPresent(keys);
@@ -275,8 +315,18 @@ public final class OteAccountBuilder {
registrars = registrars.stream().map(this::addAllowedTld).collect(toImmutableList());
// and we can save the registrars and contacts!
tm().putAll(registrars);
tm().putAll(contacts);
});
for (User user : users) {
String email = user.getEmailAddress();
if (existingUsers.containsKey(email)) {
// Note that other roles for the existing user are reset. We do this instead of simply
// saving the new user is that UserDao does not allow us to save the new user with the same
// email as the existing user.
user = existingUsers.get(email).asBuilder().setUserRoles(user.getUserRoles()).build();
}
UserDao.saveUser(user);
}
}
private Registrar addAllowedTld(Registrar registrar) {
@@ -336,15 +386,6 @@ public final class OteAccountBuilder {
.build();
}
private static RegistrarPoc createRegistrarContact(String email, Registrar registrar) {
return new RegistrarPoc.Builder()
.setRegistrar(registrar)
.setName(email)
.setEmailAddress(email)
.setLoginEmailAddress(email)
.build();
}
/** Returns the registrar IDs of the OT&amp;E, with the TLDs each has access to. */
public static ImmutableMap<String, String> createRegistrarIdToTldMap(String baseRegistrarId) {
checkArgument(
@@ -14,15 +14,14 @@
package google.registry.model.annotations;
import google.registry.model.IdService;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* This annotation is needed for any ID field that needs to be allocated with {@link IdService}
* class
* This annotation is needed for any ID field that needs to be allocated with {@link
* google.registry.persistence.transaction.TransactionManager#allocateId} class
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@@ -0,0 +1,196 @@
// 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.common;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.config.RegistryConfig.getSingletonCacheRefreshDuration;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Maps;
import google.registry.model.Buildable;
import google.registry.model.CacheUtils;
import google.registry.model.EntityYamlUtils.TimedTransitionPropertyFeatureStatusDeserializer;
import google.registry.model.ImmutableObject;
import google.registry.persistence.VKey;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.Id;
import org.joda.time.DateTime;
@Entity
public class FeatureFlag extends ImmutableObject implements Buildable {
/**
* The current status of the feature the flag represents.
*
* <p>Currently, there is no enforced ordering of these status values, but that may change in the
* future should new statuses be added to this enum that require it.
*/
public enum FeatureStatus {
ACTIVE,
INACTIVE
}
public enum FeatureName {
TEST_FEATURE,
MINIMUM_DATASET_CONTACTS_OPTIONAL,
MINIMUM_DATASET_CONTACTS_PROHIBITED
}
/** The name of the flag/feature. */
@Enumerated(EnumType.STRING)
@Id
FeatureName featureName;
/** A map of times for each {@link FeatureStatus} the FeatureFlag should hold. */
@Column(nullable = false)
@JsonDeserialize(using = TimedTransitionPropertyFeatureStatusDeserializer.class)
TimedTransitionProperty<FeatureStatus> status =
TimedTransitionProperty.withInitialValue(FeatureStatus.INACTIVE);
public static Optional<FeatureFlag> getUncached(FeatureName featureName) {
return tm().transact(() -> tm().loadByKeyIfPresent(createVKey(featureName)));
}
public static ImmutableList<FeatureFlag> getAllUncached() {
return tm().transact(() -> tm().loadAllOf(FeatureFlag.class));
}
public static FeatureFlag get(FeatureName featureName) {
Optional<FeatureFlag> maybeFeatureFlag = CACHE.get(featureName);
return maybeFeatureFlag.orElseThrow(() -> new FeatureFlagNotFoundException(featureName));
}
public static ImmutableSet<FeatureFlag> getAll(Set<FeatureName> featureNames) {
Map<FeatureName, Optional<FeatureFlag>> featureFlags = CACHE.getAll(featureNames);
ImmutableSet<FeatureName> missingFlags =
featureFlags.entrySet().stream()
.filter(e -> e.getValue().isEmpty())
.map(Map.Entry::getKey)
.collect(toImmutableSet());
if (missingFlags.isEmpty()) {
return featureFlags.values().stream().map(Optional::get).collect(toImmutableSet());
} else {
throw new FeatureFlagNotFoundException(missingFlags);
}
}
/** A cache that loads the {@link FeatureFlag} for a given featureName. */
private static final LoadingCache<FeatureName, Optional<FeatureFlag>> CACHE =
CacheUtils.newCacheBuilder(getSingletonCacheRefreshDuration())
.build(
new CacheLoader<>() {
@Override
public Optional<FeatureFlag> load(final FeatureName featureName) {
return tm().reTransact(() -> tm().loadByKeyIfPresent(createVKey(featureName)));
}
@Override
public Map<? extends FeatureName, ? extends Optional<FeatureFlag>> loadAll(
Set<? extends FeatureName> featureFlagNames) {
ImmutableMap<FeatureName, VKey<FeatureFlag>> keysMap =
featureFlagNames.stream()
.collect(
toImmutableMap(featureName -> featureName, FeatureFlag::createVKey));
Map<VKey<? extends FeatureFlag>, FeatureFlag> entities =
tm().reTransact(() -> tm().loadByKeysIfPresent(keysMap.values()));
return Maps.toMap(
featureFlagNames,
name -> Optional.ofNullable(entities.get(createVKey(name))));
}
});
public static VKey<FeatureFlag> createVKey(FeatureName featureName) {
return VKey.create(FeatureFlag.class, featureName);
}
@Override
public VKey<FeatureFlag> createVKey() {
return createVKey(featureName);
}
public FeatureName getFeatureName() {
return featureName;
}
@JsonProperty("status")
public TimedTransitionProperty<FeatureStatus> getStatusMap() {
return status;
}
public FeatureStatus getStatus(DateTime time) {
return status.getValueAtTime(time);
}
@Override
public FeatureFlag.Builder asBuilder() {
return new FeatureFlag.Builder(clone(this));
}
/** A builder for constructing {@link FeatureFlag} objects, since they are immutable. */
public static class Builder extends Buildable.Builder<FeatureFlag> {
public Builder() {}
private Builder(FeatureFlag instance) {
super(instance);
}
@Override
public FeatureFlag build() {
getInstance().status.checkValidity();
checkArgument(getInstance().featureName != null, "FeatureName cannot be null");
return super.build();
}
public Builder setFeatureName(FeatureName featureName) {
getInstance().featureName = featureName;
return this;
}
public Builder setStatusMap(ImmutableSortedMap<DateTime, FeatureStatus> statusMap) {
getInstance().status = TimedTransitionProperty.fromValueMap(statusMap);
return this;
}
}
/** Exception to throw when no FeatureFlag entity is found for given FeatureName string(s). */
public static class FeatureFlagNotFoundException extends RuntimeException {
FeatureFlagNotFoundException(ImmutableSet<FeatureName> featureNames) {
super("No feature flag object(s) found for " + Joiner.on(", ").join(featureNames));
}
public FeatureFlagNotFoundException(FeatureName featureName) {
this(ImmutableSet.of(featureName));
}
}
}
@@ -14,7 +14,19 @@
package google.registry.model.console;
import static google.registry.tools.server.UpdateUserGroupAction.GROUP_UPDATE_QUEUE;
import com.google.cloud.tasks.v2.Task;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.flogger.FluentLogger;
import google.registry.batch.CloudTasksUtils;
import google.registry.persistence.VKey;
import google.registry.request.Action.Service;
import google.registry.tools.IamClient;
import google.registry.tools.server.UpdateUserGroupAction;
import google.registry.tools.server.UpdateUserGroupAction.Mode;
import google.registry.util.RegistryEnvironment;
import java.util.Optional;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Embeddable;
@@ -31,6 +43,76 @@ import javax.persistence.Table;
@Table(indexes = {@Index(columnList = "emailAddress", name = "user_email_address_idx")})
public class User extends UserBase {
public static final String IAP_SECURED_WEB_APP_USER_ROLE = "roles/iap.httpsResourceAccessor";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/**
* Grants the user permission to pass IAP.
*
* <p>Depending on if a console user group is set up, the permission is granted either
* individually or via group membership.
*/
public static void grantIapPermission(
String emailAddress,
Optional<String> groupEmailAddress,
CloudTasksUtils cloudTasksUtils,
IamClient iamClient) {
if (RegistryEnvironment.isInTestServer()) {
return;
}
if (groupEmailAddress.isEmpty()) {
logger.atInfo().log("Granting IAP role to user %s", emailAddress);
iamClient.addBinding(emailAddress, IAP_SECURED_WEB_APP_USER_ROLE);
} else {
logger.atInfo().log("Adding %s to group %s", emailAddress, groupEmailAddress.get());
modifyGroupMembershipAsync(
emailAddress, groupEmailAddress.get(), cloudTasksUtils, UpdateUserGroupAction.Mode.ADD);
}
}
/**
* Revoke the user's permission to pass IAP.
*
* <p>Depending on if a console user group is set up, the permission is revoked either
* individually or via group membership.
*/
public static void revokeIapPermission(
String emailAddress,
Optional<String> groupEmailAddress,
CloudTasksUtils cloudTasksUtils,
IamClient iamClient) {
if (RegistryEnvironment.isInTestServer()) {
return;
}
if (groupEmailAddress.isEmpty()) {
logger.atInfo().log("Removing IAP role from user %s", emailAddress);
iamClient.removeBinding(emailAddress, IAP_SECURED_WEB_APP_USER_ROLE);
} else {
logger.atInfo().log("Removing %s from group %s", emailAddress, groupEmailAddress.get());
modifyGroupMembershipAsync(
emailAddress, groupEmailAddress.get(), cloudTasksUtils, Mode.REMOVE);
}
}
private static void modifyGroupMembershipAsync(
String userEmailAddress,
String groupEmailAddress,
CloudTasksUtils cloudTasksUtils,
Mode mode) {
Task task =
cloudTasksUtils.createPostTask(
UpdateUserGroupAction.PATH,
Service.TOOLS,
ImmutableMultimap.of(
"userEmailAddress",
userEmailAddress,
"groupEmailAddress",
groupEmailAddress,
"groupUpdateMode",
mode.name()));
cloudTasksUtils.enqueue(GROUP_UPDATE_QUEUE, task);
}
@Override
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -46,6 +46,7 @@ import google.registry.model.EppResource;
import google.registry.model.EppResource.ResourceWithTransferData;
import google.registry.model.billing.BillingRecurrence;
import google.registry.model.contact.Contact;
import google.registry.model.domain.DesignatedContact.Type;
import google.registry.model.domain.launch.LaunchNotice;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.domain.secdns.DomainDsData;
@@ -131,11 +132,11 @@ public class DomainBase extends EppResource
@Expose @Transient Set<VKey<Host>> nsHosts;
/** Contacts. */
@Expose VKey<Contact> adminContact;
@Expose @Nullable VKey<Contact> adminContact;
@Expose VKey<Contact> billingContact;
@Expose VKey<Contact> techContact;
@Expose VKey<Contact> registrantContact;
@Expose @Nullable VKey<Contact> billingContact;
@Expose @Nullable VKey<Contact> techContact;
@Expose @Nullable VKey<Contact> registrantContact;
/** Authorization info (aka transfer secret) of the domain. */
@Embedded
@@ -585,32 +586,49 @@ public class DomainBase extends EppResource
}
/** A key to the registrant who registered this domain. */
public VKey<Contact> getRegistrant() {
return registrantContact;
public Optional<VKey<Contact>> getRegistrant() {
return Optional.ofNullable(registrantContact);
}
public VKey<Contact> getAdminContact() {
return adminContact;
public Optional<VKey<Contact>> getAdminContact() {
return Optional.ofNullable(adminContact);
}
public VKey<Contact> getBillingContact() {
return billingContact;
public Optional<VKey<Contact>> getBillingContact() {
return Optional.ofNullable(billingContact);
}
public VKey<Contact> getTechContact() {
return techContact;
public Optional<VKey<Contact>> getTechContact() {
return Optional.ofNullable(techContact);
}
/** Associated contacts for the domain (other than registrant). */
/**
* Associated contacts for the domain (other than registrant).
*
* <p>Note: This can be an empty set if no contacts are present for the domain.
*/
public ImmutableSet<DesignatedContact> getContacts() {
return getAllContacts(false);
}
/**
* Gets all associated contacts for the domain, including the registrant.
*
* <p>Note: This can be an empty set if no contacts are present for the domain.
*/
public ImmutableSet<DesignatedContact> getAllContacts() {
return getAllContacts(true);
}
public DomainAuthInfo getAuthInfo() {
return authInfo;
}
/** Returns all referenced contacts from this domain. */
/**
* Returns all referenced contacts from this domain.
*
* <p>Note: This can be an empty set if no contacts are present for the domain.
*/
public ImmutableSet<VKey<Contact>> getReferencedContacts() {
return nullToEmptyImmutableCopy(getAllContacts(true)).stream()
.map(DesignatedContact::getContactKey)
@@ -620,18 +638,12 @@ public class DomainBase extends EppResource
private ImmutableSet<DesignatedContact> getAllContacts(boolean includeRegistrant) {
ImmutableSet.Builder<DesignatedContact> builder = new ImmutableSet.Builder<>();
if (includeRegistrant && registrantContact != null) {
builder.add(DesignatedContact.create(DesignatedContact.Type.REGISTRANT, registrantContact));
}
if (adminContact != null) {
builder.add(DesignatedContact.create(DesignatedContact.Type.ADMIN, adminContact));
}
if (billingContact != null) {
builder.add(DesignatedContact.create(DesignatedContact.Type.BILLING, billingContact));
}
if (techContact != null) {
builder.add(DesignatedContact.create(DesignatedContact.Type.TECH, techContact));
if (includeRegistrant) {
getRegistrant().ifPresent(c -> builder.add(DesignatedContact.create(Type.REGISTRANT, c)));
}
getAdminContact().ifPresent(c -> builder.add(DesignatedContact.create(Type.ADMIN, c)));
getBillingContact().ifPresent(c -> builder.add(DesignatedContact.create(Type.BILLING, c)));
getTechContact().ifPresent(c -> builder.add(DesignatedContact.create(Type.TECH, c)));
return builder.build();
}
@@ -647,11 +659,13 @@ public class DomainBase extends EppResource
*/
void setContactFields(Set<DesignatedContact> contacts, boolean includeRegistrant) {
// Set the individual contact fields.
billingContact = techContact = adminContact = null;
billingContact = null;
techContact = null;
adminContact = null;
if (includeRegistrant) {
registrantContact = null;
}
HashSet<DesignatedContact.Type> contactsDiscovered = new HashSet<>();
HashSet<Type> contactsDiscovered = new HashSet<>();
for (DesignatedContact contact : contacts) {
checkArgument(
!contactsDiscovered.contains(contact.getType()),
@@ -682,7 +696,7 @@ public class DomainBase extends EppResource
/** Predicate to determine if a given {@link DesignatedContact} is the registrant. */
static final Predicate<DesignatedContact> IS_REGISTRANT =
(DesignatedContact contact) -> DesignatedContact.Type.REGISTRANT.equals(contact.type);
(DesignatedContact contact) -> Type.REGISTRANT.equals(contact.type);
/** An override of {@link EppResource#asBuilder} with tighter typing. */
@Override
@@ -717,7 +731,6 @@ public class DomainBase extends EppResource
instance.autorenewEndTime = firstNonNull(getInstance().autorenewEndTime, END_OF_TIME);
checkArgumentNotNull(emptyToNull(instance.domainName), "Missing domainName");
checkArgumentNotNull(instance.getRegistrant(), "Missing registrant");
instance.tld = getTldFromDomainName(instance.domainName);
T newDomain = super.build();
@@ -749,9 +762,9 @@ public class DomainBase extends EppResource
return thisCastToDerived();
}
public B setRegistrant(VKey<Contact> registrant) {
public B setRegistrant(Optional<VKey<Contact>> registrant) {
// Set the registrant field specifically.
getInstance().registrantContact = registrant;
getInstance().registrantContact = registrant.orElse(null);
return thisCastToDerived();
}
@@ -40,6 +40,7 @@ import google.registry.model.eppinput.ResourceCommand.ResourceUpdate;
import google.registry.model.eppinput.ResourceCommand.SingleResourceCommand;
import google.registry.model.host.Host;
import google.registry.persistence.VKey;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nullable;
import javax.xml.bind.annotation.XmlAttribute;
@@ -76,21 +77,21 @@ public class DomainCommand {
/** The contactId of the registrant who registered this domain. */
@XmlElement(name = "registrant")
@Nullable
String registrantContactId;
/** A resolved key to the registrant who registered this domain. */
@XmlTransient VKey<Contact> registrant;
@Nullable @XmlTransient VKey<Contact> registrant;
/** Authorization info (aka transfer secret) of the domain. */
DomainAuthInfo authInfo;
public String getRegistrantContactId() {
return registrantContactId;
public Optional<String> getRegistrantContactId() {
return Optional.ofNullable(registrantContactId);
}
@Nullable
public VKey<Contact> getRegistrant() {
return registrant;
public Optional<VKey<Contact>> getRegistrant() {
return Optional.ofNullable(registrant);
}
public DomainAuthInfo getAuthInfo() {
@@ -63,6 +63,7 @@ public abstract class DomainInfoData implements ResponseData {
abstract ImmutableSet<StatusValue> getStatusValues();
@XmlElement(name = "registrant")
@Nullable
abstract String getRegistrant();
@XmlElement(name = "contact")
@@ -15,7 +15,7 @@
package google.registry.model.domain;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.model.IdService.allocateId;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.common.annotations.VisibleForTesting;
@@ -70,7 +70,8 @@ public class GracePeriod extends GracePeriodBase {
(billingRecurrence != null) == GracePeriodStatus.AUTO_RENEW.equals(type),
"BillingRecurrences must be present on (and only on) autorenew grace periods");
GracePeriod instance = new GracePeriod();
instance.gracePeriodId = gracePeriodId == null ? allocateId() : gracePeriodId;
instance.gracePeriodId =
gracePeriodId == null ? tm().reTransact(tm()::allocateId) : gracePeriodId;
instance.type = checkArgumentNotNull(type);
instance.domainRepoId = checkArgumentNotNull(domainRepoId);
instance.expirationTime = checkArgumentNotNull(expirationTime);
@@ -198,7 +199,7 @@ public class GracePeriod extends GracePeriodBase {
static GracePeriodHistory createFrom(long historyRevisionId, GracePeriod gracePeriod) {
GracePeriodHistory instance = new GracePeriodHistory();
instance.gracePeriodHistoryRevisionId = allocateId();
instance.gracePeriodHistoryRevisionId = tm().reTransact(tm()::allocateId);
instance.domainHistoryRevisionId = historyRevisionId;
instance.gracePeriodId = gracePeriod.gracePeriodId;
instance.type = gracePeriod.type;
@@ -14,7 +14,7 @@
package google.registry.model.domain.secdns;
import static google.registry.model.IdService.allocateId;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import google.registry.model.domain.DomainHistory;
import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
@@ -48,7 +48,7 @@ public class DomainDsDataHistory extends DomainDsDataBase {
instance.algorithm = dsData.getAlgorithm();
instance.digestType = dsData.getDigestType();
instance.digest = dsData.getDigest();
instance.dsDataHistoryRevisionId = allocateId();
instance.dsDataHistoryRevisionId = tm().reTransact(tm()::allocateId);
return instance;
}
@@ -50,6 +50,7 @@ import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName
import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
import google.registry.persistence.VKey;
import google.registry.persistence.WithVKey;
import google.registry.persistence.converter.JodaMoneyType;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -63,6 +64,9 @@ import javax.persistence.Enumerated;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.Table;
import org.hibernate.annotations.Columns;
import org.hibernate.annotations.Type;
import org.joda.money.Money;
import org.joda.time.DateTime;
/** An entity representing an allocation token. */
@@ -115,7 +119,17 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda
*/
BYPASS_TLD_STATE,
/** Bypasses most checks and creates the domain as an anchor tenant, with all that implies. */
ANCHOR_TENANT
ANCHOR_TENANT,
/**
* Bypasses the premium list to use the standard creation price. Does not affect the renewal
* price.
*
* <p>This cannot be specified along with a discount fraction, and any renewals (automatic or
* otherwise) will use the premium price for the domain if one exists.
*
* <p>Tokens with this behavior must be tied to a single particular domain.
*/
NONPREMIUM_CREATE
}
/** Type of the token that indicates how and where it should be used. */
@@ -226,6 +240,12 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda
@Column(name = "renewalPriceBehavior", nullable = false)
RenewalPriceBehavior renewalPriceBehavior = RenewalPriceBehavior.DEFAULT;
/** The price used for renewals iff the renewalPriceBehavior is SPECIFIED. */
@Nullable
@Type(type = JodaMoneyType.TYPE_NAME)
@Columns(columns = {@Column(name = "renewalPriceAmount"), @Column(name = "renewalPriceCurrency")})
Money renewalPrice;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
RegistrationBehavior registrationBehavior = RegistrationBehavior.DEFAULT;
@@ -300,6 +320,10 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda
return renewalPriceBehavior;
}
public Optional<Money> getRenewalPrice() {
return Optional.ofNullable(renewalPrice);
}
public RegistrationBehavior getRegistrationBehavior() {
return registrationBehavior;
}
@@ -367,29 +391,12 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda
public AllocationToken build() {
checkArgumentNotNull(getInstance().tokenType, "Token type must be specified");
checkArgument(!Strings.isNullOrEmpty(getInstance().token), "Token must not be null or empty");
checkArgument(
!getInstance().tokenType.equals(TokenType.BULK_PRICING)
|| getInstance().renewalPriceBehavior.equals(RenewalPriceBehavior.SPECIFIED),
"Bulk tokens must have renewalPriceBehavior set to SPECIFIED");
checkArgument(
!getInstance().tokenType.equals(TokenType.BULK_PRICING)
|| ImmutableSet.of(CommandName.CREATE).equals(getInstance().allowedEppActions),
"Bulk tokens may only be valid for CREATE actions");
checkArgument(
!getInstance().tokenType.equals(TokenType.BULK_PRICING)
|| !getInstance().discountPremiums,
"Bulk tokens cannot discount premium names");
checkArgument(
getInstance().domainName == null || getInstance().tokenType.isOneTimeUse(),
"Domain name can only be specified for SINGLE_USE or REGISTER_BSA tokens");
checkArgument(
getInstance().redemptionHistoryId == null || getInstance().tokenType.isOneTimeUse(),
"Redemption history entry can only be specified for SINGLE_USE or REGISTER_BSA tokens");
checkArgument(
getInstance().tokenType != TokenType.BULK_PRICING
|| (getInstance().allowedClientIds != null
&& getInstance().allowedClientIds.size() == 1),
"BULK_PRICING tokens must have exactly one allowed client registrar");
checkArgument(
getInstance().discountFraction > 0 || !getInstance().discountPremiums,
"Discount premiums can only be specified along with a discount fraction");
@@ -404,6 +411,44 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda
checkArgumentNotNull(
getInstance().domainName, "ANCHOR_TENANT tokens must be tied to a domain");
}
if (getInstance().registrationBehavior.equals(RegistrationBehavior.NONPREMIUM_CREATE)) {
checkArgument(
getInstance().discountFraction == 0.0,
"NONPREMIUM_CREATE tokens cannot apply a discount");
checkArgumentNotNull(
getInstance().domainName, "NONPREMIUM_CREATE tokens must be tied to a domain");
checkArgument(
getInstance().allowedEppActions == null
|| getInstance().allowedEppActions.contains(CommandName.CREATE),
"NONPREMIUM_CREATE tokens must allow for CREATE actions");
}
checkArgument(
getInstance().renewalPriceBehavior.equals(RenewalPriceBehavior.SPECIFIED)
== (getInstance().renewalPrice != null),
"renewalPrice must be specified iff renewalPriceBehavior is SPECIFIED");
if (getInstance().tokenType.equals(TokenType.BULK_PRICING)) {
checkArgument(
getInstance().discountFraction == 1.0,
"BULK_PRICING tokens must have a discountFraction of 1.0");
checkArgument(
!getInstance().shouldDiscountPremiums(),
"BULK_PRICING tokens cannot discount premium names");
checkArgument(
getInstance().renewalPriceBehavior.equals(RenewalPriceBehavior.SPECIFIED),
"BULK_PRICING tokens must have renewalPriceBehavior set to SPECIFIED");
checkArgument(
getInstance().renewalPrice.getAmount().intValue() == 0,
"BULK_PRICING tokens must have a renewal price of 0");
checkArgument(
ImmutableSet.of(CommandName.CREATE).equals(getInstance().allowedEppActions),
"BULK_PRICING tokens may only be valid for CREATE actions");
checkArgument(
getInstance().allowedClientIds != null && getInstance().allowedClientIds.size() == 1,
"BULK_PRICING tokens must have exactly one allowed client registrar");
}
if (getInstance().domainName != null) {
try {
DomainFlowUtils.validateDomainName(getInstance().domainName);
@@ -500,6 +545,11 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda
return this;
}
public Builder setRenewalPrice(Money renewalPrice) {
getInstance().renewalPrice = renewalPrice;
return this;
}
public Builder setRegistrationBehavior(RegistrationBehavior registrationBehavior) {
getInstance().registrationBehavior = registrationBehavior;
return this;
@@ -46,7 +46,7 @@ public interface PremiumPricingEngine {
private Money createCost;
private Money renewCost;
static DomainPrices create(boolean isPremium, Money createCost, Money renewCost) {
public static DomainPrices create(boolean isPremium, Money createCost, Money renewCost) {
DomainPrices instance = new DomainPrices();
instance.isPremium = isPremium;
instance.createCost = createCost;
@@ -62,6 +62,8 @@ import google.registry.model.tld.Tld.TldType;
import google.registry.persistence.VKey;
import google.registry.util.CidrAddressBlock;
import google.registry.util.PasswordUtils;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
import java.security.cert.CertificateParsingException;
import java.util.Comparator;
import java.util.List;
@@ -72,8 +74,6 @@ import java.util.Set;
import java.util.function.Predicate;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.AttributeOverride;
@@ -192,24 +192,19 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
/** Returns the TLD for a given TLD, throwing if none exists. */
public static Tld get(String tld) {
Tld maybeTld = CACHE.get(tld);
if (maybeTld == null) {
throw new TldNotFoundException(tld);
} else {
return maybeTld;
}
return CACHE.get(tld).orElseThrow(() -> new TldNotFoundException(tld));
}
/** Returns the TLD entities for the given TLD strings, throwing if any don't exist. */
public static ImmutableSet<Tld> get(Set<String> tlds) {
Map<String, Tld> registries = CACHE.getAll(tlds);
Map<String, Optional<Tld>> registries = CACHE.getAll(tlds);
ImmutableSet<String> missingRegistries =
registries.entrySet().stream()
.filter(e -> e.getValue() == null)
.filter(e -> e.getValue().isEmpty())
.map(Map.Entry::getKey)
.collect(toImmutableSet());
if (missingRegistries.isEmpty()) {
return registries.values().stream().collect(toImmutableSet());
return registries.values().stream().map(Optional::get).collect(toImmutableSet());
} else {
throw new TldNotFoundException(missingRegistries);
}
@@ -227,23 +222,24 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
}
/** A cache that loads the {@link Tld} for a given tld. */
private static final LoadingCache<String, Tld> CACHE =
private static final LoadingCache<String, Optional<Tld>> CACHE =
CacheUtils.newCacheBuilder(getSingletonCacheRefreshDuration())
.build(
new CacheLoader<>() {
@Override
public Tld load(final String tld) {
return tm().reTransact(() -> tm().loadByKeyIfPresent(createVKey(tld)))
.orElse(null);
public Optional<Tld> load(final String tld) {
return tm().reTransact(() -> tm().loadByKeyIfPresent(createVKey(tld)));
}
@Override
public Map<? extends String, ? extends Tld> loadAll(Set<? extends String> tlds) {
public Map<? extends String, ? extends Optional<Tld>> loadAll(
Set<? extends String> tlds) {
ImmutableMap<String, VKey<Tld>> keysMap =
tlds.stream().collect(toImmutableMap(tld -> tld, Tld::createVKey));
Map<VKey<? extends Tld>, Tld> entities =
tm().reTransact(() -> tm().loadByKeysIfPresent(keysMap.values()));
return Maps.transformEntries(keysMap, (k, v) -> entities.getOrDefault(v, null));
return Maps.toMap(
tlds, tld -> Optional.ofNullable(entities.get(createVKey(tld))));
}
});
@@ -114,6 +114,8 @@ import google.registry.ui.server.console.ConsoleDomainListAction;
import google.registry.ui.server.console.ConsoleDumDownloadAction;
import google.registry.ui.server.console.ConsoleEppPasswordAction;
import google.registry.ui.server.console.ConsoleRegistryLockAction;
import google.registry.ui.server.console.ConsoleRegistryLockVerifyAction;
import google.registry.ui.server.console.ConsoleUpdateRegistrarAction;
import google.registry.ui.server.console.ConsoleUserDataAction;
import google.registry.ui.server.console.RegistrarsAction;
import google.registry.ui.server.console.settings.ContactAction;
@@ -190,8 +192,12 @@ interface RequestComponent {
ConsoleRegistryLockAction consoleRegistryLockAction();
ConsoleRegistryLockVerifyAction consoleRegistryLockVerifyAction();
ConsoleUiAction consoleUiAction();
ConsoleUpdateRegistrarAction consoleUpdateRegistrarAction();
ConsoleUserDataAction consoleUserDataAction();
ConsoleDumDownloadAction consoleDumDownloadAction();
@@ -30,6 +30,8 @@ import google.registry.ui.server.console.ConsoleDomainListAction;
import google.registry.ui.server.console.ConsoleDumDownloadAction;
import google.registry.ui.server.console.ConsoleEppPasswordAction;
import google.registry.ui.server.console.ConsoleRegistryLockAction;
import google.registry.ui.server.console.ConsoleRegistryLockVerifyAction;
import google.registry.ui.server.console.ConsoleUpdateRegistrarAction;
import google.registry.ui.server.console.ConsoleUserDataAction;
import google.registry.ui.server.console.RegistrarsAction;
import google.registry.ui.server.console.settings.ContactAction;
@@ -68,8 +70,12 @@ public interface FrontendRequestComponent {
ConsoleRegistryLockAction consoleRegistryLockAction();
ConsoleRegistryLockVerifyAction consoleRegistryLockVerifyAction();
ConsoleUiAction consoleUiAction();
ConsoleUpdateRegistrarAction consoleUpdateRegistrarAction();
ConsoleUserDataAction consoleUserDataAction();
ConsoleDumDownloadAction consoleDumDownloadAction();
@@ -0,0 +1,34 @@
// 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.persistence.converter;
import google.registry.model.common.FeatureFlag.FeatureStatus;
import javax.persistence.Converter;
/** JPA converter for {@link google.registry.model.common.FeatureFlag} status transitions. */
@Converter(autoApply = true)
public class FeatureFlagStatusConverter
extends TimedTransitionPropertyConverterBase<FeatureStatus> {
@Override
protected String convertValueToString(FeatureStatus value) {
return value.toString();
}
@Override
protected FeatureStatus convertStringToValue(String string) {
return FeatureStatus.valueOf(string);
}
}
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
package google.registry.model;
package google.registry.persistence.transaction;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
@@ -22,7 +22,7 @@ import java.util.concurrent.atomic.AtomicLong;
/**
* Allocates a {@code long} to use as a {@code @Id}, (part) of the primary SQL key for an entity.
*/
public final class IdService {
final class IdService {
private IdService() {}
@@ -32,7 +32,7 @@ public final class IdService {
*
* <p>The generated IDs are project-wide unique.
*/
public static long allocateId() {
static long allocateId() {
return tm().transact(
() ->
(BigInteger)
@@ -56,6 +56,7 @@ import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.function.Supplier;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import javax.annotation.Nullable;
@@ -137,6 +138,12 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
return transactionInfo.get().inTransaction;
}
@Override
public long allocateId() {
assertInTransaction();
return transactionInfo.get().idProvider.get();
}
@Override
public void assertInTransaction() {
if (!inTransaction()) {
@@ -210,7 +217,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
EntityTransaction txn = txnInfo.entityManager.getTransaction();
try {
txn.begin();
txnInfo.start(clock);
txnInfo.start(clock, readOnly ? ReplicaDbIdService::allocatedId : IdService::allocateId);
if (readOnly) {
getEntityManager().createNativeQuery("SET TRANSACTION READ ONLY").executeUpdate();
logger.atInfo().log("Using read-only SQL replica");
@@ -668,6 +675,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
EntityManager entityManager;
boolean inTransaction = false;
DateTime transactionTime;
Supplier<Long> idProvider;
// The set of entity objects that have been either persisted (via insert()) or merged (via
// put()/update()). If the entity manager returns these as a result of a find() or query
@@ -676,13 +684,15 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
Set<Object> objectsToSave = Collections.newSetFromMap(new IdentityHashMap<>());
/** Start a new transaction. */
private void start(Clock clock) {
private void start(Clock clock, Supplier<Long> idProvider) {
checkArgumentNotNull(clock);
inTransaction = true;
transactionTime = clock.nowUtc();
this.idProvider = idProvider;
}
private void clear() {
idProvider = null;
inTransaction = false;
transactionTime = null;
objectsToSave = Collections.newSetFromMap(new IdentityHashMap<>());
@@ -0,0 +1,38 @@
// 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.persistence.transaction;
import java.util.concurrent.atomic.AtomicLong;
/**
* Provides {@code long} values for use as {@code id} by JPA model entities in (read-only)
* transactions in the replica database. Each id is only unique in the JVM instance.
*
* <p>The {@link IdService database sequence-based id service} cannot be used with the replica
* because id generation is a write operation.
*/
final class ReplicaDbIdService {
private ReplicaDbIdService() {}
private static final AtomicLong nextId = new AtomicLong(1);
/**
* Returns the next long value from a {@link AtomicLong}. Each id is unique in the JVM instance.
*/
static final long allocatedId() {
return nextId.getAndIncrement();
}
}
@@ -48,6 +48,15 @@ public interface TransactionManager {
*/
void assertInTransaction();
/**
* Returns a {@link long} value that can be used as {@code id} by a JPA model entity.
*
* <p>The returned value must be project-wide unique when transacting on the primary database
* instance, but only needs to be unique within a JVM instance when transacting on the replica
* instance.
*/
long allocateId();
/** Executes the work in a transaction and returns the result. */
<T> T transact(Callable<T> work);
@@ -76,7 +76,7 @@ public class RdapEntityAction extends RdapActionBase {
// they are global, and might have different roles for different domains.
if (contact.isPresent() && isAuthorized(contact.get())) {
return rdapJsonFormatter.createRdapContactEntity(
contact.get(), ImmutableSet.of(), OutputDataType.FULL);
contact, ImmutableSet.of(), OutputDataType.FULL);
}
}
@@ -461,7 +461,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
.entitySearchResultsBuilder()
.add(
rdapJsonFormatter.createRdapContactEntity(
contact, ImmutableSet.of(), outputDataType));
Optional.of(contact), ImmutableSet.of(), outputDataType));
newCursor =
Optional.of(
CONTACT_CURSOR_PREFIX
@@ -61,6 +61,7 @@ import google.registry.rdap.RdapDataStructures.RdapStatus;
import google.registry.rdap.RdapObjectClasses.RdapContactEntity;
import google.registry.rdap.RdapObjectClasses.RdapDomain;
import google.registry.rdap.RdapObjectClasses.RdapEntity;
import google.registry.rdap.RdapObjectClasses.RdapEntity.Role;
import google.registry.rdap.RdapObjectClasses.RdapNameserver;
import google.registry.rdap.RdapObjectClasses.RdapRegistrarEntity;
import google.registry.rdap.RdapObjectClasses.SecureDns;
@@ -74,6 +75,7 @@ import java.net.InetAddress;
import java.net.URI;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
@@ -249,6 +251,16 @@ public class RdapJsonFormatter {
private static final Ordering<DesignatedContact> DESIGNATED_CONTACT_ORDERING =
Ordering.natural().onResultOf(DesignatedContact::getType);
/**
* The list of RDAP contact roles that are required to be present on each domain.
*
* <p>Per RDAP Response Profile 2.7.3, A domain MUST have the REGISTRANT, ADMIN, TECH roles and
* MAY have others. We also have the BILLING role in our system but it isn't required and is only
* listed if actually present.
*/
private static final ImmutableSet<Role> REQUIRED_CONTACT_ROLES =
ImmutableSet.of(Role.REGISTRANT, Role.ADMIN, Role.TECH);
/** Creates the TOS notice that is added to every reply. */
Notice createTosNotice() {
String linkValue = makeRdapServletRelativeUrl("help", RdapHelpAction.TOS_PATH);
@@ -369,38 +381,53 @@ public class RdapJsonFormatter {
() ->
ImmutableSet.copyOf(replicaTm().loadByKeys(domain.getNameservers()).values()));
// Load the registrant and other contacts and add them to the data.
ImmutableSet<VKey<Contact>> contacts = domain.getReferencedContacts();
ImmutableMap<VKey<? extends Contact>, Contact> loadedContacts =
replicaTm().transact(() -> replicaTm().loadByKeysIfPresent(domain.getReferencedContacts()));
// RDAP Response Profile 2.7.3, A domain MUST have the REGISTRANT, ADMIN, TECH roles and MAY
// have others. We also add the BILLING.
//
contacts.isEmpty()
? ImmutableMap.of()
: replicaTm().transact(() -> replicaTm().loadByKeysIfPresent(contacts));
// RDAP Response Profile 2.7.1, 2.7.3 - we MUST have the contacts. 2.7.4 discusses redaction of
// fields we don't want to show (as opposed to not having contacts at all) because of GDPR etc.
//
// the GDPR redaction is handled in createRdapContactEntity
// The GDPR redaction is handled in createRdapContactEntity.
// Load all contacts that are present and group them by type (it is common for a single contact
// entity to be used across multiple contact types on domain, e.g. registrant and admin).
ImmutableSetMultimap<VKey<Contact>, Type> contactsToRoles =
Streams.concat(
domain.getContacts().stream(),
Stream.of(DesignatedContact.create(Type.REGISTRANT, domain.getRegistrant())))
domain.getAllContacts().stream()
.sorted(DESIGNATED_CONTACT_ORDERING)
.collect(
toImmutableSetMultimap(
DesignatedContact::getContactKey, DesignatedContact::getType));
// Convert the contact entities to RDAP output contacts (this also converts the contact types
// to RDAP roles).
Set<RdapContactEntity> rdapContacts = new LinkedHashSet<>();
for (VKey<Contact> contactKey : contactsToRoles.keySet()) {
Set<RdapEntity.Role> roles =
Set<Role> roles =
contactsToRoles.get(contactKey).stream()
.map(RdapJsonFormatter::convertContactTypeToRdapRole)
.collect(toImmutableSet());
if (roles.isEmpty()) {
continue;
}
builder
.entitiesBuilder()
.add(
createRdapContactEntity(
loadedContacts.get(contactKey), roles, OutputDataType.INTERNAL));
rdapContacts.add(
createRdapContactEntity(
Optional.ofNullable(loadedContacts.get(contactKey)), roles, OutputDataType.INTERNAL));
}
// Loop through all required contact roles and fill in placeholder REDACTED info for any
// required ones that are missing, i.e. because of minimum registration data set.
for (Role role : REQUIRED_CONTACT_ROLES) {
if (rdapContacts.stream().noneMatch(c -> c.roles().contains(role))) {
rdapContacts.add(
createRdapContactEntity(
Optional.empty(), ImmutableSet.of(role), OutputDataType.INTERNAL));
}
}
builder.entitiesBuilder().addAll(rdapContacts);
// Add the nameservers to the data; the load was kicked off above for efficiency.
// RDAP Response Profile 2.9: we MUST have the nameservers
for (Host host : HOST_RESOURCE_ORDERING.immutableSortedCopy(loadedHosts)) {
@@ -502,25 +529,36 @@ public class RdapJsonFormatter {
/**
* Creates a JSON object for a {@link Contact} and associated contact type.
*
* <p>If the contact isn't present (i.e. because of minimum registration data set), then always
* show all of its fields as if they were redacted, and always deny RDAP authorization.
*
* @param contact the contact resource object from which the JSON object should be created
* @param roles the roles of this contact
* @param outputDataType whether to generate full or summary data
*/
RdapContactEntity createRdapContactEntity(
Contact contact, Iterable<RdapEntity.Role> roles, OutputDataType outputDataType) {
Optional<Contact> contact, Iterable<RdapEntity.Role> roles, OutputDataType outputDataType) {
RdapContactEntity.Builder contactBuilder = RdapContactEntity.builder();
// RDAP Response Profile 2.7.1, 2.7.3 - we MUST have the contacts. 2.7.4 discusses censoring of
// fields we don't want to show (as opposed to not having contacts at all) because of GDPR etc.
//
// 2.8 allows for unredacted output for authorized people.
// TODO(mcilwain): Once the RDAP profile is fully updated for minimum registration data set,
// we will want to not include non-existent contacts at all, rather than
// pretending they exist and just showing REDACTED info. This is especially
// important for authorized flows, where you wouldn't expect to see redaction
// (although no one actually has access to authorized flows yet).
boolean isAuthorized =
rdapAuthorization.isAuthorizedForRegistrar(contact.getCurrentSponsorRegistrarId());
contact.isPresent()
&& rdapAuthorization.isAuthorizedForRegistrar(
contact.get().getCurrentSponsorRegistrarId());
VcardArray.Builder vcardBuilder = VcardArray.builder();
if (isAuthorized) {
fillRdapContactEntityWhenAuthorized(contactBuilder, vcardBuilder, contact, outputDataType);
fillRdapContactEntityWhenAuthorized(
contactBuilder, vcardBuilder, contact.get(), outputDataType);
} else {
// GTLD Registration Data Temp Spec 17may18, Appendix A, 2.3, 2.4 and RDAP Response Profile
// 2.7.4.1, 2.7.4.2 - the following fields must be redacted:
@@ -20,7 +20,6 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
import com.google.common.base.Ascii;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import google.registry.model.contact.Contact;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.Domain;
@@ -45,12 +44,11 @@ import google.registry.xjc.rgp.XjcRgpStatusType;
import google.registry.xjc.rgp.XjcRgpStatusValueType;
import google.registry.xjc.secdns.XjcSecdnsDsDataType;
import google.registry.xjc.secdns.XjcSecdnsDsOrKeyType;
import java.util.Optional;
/** Utility class that turns {@link Domain} as {@link XjcRdeDomainElement}. */
final class DomainToXjcConverter {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/** Converts {@link Domain} to {@link XjcRdeDomainElement}. */
static XjcRdeDomainElement convert(Domain domain, RdeMode mode) {
return new XjcRdeDomainElement(convertDomain(domain, mode));
@@ -168,11 +166,9 @@ final class DomainToXjcConverter {
// o An OPTIONAL <registrant> element that contain the identifier for
// the human or organizational social information object associated
// as the holder of the domain name object.
VKey<Contact> registrant = model.getRegistrant();
if (registrant == null) {
logger.atWarning().log("Domain %s has no registrant contact.", domainName);
} else {
Contact registrantContact = tm().transact(() -> tm().loadByKey(registrant));
Optional<VKey<Contact>> registrant = model.getRegistrant();
if (registrant.isPresent()) {
Contact registrantContact = tm().transact(() -> tm().loadByKey(registrant.get()));
checkState(
registrantContact != null,
"Registrant contact %s on domain %s does not exist",
@@ -26,9 +26,9 @@ import google.registry.gcs.GcsUtils;
import google.registry.groups.GmailClient;
import google.registry.reporting.billing.BillingModule.InvoiceDirectoryPrefix;
import google.registry.util.EmailMessage;
import jakarta.mail.internet.InternetAddress;
import java.util.Optional;
import javax.inject.Inject;
import javax.mail.internet.InternetAddress;
import org.joda.time.YearMonth;
/** Utility functions for sending emails involving monthly invoices. */
@@ -38,9 +38,9 @@ import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.util.EmailMessage;
import google.registry.util.Retrier;
import jakarta.mail.internet.InternetAddress;
import java.util.Optional;
import javax.inject.Inject;
import javax.mail.internet.InternetAddress;
import org.joda.time.Duration;
import org.joda.time.YearMonth;
import org.joda.time.format.DateTimeFormat;
@@ -42,13 +42,13 @@ import google.registry.request.lock.LockHandler;
import google.registry.util.Clock;
import google.registry.util.EmailMessage;
import google.registry.util.Retrier;
import jakarta.mail.internet.InternetAddress;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.mail.internet.InternetAddress;
import org.joda.time.DateTime;
import org.joda.time.Duration;
@@ -38,11 +38,11 @@ import google.registry.model.registrar.RegistrarPoc;
import google.registry.reporting.spec11.soy.Spec11EmailSoyInfo;
import google.registry.util.EmailMessage;
import google.registry.util.Sleeper;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.InternetAddress;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
import org.joda.time.Duration;
import org.joda.time.LocalDate;
@@ -16,11 +16,9 @@ package google.registry.storage.drive;
import com.google.api.client.http.ByteArrayContent;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.Drive.Children;
import com.google.api.services.drive.model.ChildList;
import com.google.api.services.drive.model.ChildReference;
import com.google.api.services.drive.Drive.Files;
import com.google.api.services.drive.model.File;
import com.google.api.services.drive.model.ParentReference;
import com.google.api.services.drive.model.FileList;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.net.MediaType;
@@ -37,16 +35,19 @@ public class DriveConnection {
/** Drive client instance wrapped by this class. */
@Inject Drive drive;
@Inject public DriveConnection() {}
@Inject
public DriveConnection() {}
/**
* Creates a folder with the given parent.
*
* @return the folder id.
*/
public String createFolder(String title, String parentFolderId) throws IOException {
return drive.files()
.insert(createFileReference(title, GOOGLE_FOLDER, parentFolderId))
public String createFolder(String name, @Nullable String parentFolderId) throws IOException {
return drive
.files()
.create(createFileReference(name, GOOGLE_FOLDER, parentFolderId))
.execute()
.getId();
}
@@ -60,11 +61,12 @@ public class DriveConnection {
*
* @return the file id.
*/
public String createFile(String title, MediaType mimeType, String parentFolderId, byte[] bytes)
public String createFile(String name, MediaType mimeType, String parentFolderId, byte[] bytes)
throws IOException {
return drive.files()
.insert(
createFileReference(title, mimeType, parentFolderId),
return drive
.files()
.create(
createFileReference(name, mimeType, parentFolderId),
new ByteArrayContent(mimeType.toString(), bytes))
.execute()
.getId();
@@ -72,79 +74,79 @@ public class DriveConnection {
/**
* Creates a file with the given parent or updates the existing one if a file already exists with
* that same title and parent.
* that same name and parent.
*
* @throws IllegalStateException if multiple files with that name exist in the given folder.
* @throws IOException if communication with Google Drive fails for any reason.
* @return the file id.
*/
public String createOrUpdateFile(
String title, MediaType mimeType, String parentFolderId, byte[] bytes) throws IOException {
List<String> existingFiles = listFiles(parentFolderId, String.format("title = '%s'", title));
String name, MediaType mimeType, String parentFolderId, byte[] bytes) throws IOException {
List<String> existingFiles = listFiles(parentFolderId, String.format("name = '%s'", name));
if (existingFiles.size() > 1) {
throw new IllegalStateException(String.format(
"Could not update file '%s' in Drive folder id '%s' because multiple files with that "
+ "name already exist.",
title,
parentFolderId));
throw new IllegalStateException(
String.format(
"Could not update file '%s' in Drive folder id '%s' because multiple files with that "
+ "name already exist.",
name, parentFolderId));
}
return existingFiles.isEmpty()
? createFile(title, mimeType, parentFolderId, bytes)
: updateFile(existingFiles.get(0), title, mimeType, bytes);
? createFile(name, mimeType, parentFolderId, bytes)
: updateFile(existingFiles.getFirst(), name, mimeType, bytes);
}
/**
* Updates the file with the given id in place, setting the title, content, and mime type to the
* Updates the file with the given id in place, setting the name, content, and mime type to the
* newly specified values.
*
* @return the file id.
*/
public String updateFile(String fileId, String title, MediaType mimeType, byte[] bytes)
public String updateFile(String fileId, String name, MediaType mimeType, byte[] bytes)
throws IOException {
File file = new File().setTitle(title);
return drive.files()
File file = new File().setName(name);
return drive
.files()
.update(fileId, file, new ByteArrayContent(mimeType.toString(), bytes))
.execute()
.getId();
}
/**
* Returns a list of Drive file ids for all files in Google Drive in the folder with the
* specified id.
* Returns a list of Drive file ids for all files in Google Drive in the folder with the specified
* id.
*/
public List<String> listFiles(String parentFolderId) throws IOException {
return listFiles(parentFolderId, null);
}
/**
* Returns a list of Drive file ids for all files in Google Drive in the folder with the
* specified id and matching the given Drive query.
* Returns a list of Drive file ids for all files in Google Drive in the folder with the specified
* id and matching the given Drive query.
*
* @see <a href="https://developers.google.com/drive/web/search-parameters">The query format</a>
*/
public List<String> listFiles(String parentFolderId, String query) throws IOException {
ImmutableList.Builder<String> result = new ImmutableList.Builder<>();
Children.List req = drive.children().list(parentFolderId);
Files.List req = drive.files().list();
StringBuilder q = new StringBuilder(String.format("'%s' in parents", parentFolderId));
if (!Strings.isNullOrEmpty(query)) {
req.setQ(query);
q.append(String.format(" and %s", query));
}
req.setQ(q.toString());
do {
ChildList files = req.execute();
for (ChildReference child : files.getItems()) {
result.add(child.getId());
}
FileList files = req.execute();
files.getFiles().forEach(file -> result.add(file.getId()));
req.setPageToken(files.getNextPageToken());
} while (!Strings.isNullOrEmpty(req.getPageToken()));
return result.build();
}
/** Constructs an object representing a file (or folder) with a given title and parent. */
/** Constructs an object representing a file (or folder) with a given name and parent. */
private File createFileReference(
String title, MediaType mimeType, @Nullable String parentFolderId) {
String name, MediaType mimeType, @Nullable String parentFolderId) {
return new File()
.setTitle(title)
.setName(name)
.setMimeType(mimeType.toString())
.setParents(parentFolderId == null
? null
: ImmutableList.of(new ParentReference().setId(parentFolderId)));
.setParents(parentFolderId == null ? null : ImmutableList.of(parentFolderId));
}
}
@@ -0,0 +1,55 @@
// 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.tools;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.model.common.FeatureFlag;
import google.registry.model.common.FeatureFlag.FeatureName;
import google.registry.model.common.FeatureFlag.FeatureStatus;
import google.registry.tools.params.TransitionListParameter.FeatureStatusTransitions;
import java.util.List;
import java.util.Optional;
import org.joda.time.DateTime;
/** Command for creating and updating {@link FeatureFlag} objects. */
@Parameters(separators = " =", commandDescription = "Create or update a feature flag.")
public class ConfigureFeatureFlagCommand extends MutatingCommand {
@Parameter(description = "Feature flag name(s) to create or update", required = true)
private List<FeatureName> mainParameters;
@Parameter(
names = "--status_map",
converter = FeatureStatusTransitions.class,
validateWith = FeatureStatusTransitions.class,
description =
"Comma-delimited list of feature status transitions effective on specific dates, of the"
+ " form <time>=<status>[,<time>=<status>]* where each status represents the status"
+ " of the feature flag.",
required = true)
private ImmutableSortedMap<DateTime, FeatureStatus> featureStatusTransitions;
@Override
protected void init() throws Exception {
for (FeatureName name : mainParameters) {
Optional<FeatureFlag> oldFlag = FeatureFlag.getUncached(name);
FeatureFlag.Builder newFlagBuilder =
new FeatureFlag().asBuilder().setFeatureName(name).setStatusMap(featureStatusTransitions);
stageEntityChange(oldFlag.orElse(null), newFlagBuilder.build());
}
}
}
@@ -111,8 +111,8 @@ abstract class CreateOrUpdateBulkPricingPackageCommand extends MutatingCommand {
if (clearLastNotificationSent()) {
builder.setLastNotificationSent(null);
}
BulkPricingPackage newBUlkPricingPackage = builder.build();
stageEntityChange(oldBulkPricingPackage, newBUlkPricingPackage);
BulkPricingPackage newBulkPricingPackage = builder.build();
stageEntityChange(oldBulkPricingPackage, newBulkPricingPackage);
});
}
}
@@ -15,30 +15,25 @@
package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.model.console.User.grantIapPermission;
import com.beust.jcommander.Parameters;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import google.registry.batch.CloudTasksUtils;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.tools.server.UpdateUserGroupAction;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.inject.Inject;
/** Command to create a new User. */
@Parameters(separators = " =", commandDescription = "Update a user account")
public class CreateUserCommand extends CreateOrUpdateUserCommand implements CommandWithConnection {
static final String IAP_SECURED_WEB_APP_USER_ROLE = "roles/iap.httpsResourceAccessor";
static final FluentLogger logger = FluentLogger.forEnclosingClass();
private ServiceConnection connection;
public class CreateUserCommand extends CreateOrUpdateUserCommand {
@Inject IamClient iamClient;
@Inject CloudTasksUtils cloudTasksUtils;
@Inject
@Config("gSuiteConsoleUserGroupEmailAddress")
Optional<String> maybeGroupEmailAddress;
@@ -53,29 +48,7 @@ public class CreateUserCommand extends CreateOrUpdateUserCommand implements Comm
@Override
protected String execute() throws Exception {
String ret = super.execute();
String groupEmailAddress = maybeGroupEmailAddress.orElse(null);
if (groupEmailAddress != null) {
logger.atInfo().log("Adding %s to group %s", email, groupEmailAddress);
connection.sendPostRequest(
UpdateUserGroupAction.PATH,
ImmutableMap.of(
"userEmailAddress",
email,
"groupEmailAddress",
groupEmailAddress,
"groupUpdateMode",
"ADD"),
MediaType.PLAIN_TEXT_UTF_8,
new byte[0]);
} else {
logger.atInfo().log("Granting IAP role to user %s", email);
iamClient.addBinding(email, IAP_SECURED_WEB_APP_USER_ROLE);
}
grantIapPermission(email, maybeGroupEmailAddress, cloudTasksUtils, iamClient);
return ret;
}
@Override
public void setConnection(ServiceConnection connection) {
this.connection = connection;
}
}
@@ -15,32 +15,27 @@
package google.registry.tools;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.tools.CreateUserCommand.IAP_SECURED_WEB_APP_USER_ROLE;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import google.registry.batch.CloudTasksUtils;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.tools.server.UpdateUserGroupAction;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.inject.Inject;
/** Deletes a {@link User}. */
@Parameters(separators = " =", commandDescription = "Delete a user account")
public class DeleteUserCommand extends ConfirmingCommand implements CommandWithConnection {
public class DeleteUserCommand extends ConfirmingCommand {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private ServiceConnection connection;
@Inject IamClient iamClient;
@Inject CloudTasksUtils cloudTasksUtils;
@Inject
@Config("gSuiteConsoleUserGroupEmailAddress")
Optional<String> maybeGroupEmailAddress;
@@ -49,11 +44,6 @@ public class DeleteUserCommand extends ConfirmingCommand implements CommandWithC
@Parameter(names = "--email", description = "Email address of the user", required = true)
String email;
@Override
public void setConnection(ServiceConnection connection) {
this.connection = connection;
}
@Override
protected String prompt() {
checkArgumentNotNull(email, "Email must be provided");
@@ -69,24 +59,7 @@ public class DeleteUserCommand extends ConfirmingCommand implements CommandWithC
checkArgumentPresent(optionalUser, "Email no longer corresponds to a valid user");
tm().delete(optionalUser.get());
});
String groupEmailAddress = maybeGroupEmailAddress.orElse(null);
if (groupEmailAddress != null) {
logger.atInfo().log("Removing %s from group %s", email, groupEmailAddress);
connection.sendPostRequest(
UpdateUserGroupAction.PATH,
ImmutableMap.of(
"userEmailAddress",
email,
"groupEmailAddress",
groupEmailAddress,
"groupUpdateMode",
"REMOVE"),
MediaType.PLAIN_TEXT_UTF_8,
new byte[0]);
} else {
logger.atInfo().log("Removing IAP role from user %s", email);
iamClient.removeBinding(email, IAP_SECURED_WEB_APP_USER_ROLE);
}
User.revokeIapPermission(email, maybeGroupEmailAddress, cloudTasksUtils, iamClient);
return String.format("Deleted user with email %s", email);
}
}
@@ -30,6 +30,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.beust.jcommander.internal.Nullable;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
@@ -62,6 +63,7 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.inject.Inject;
import javax.inject.Named;
import org.joda.money.Money;
import org.joda.time.DateTime;
/** Command to generate and persist {@link AllocationToken}s. */
@@ -158,10 +160,16 @@ class GenerateAllocationTokensCommand implements Command {
"The type of renewal price behavior, either DEFAULT (default), NONPREMIUM, or SPECIFIED."
+ " This indicates how a domain should be charged for renewal. By default, a domain"
+ " will be renewed at the renewal price from the pricing engine. If the renewal"
+ " price behavior is set to SPECIFIED, it means that the renewal cost will be the"
+ " same as the domain's calculated create price.")
+ " price behavior is set to SPECIFIED, it means that the renewal cost will be equal"
+ " to the provided renewal price.")
private RenewalPriceBehavior renewalPriceBehavior = DEFAULT;
@Parameter(
names = {"--renewal_price"},
description = "The renewal price amount iff the renewal price behavior is SPECIFIED.")
@Nullable
private Money renewalPrice;
@Parameter(
names = {"--registration_behavior"},
description =
@@ -228,6 +236,7 @@ class GenerateAllocationTokensCommand implements Command {
Optional.ofNullable(discountYears).ifPresent(token::setDiscountYears);
Optional.ofNullable(tokenStatusTransitions)
.ifPresent(token::setTokenStatusTransitions);
Optional.ofNullable(renewalPrice).ifPresent(token::setRenewalPrice);
Optional.ofNullable(domainNames)
.ifPresent(d -> token.setDomainName(d.removeFirst()));
return token.build();
@@ -295,6 +304,10 @@ class GenerateAllocationTokensCommand implements Command {
+ " map");
}
checkArgument(
renewalPriceBehavior.equals(RenewalPriceBehavior.SPECIFIED) == (renewalPrice != null),
"renewal_price must be specified iff renewal_price_behavior is SPECIFIED");
if (tokenStrings != null) {
verifyTokenStringsDoNotExist();
}
@@ -0,0 +1,53 @@
// 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.tools;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.fasterxml.jackson.databind.ObjectMapper;
import google.registry.model.common.FeatureFlag;
import google.registry.model.common.FeatureFlag.FeatureFlagNotFoundException;
import google.registry.model.common.FeatureFlag.FeatureName;
import java.io.PrintStream;
import java.util.List;
import java.util.Optional;
import javax.inject.Inject;
/** Command to show a {@link FeatureFlag}. */
@Parameters(separators = " =", commandDescription = "Show FeatureFlag record(s)")
public class GetFeatureFlagCommand implements Command {
@Parameter(description = "Feature flag(s) to show", required = true)
private List<FeatureName> mainParameters;
@Inject ObjectMapper objectMapper;
@Override
public void run() throws Exception {
// Don't use try-with-resources to manage standard output streams, closing the stream will
// cause subsequent output to standard output or standard error to be lost
// See: https://errorprone.info/bugpattern/ClosingStandardOutputStreams
PrintStream printStream = new PrintStream(System.out, false, UTF_8);
for (FeatureName featureFlag : mainParameters) {
Optional<FeatureFlag> maybeFeatureFlag = FeatureFlag.getUncached(featureFlag);
if (maybeFeatureFlag.isEmpty()) {
throw new FeatureFlagNotFoundException(featureFlag);
}
printStream.println(objectMapper.writeValueAsString(maybeFeatureFlag.get()));
}
}
}
@@ -20,7 +20,7 @@ import com.google.api.services.cloudresourcemanager.model.GetIamPolicyRequest;
import com.google.api.services.cloudresourcemanager.model.Policy;
import com.google.api.services.cloudresourcemanager.model.SetIamPolicyRequest;
import com.google.common.base.Ascii;
import google.registry.config.CredentialModule.LocalCredential;
import google.registry.config.CredentialModule.ApplicationDefaultCredential;
import google.registry.config.RegistryConfig.Config;
import google.registry.util.GoogleCredentialsBundle;
import java.io.IOException;
@@ -38,7 +38,7 @@ public class IamClient {
@Inject
public IamClient(
@LocalCredential GoogleCredentialsBundle credentialsBundle,
@ApplicationDefaultCredential GoogleCredentialsBundle credentialsBundle,
@Config("projectId") String projectId) {
this(
new CloudResourceManager.Builder(
@@ -0,0 +1,43 @@
// 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.tools;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.beust.jcommander.Parameters;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableList;
import google.registry.model.common.FeatureFlag;
import java.io.PrintStream;
import javax.inject.Inject;
/** Command to list all {@link google.registry.model.common.FeatureFlag} objects. */
@Parameters(separators = " =", commandDescription = "List all feature flags.")
public class ListFeatureFlagsCommand implements Command {
@Inject ObjectMapper mapper;
@Override
public void run() throws Exception {
// Don't use try-with-resources to manage standard output streams, closing the stream will
// cause subsequent output to standard output or standard error to be lost
// See: https://errorprone.info/bugpattern/ClosingStandardOutputStreams
PrintStream printStream = new PrintStream(System.out, false, UTF_8);
ImmutableList<FeatureFlag> featureFlags = FeatureFlag.getAllUncached();
for (FeatureFlag featureFlag : featureFlags) {
printStream.println(mapper.writeValueAsString(featureFlag));
}
}
}
@@ -33,6 +33,7 @@ public final class RegistryTool {
.put("canonicalize_labels", CanonicalizeLabelsCommand.class)
.put("check_domain", CheckDomainCommand.class)
.put("check_domain_claims", CheckDomainClaimsCommand.class)
.put("configure_feature_flag", ConfigureFeatureFlagCommand.class)
.put("configure_tld", ConfigureTldCommand.class)
.put("convert_idn", ConvertIdnCommand.class)
.put("count_domains", CountDomainsCommand.class)
@@ -71,6 +72,7 @@ public final class RegistryTool {
.put("get_claims_list", GetClaimsListCommand.class)
.put("get_contact", GetContactCommand.class)
.put("get_domain", GetDomainCommand.class)
.put("get_feature_flag", GetFeatureFlagCommand.class)
.put("get_history_entries", GetHistoryEntriesCommand.class)
.put("get_host", GetHostCommand.class)
.put("get_keyring_secret", GetKeyringSecretCommand.class)
@@ -85,6 +87,7 @@ public final class RegistryTool {
.put("hash_certificate", HashCertificateCommand.class)
.put("list_cursors", ListCursorsCommand.class)
.put("list_domains", ListDomainsCommand.class)
.put("list_feature_flags", ListFeatureFlagsCommand.class)
.put("list_hosts", ListHostsCommand.class)
.put("list_premium_lists", ListPremiumListsCommand.class)
.put("list_registrars", ListRegistrarsCommand.class)
@@ -119,6 +119,8 @@ interface RegistryToolComponent {
void inject(GetDomainCommand command);
void inject(GetFeatureFlagCommand command);
void inject(GetHostCommand command);
void inject(GetKeyringSecretCommand command);
@@ -131,6 +133,8 @@ interface RegistryToolComponent {
void inject(ListCursorsCommand command);
void inject(ListFeatureFlagsCommand command);
void inject(LockDomainCommand command);
void inject(LoginCommand command);
@@ -17,7 +17,7 @@ package google.registry.tools;
import com.google.api.services.dataflow.Dataflow;
import dagger.Module;
import dagger.Provides;
import google.registry.config.CredentialModule.LocalCredential;
import google.registry.config.CredentialModule.ApplicationDefaultCredential;
import google.registry.config.RegistryConfig.Config;
import google.registry.util.GoogleCredentialsBundle;
@@ -27,7 +27,7 @@ public class RegistryToolDataflowModule {
@Provides
static Dataflow provideDataflow(
@LocalCredential GoogleCredentialsBundle credentialsBundle,
@ApplicationDefaultCredential GoogleCredentialsBundle credentialsBundle,
@Config("projectId") String projectId) {
return new Dataflow.Builder(
credentialsBundle.getHttpTransport(),
@@ -22,6 +22,8 @@ import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.MoreFiles;
import google.registry.batch.CloudTasksUtils;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.OteAccountBuilder;
import google.registry.tools.params.PathParameter;
import google.registry.util.Clock;
@@ -30,6 +32,7 @@ import google.registry.util.StringGenerator;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Named;
@@ -47,7 +50,7 @@ final class SetupOteCommand extends ConfirmingCommand {
@Parameter(
names = {"-a", "--ip_allow_list"},
description = "Comma-separated list of IP addreses or CIDR ranges.",
description = "Comma-separated list of IP addresses or CIDR ranges.",
required = true)
private List<String> ipAllowList = new ArrayList<>();
@@ -55,7 +58,7 @@ final class SetupOteCommand extends ConfirmingCommand {
names = {"--email"},
description =
"The registrar's account to use for console access. "
+ "Must be on the registry's G Suite domain.",
+ "Must be on the registry's Google Workspace domain.",
required = true)
private String email;
@@ -76,6 +79,14 @@ final class SetupOteCommand extends ConfirmingCommand {
@Inject Clock clock;
@Inject CloudTasksUtils cloudTasksUtils;
@Inject IamClient iamClient;
@Inject
@Config("gSuiteConsoleUserGroupEmailAddress")
Optional<String> maybeGroupEmailAddress;
OteAccountBuilder oteAccountBuilder;
String password;
@@ -87,7 +98,7 @@ final class SetupOteCommand extends ConfirmingCommand {
password = passwordGenerator.createString(PASSWORD_LENGTH);
oteAccountBuilder =
OteAccountBuilder.forRegistrarId(registrar)
.addContact(email)
.addUser(email)
.setPassword(password)
.setIpAllowList(ipAllowList)
.setReplaceExisting(overwrite);
@@ -114,8 +125,11 @@ final class SetupOteCommand extends ConfirmingCommand {
&& RegistryEnvironment.get() != RegistryEnvironment.UNITTEST) {
builder.append(
String.format(
"\n\nWARNING: Running against %s environment. Are "
+ "you sure you didn\'t mean to run this against sandbox (e.g. \"-e SANDBOX\")?",
"""
WARNING: Running against %s environment. Are \
you sure you didn't mean to run this against sandbox (e.g. "-e SANDBOX")?""",
RegistryEnvironment.get()));
}
@@ -125,15 +139,15 @@ final class SetupOteCommand extends ConfirmingCommand {
@Override
public String execute() {
ImmutableMap<String, String> clientIdToTld = oteAccountBuilder.buildAndPersist();
oteAccountBuilder.grantIapPermission(maybeGroupEmailAddress, cloudTasksUtils, iamClient);
StringBuilder output = new StringBuilder();
output.append("Copy these usernames/passwords back into the onboarding bug:\n\n");
clientIdToTld.forEach(
(clientId, tld) -> {
output.append(
String.format("Login: %s\nPassword: %s\nTLD: %s\n\n", clientId, password, tld));
});
(clientId, tld) ->
output.append(
String.format("Login: %s\nPassword: %s\nTLD: %s\n\n", clientId, password, tld)));
return output.toString();
}
@@ -40,6 +40,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import org.joda.money.Money;
import org.joda.time.DateTime;
/** Command to update existing {@link AllocationToken}s. */
@@ -110,11 +111,17 @@ final class UpdateAllocationTokensCommand extends UpdateOrDeleteAllocationTokens
"The type of renewal price behavior, either DEFAULT (default), NONPREMIUM, or SPECIFIED."
+ " This indicates how a domain should be charged for renewal. By default, a domain"
+ " will be renewed at the renewal price from the pricing engine. If the renewal"
+ " price behavior is set to SPECIFIED, it means that the renewal cost will be the"
+ " same as the domain's calculated create price.")
+ " price behavior is set to SPECIFIED, it means that the renewal cost will be equal"
+ " to the provided renewal price.")
@Nullable
private RenewalPriceBehavior renewalPriceBehavior;
@Parameter(
names = {"--renewal_price"},
description = "The renewal price amount iff the renewal price behavior is SPECIFIED.")
@Nullable
private Money renewalPrice;
@Parameter(
names = {"--registration_behavior"},
description =
@@ -189,17 +196,22 @@ final class UpdateAllocationTokensCommand extends UpdateOrDeleteAllocationTokens
.ifPresent(tlds -> builder.setAllowedTlds(ImmutableSet.copyOf(tlds)));
Optional.ofNullable(allowedEppActions)
.ifPresent(
eppActions -> {
builder.setAllowedEppActions(
eppActions.stream()
.map(CommandName::parseKnownCommand)
.collect(toImmutableSet()));
});
eppActions ->
builder.setAllowedEppActions(
eppActions.stream()
.map(CommandName::parseKnownCommand)
.collect(toImmutableSet())));
Optional.ofNullable(discountFraction).ifPresent(builder::setDiscountFraction);
Optional.ofNullable(discountPremiums).ifPresent(builder::setDiscountPremiums);
Optional.ofNullable(discountYears).ifPresent(builder::setDiscountYears);
Optional.ofNullable(tokenStatusTransitions).ifPresent(builder::setTokenStatusTransitions);
if (renewalPriceBehavior != null
&& renewalPriceBehavior != original.getRenewalPriceBehavior()) {
builder.setRenewalPrice(null);
}
Optional.ofNullable(renewalPriceBehavior).ifPresent(builder::setRenewalPriceBehavior);
Optional.ofNullable(renewalPrice).ifPresent(builder::setRenewalPrice);
Optional.ofNullable(registrationBehavior).ifPresent(builder::setRegistrationBehavior);
return builder.build();
}

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