1
0
mirror of https://github.com/google/nomulus synced 2026-06-09 16:33:02 +00:00

Compare commits

...

36 Commits

Author SHA1 Message Date
Pavlo Tkach ab5f6cc229 Add environment support to the console build (#2539) 2024-08-30 18:31:28 +00:00
gbrodman 1765f4f0b4 Allow skip of emailing/uploading for activity reports (#2538)
This will help us if/when we need to run the report generation multiple
times, or for past dates and we don't want to send extra emails or
upload any extra reports to ICANN.
2024-08-26 20:25:31 +00:00
gbrodman e88c6e1550 Update activity/txn reporting to use new GAE log format (#2535)
Instead of having to parse the protoPayload.line from the request logs,
we just want to inspect the textPayload from the app logs (stored in a
separate table). This applies to the EPP metrics from the activity
reporting and the attempted-adds column for the transaction reporting.
2024-08-26 19:41:40 +00:00
Pavlo Tkach 1739c6d74f Update node.js to v22 (#2537) 2024-08-26 18:15:39 +00:00
Pavlo Tkach 66513a114e Add OT&E UI to the new console (#2536) 2024-08-23 20:53:45 +00:00
Pavlo Tkach 0e808a4c01 Add OT&E create and status to the new console (#2534) 2024-08-22 20:03:56 +00:00
Lai Jiang 4e013603be Make GKE networking work more properly (#2531) 2024-08-22 13:10:56 +00:00
gbrodman 730585cd14 Fix front-end unit tests (#2529)
This doesn't really add any tests, and we'll require many more additions
if we actually want to have full unit testing, but this at least makes
the tests pass when running `npm test`.
2024-08-21 16:39:29 +00:00
gbrodman fd7820759d Use token's renewalPrice if renewalBehavior is SPECIFIED (#2502)
Previous PRs and token changes (see b/332928676) have made it so that
SPECIFIED renewalPriceBehavior tokens must have a renewal price. As
such, we can now use that renewalPrice when creating domains with
SPECIFIED tokens.
2024-08-15 19:06:32 +00:00
sarahcaseybot 69359bb1e6 Add QPS and incomplete connections metrics to load test client (#2487)
* Add QPS and incomplete connections metrics to load test client

* Add a failed request count

* Add todos

* Reuse contact

* Add bugs to todos

* small fix

* Clarify QPS
2024-08-14 18:14:17 +00:00
gbrodman 35b602a76e Remove User ID field from SQL (#2523)
This will fail tests until the corresponding PR in Java is deployed.
2024-08-14 17:51:15 +00:00
dependabot[bot] 82002d1f75 Bump axios in /console-webapp in the npm_and_yarn group (#2532)
Bumps the npm_and_yarn group in /console-webapp with 1 update: [axios](https://github.com/axios/axios).


Updates `axios` from 1.7.2 to 1.7.4
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.7.2...v1.7.4)

---
updated-dependencies:
- dependency-name: axios
  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-08-14 15:44:14 +00:00
Lai Jiang 2fd9b062df Make Nomulus work on GKE with external load balancer (#2527)
This will create a multi-cluster external load balancer exposing HTTP
traffic to nomulus running in clusters in the fleet.
2024-08-14 14:32:39 +00:00
Ben McIlwain ec3804e87e Make domain update flow handle null auth data (#2530)
It's valid for the auth data to be null (although it only happens 10 times
across our entire registry), so the domain update flow should not fail out with
a NullPointerException when the existing state of the data is null and the
update isn't adding that data either.

BUG=http://b/359264787
2024-08-13 18:19:44 +00:00
Pavlo Tkach d0d28cc7e6 Fix console contact delete button not working (#2528) 2024-08-09 16:42:39 +00:00
Pavlo Tkach 2d1260c01b Allow updating icannReferralEmail through the new console ui (#2525) 2024-08-07 16:28:08 +00:00
Pavlo Tkach 06da6a2cc6 Make ContactActionTest deterministic for stop fail under new Hibernate (#2524) 2024-08-07 13:37:13 +00:00
Lai Jiang 858a22f82e Delete a duplicate resource file (#2522)
It already exists under the resources folder.
2024-08-06 18:42:29 +00:00
gbrodman 3c126ddfd4 Remove ID field from User in Java classes and remove UserDao (#2517)
This is the first step in the field removal (second will be removing the
column from SQL once this is deployed).

There's no point in using a UserDao versus just doing the standard
loading-from-DB that we do everywhere else. No need to special-case it.
2024-08-05 20:36:17 +00:00
gbrodman 2b98e6f177 Add deprecation message to old console (#2516) 2024-08-02 15:59:08 +00:00
gbrodman 20036b6a74 Fix wording on registry lock verification (#2518) 2024-08-01 20:17:46 +00:00
Lai Jiang 396cbd6bd3 Remove login_email_address from RegistrarPoc (part 2) (#2510)
Remove the field from the schema.
2024-08-01 17:07:03 +00:00
Lai Jiang 71ea16ff69 Call Workspace Groups API directly from nomulus tool (#2515)
When creating/deleting users, we need to add/remove the emails in
question to/from the console email group (if it exists). This used to be
done synchronously by calling the Groups API directly from the nomulus
tool. However #2488 made it so that in all cases where group membership
is modified, a Cloud Tasks task is created to execute the change on
the server side asynchronously (because there are multiple places where
this change needs to be done, and it is easier to make it all happen on the
server side).

Alas, as it turns out, Cloud Tasks tasks need to be created with a
service account's credential (which is trivially done on the server side
because the ADC is a service account). Nomulus command runs with a user
credential, and we need to grant the relevant user permission to
masquerade as a service account, in order to enqueue tasks from the
nomulus tool. It is therefore easier to just revert to the old behavior.
2024-08-01 15:29:57 +00:00
Pavlo Tkach 45331be166 Add redirect to the new console from the old console for tech support (#2514) 2024-07-31 17:16:12 +00:00
gbrodman beb7c14adb Drop not-null constraint on UserUpdateHistory:user_id (#2513)
Dependency Submission / dependency-submission (push) Successful in 3m55s
CodeQL / Analyze (java) (push) Failing after 3m42s
CodeQL / Analyze (javascript) (push) Failing after 52s
CodeQL / Analyze (python) (push) Failing after 50s
This is nullable now that we're switching from using an ID field to
using the email address as the primary identifier.
2024-07-30 19:19:29 +00:00
gbrodman d33571dde3 Change pkey of User to emailAddress (#2505)
CodeQL / Analyze (java) (push) Failing after 1m22s
CodeQL / Analyze (javascript) (push) Failing after 1m13s
CodeQL / Analyze (python) (push) Failing after 51s
Dependency Submission / dependency-submission (push) Successful in 2m11s
Originally, we though that User entities were going to have mutable
email addresses, and thus would require a non-changing primary key. This
proved to not be the case. It'll simplify the User loading/saving code
if we just do everything by email address.

Obviously this doesn't change much functionality, but it prepares us for
removing the id field down the line once the changes propagate.
2024-07-29 22:27:06 +00:00
gbrodman 53a7d1b66c Add feature flag for new console release #2511 (#2512)
* Add feature flag for new console release

* Run feature flag query in a transaction

---------

Co-authored-by: ptkach <ptkach@google.com>
2024-07-29 21:55:12 +00:00
Pavlo Tkach fa721e82ff Mark console state field on new registrar form as required (#2509)
CodeQL / Analyze (java) (push) Failing after 58s
CodeQL / Analyze (javascript) (push) Failing after 56s
CodeQL / Analyze (python) (push) Failing after 53s
Dependency Submission / dependency-submission (push) Successful in 2m12s
2024-07-26 18:46:43 +00:00
Lai Jiang d4faa77ee4 Remove login_email_address from RegistrarPoc (#2507) 2024-07-26 17:56:34 +00:00
sarahcaseybot 96d3d88c2f Remove TODOs assigned to sarahbot (#2508) 2024-07-26 17:50:35 +00:00
Pavlo Tkach 213e06f02e Add registry lock ui (#2500) 2024-07-26 16:02:19 +00:00
gbrodman d5445dd049 Allow new-console use for users with perm to the admin registrar ID (#2506)
For instance, on sandbox this will allow us to remove our global roles
but keep roles to the CharlestonRoad admin registrar. Then, when we view
the console, it will be as if we were a registrar user.
2024-07-25 20:25:55 +00:00
Lai Jiang af5adcb0ba Upgrade to Gradle 8.9 (#2504) 2024-07-25 19:01:06 +00:00
gbrodman ca238a8578 Change RL input to be a POST body (#2503) 2024-07-25 18:18:10 +00:00
Pavlo Tkach 1a8f133d54 Filter console registrars per user role (#2501) 2024-07-24 18:31:23 +00:00
gbrodman 233ee09efe Add simple registry-lock-verification page (#2499)
This is a fairly simple page that solely exists to display the result
from the action, and to link the user back to the domain list.
2024-07-23 19:04:35 +00:00
253 changed files with 6629 additions and 4710 deletions
+2 -1
View File
@@ -60,7 +60,7 @@ dependencyLocking {
node {
download = false
version = "20.10.0"
version = "22.7.0"
}
wrapper {
@@ -119,6 +119,7 @@ if (environment == '') {
rootProject.ext.environment = environment
rootProject.ext.gcpProject = gcpProject
rootProject.ext.baseDomain = baseDomains[environment]
rootProject.ext.prodOrSandboxEnv = environment in ['production', 'sandbox']
// Function to verify that the deployment parameters have been set.
+1 -1
View File
@@ -13,7 +13,7 @@ Webapp is deployed with the nomulus default service war to Google App Engine.
During nomulus default service war build task, gradle script triggers the
following:
1) Console webapp build script `buildConsoleWebappProd`, which installs
1) Console webapp build script `buildConsoleWebapp`, which installs
dependencies, assembles a compiled ts -> js, minified, optimized static
artifact (html, css, js)
2) Artifact assembled in step 1 then gets copied to core project web artifact
+42
View File
@@ -63,6 +63,39 @@
],
"outputHashing": "all"
},
"sandbox": {
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.sandbox.ts"
}
],
"outputHashing": "all"
},
"crash": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
},
"alpha": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
},
"development": {
"optimization": false,
"extractLicenses": false,
@@ -78,6 +111,15 @@
"production": {
"buildTarget": "console-webapp:build:production"
},
"alpha": {
"buildTarget": "console-webapp:build:alpha"
},
"crash": {
"buildTarget": "console-webapp:build:crash"
},
"sandbox": {
"buildTarget": "console-webapp:build:sandbox"
},
"development": {
"buildTarget": "console-webapp:build:development"
}
+12 -11
View File
@@ -37,17 +37,16 @@ task runConsoleWebappUnitTests(type: Exec) {
args 'run', 'test'
}
task buildConsoleWebappNonProd(type: Exec) {
task buildConsoleWebapp(type: Exec) {
workingDir "${consoleDir}/"
executable 'npm'
args 'run', 'build'
}
// Keeping the same as non prod for now before we figure out optimization we want to include
task buildConsoleWebappProd(type: Exec) {
workingDir "${consoleDir}/"
executable 'npm'
args 'run', 'build'
def configuration = project.hasProperty('configuration') ?
project.getProperty('configuration') :
'production'
args 'run', "build", "--configuration=${configuration}"
doFirst {
println "Building console for environment: ${configuration}"
}
}
task applyFormatting(type: Exec) {
@@ -68,8 +67,10 @@ task deploy(type: Exec) {
args 'app', 'deploy', "${projectParam}", '--quiet'
}
tasks.buildConsoleWebappProd.dependsOn(tasks.npmInstallDeps)
tasks.buildConsoleWebapp.dependsOn(tasks.npmInstallDeps)
tasks.runConsoleWebappUnitTests.dependsOn(tasks.npmInstallDeps)
tasks.applyFormatting.dependsOn(tasks.npmInstallDeps)
tasks.checkFormatting.dependsOn(tasks.npmInstallDeps)
tasks.build.dependsOn(tasks.checkFormatting)
tasks.deploy.dependsOn(tasks.buildConsoleWebappProd)
tasks.build.dependsOn(tasks.runConsoleWebappUnitTests)
tasks.deploy.dependsOn(tasks.buildConsoleWebapp)
+11
View File
@@ -3,6 +3,17 @@
module.exports = function (config) {
config.set({
customLaunchers: {
ChromeHeadless: {
base: 'Chrome',
flags: [
'--no-sandbox',
'--disable-gpu',
'--headless',
'--remote-debugging-port=9222'
]
}
},
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
+3 -3
View File
@@ -6451,9 +6451,9 @@
}
},
"node_modules/axios": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
"dev": true,
"dependencies": {
"follow-redirects": "^1.15.6",
+3 -3
View File
@@ -4,9 +4,9 @@
"scripts": {
"ng": "ng",
"start": "ng serve --proxy-config dev-proxy.config.json",
"build": "ng build --base-href=/console/",
"build": "ng build --base-href=/console/ --configuration=$npm_config_configuration",
"build:local": "ng build --base-href=/default/console/",
"watch": "ng build --watch --configuration development",
"watch": "ng build --watch --configuration=development",
"test": "ng test --browsers=ChromeHeadless --watch=false",
"run:dev": "",
"prettify": "npx prettier --write ./src/",
@@ -54,4 +54,4 @@
"prettier": "2.8.7",
"typescript": "~5.4.5"
}
}
}
@@ -17,6 +17,9 @@ import { Route, RouterModule } from '@angular/router';
import { BillingInfoComponent } from './billingInfo/billingInfo.component';
import { DomainListComponent } from './domains/domainList.component';
import { HomeComponent } from './home/home.component';
import { RegistryLockVerifyComponent } from './lock/registryLockVerify.component';
import { NewOteComponent } from './ote/newOte.component';
import { OteStatusComponent } from './ote/oteStatus.component';
import { RegistrarDetailsComponent } from './registrar/registrarDetails.component';
import { RegistrarComponent } from './registrar/registrarsTable.component';
import { ResourcesComponent } from './resources/resources.component';
@@ -31,8 +34,26 @@ export interface RouteWithIcon extends Route {
iconName?: string;
}
export const PATHS = {
NewOteComponent: 'new-ote',
OteStatusComponent: 'ote-status/:registrarId',
};
export const routes: RouteWithIcon[] = [
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{
path: RegistryLockVerifyComponent.PATH,
component: RegistryLockVerifyComponent,
},
{
path: PATHS.NewOteComponent,
loadComponent: () =>
import('./ote/newOte.component').then((mod) => mod.NewOteComponent),
},
{
path: PATHS.OteStatusComponent,
loadComponent: () =>
import('./ote/oteStatus.component').then((mod) => mod.OteStatusComponent),
},
{ path: 'registrars', component: RegistrarComponent },
{
path: 'home',
+2 -2
View File
@@ -16,16 +16,16 @@ 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 { MaterialModule } from './material.module';
import { BackendService } from './shared/services/backend.service';
import { AppRoutingModule } from './app-routing.module';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AppComponent],
imports: [RouterTestingModule, MaterialModule, BrowserAnimationsModule],
imports: [MaterialModule, BrowserAnimationsModule, AppRoutingModule],
providers: [
BackendService,
provideHttpClient(),
+7
View File
@@ -27,9 +27,13 @@ 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 { RegistryLockComponent } from './domains/registryLock.component';
import { HeaderComponent } from './header/header.component';
import { HomeComponent } from './home/home.component';
import { RegistryLockVerifyComponent } from './lock/registryLockVerify.component';
import { NavigationComponent } from './navigation/navigation.component';
import { NewOteComponent } from './ote/newOte.component';
import { OteStatusComponent } from './ote/oteStatus.component';
import NewRegistrarComponent from './registrar/newRegistrar.component';
import { RegistrarDetailsComponent } from './registrar/registrarDetails.component';
import { RegistrarSelectorComponent } from './registrar/registrarSelector.component';
@@ -70,7 +74,9 @@ import { TldsComponent } from './tlds/tlds.component';
NotificationsComponent,
RegistrarComponent,
RegistrarDetailsComponent,
RegistryLockComponent,
RegistrarSelectorComponent,
RegistryLockVerifyComponent,
ResourcesComponent,
SecurityComponent,
SecurityEditComponent,
@@ -91,6 +97,7 @@ import { TldsComponent } from './tlds/tlds.component';
MaterialModule,
SnackBarModule,
],
exports: [SelectedRegistrarWrapper],
providers: [
BackendService,
BreakPointObserverService,
@@ -2,6 +2,17 @@
<div class="console-app-domains">
<h1 class="mat-headline-4">Domains</h1>
<div
class="console-app-domains__actions-wrapper"
[hidden]="!domainListService.activeActionComponent"
>
<ng-container
v-if="domainListService.activeActionComponent"
*ngComponentOutlet="domainListService.activeActionComponent"
>
</ng-container>
</div>
@if (!isLoading && totalResults == 0) {
<div class="console-app__empty-domains">
<h1>
@@ -12,7 +23,18 @@
<h1>No domains found</h1>
</div>
} @else {
<div class="console-app__domains-table-parent">
<mat-menu #actions="matMenu">
<ng-template matMenuContent let-domainName="domainName">
<button mat-menu-item (click)="openRegistryLock(domainName)">
<mat-icon>key</mat-icon>
<span>Registry Lock</span>
</button>
</ng-template>
</mat-menu>
<div
class="console-app__domains-table-parent"
[hidden]="domainListService.activeActionComponent"
>
<div class="console-app__scrollable-wrapper">
<div class="console-app__scrollable">
@if (isLoading) {
@@ -78,6 +100,29 @@
}}</mat-cell>
</ng-container>
<ng-container matColumnDef="registryLock">
<mat-header-cell *matHeaderCellDef
>Registry-Locked</mat-header-cell
>
<mat-cell *matCellDef="let element">{{
isDomainLocked(element.domainName)
}}</mat-cell>
</ng-container>
<ng-container matColumnDef="actions">
<mat-header-cell *matHeaderCellDef>Actions</mat-header-cell>
<mat-cell *matCellDef="let element">
<button
mat-icon-button
[matMenuTriggerFor]="actions"
[matMenuTriggerData]="{ domainName: element.domainName }"
aria-label="Domain actions"
>
<mat-icon>more_horiz</mat-icon>
</button>
</mat-cell>
</ng-container>
<mat-header-row
*matHeaderRowDef="displayedColumns"
></mat-header-row>
@@ -85,7 +130,7 @@
<!-- Row shown when there is no matching data. -->
<mat-row *matNoDataRow>
<mat-cell colspan="4">No domains found</mat-cell>
<mat-cell colspan="6">No domains found</mat-cell>
</mat-row>
</mat-table>
<mat-paginator
@@ -30,6 +30,12 @@
&__domains-table {
min-width: $min-width !important;
.mat-column-actions {
max-width: 100px;
}
.mat-column-registryLock {
max-width: 150px;
}
}
&__domains-spinner {
@@ -20,6 +20,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MaterialModule } from '../material.module';
import { BackendService } from '../shared/services/backend.service';
import { DomainListComponent } from './domainList.component';
import { FormsModule } from '@angular/forms';
describe('DomainListComponent', () => {
let component: DomainListComponent;
@@ -28,7 +29,7 @@ describe('DomainListComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DomainListComponent],
imports: [MaterialModule, BrowserAnimationsModule],
imports: [MaterialModule, BrowserAnimationsModule, FormsModule],
providers: [
BackendService,
provideHttpClient(),
@@ -19,14 +19,14 @@ import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTableDataSource } from '@angular/material/table';
import { Subject, debounceTime } from 'rxjs';
import { RegistrarService } from '../registrar/registrar.service';
import { BackendService } from '../shared/services/backend.service';
import { Domain, DomainListService } from './domainList.service';
import { RegistryLockComponent } from './registryLock.component';
import { RegistryLockService } from './registryLock.service';
@Component({
selector: 'app-domain-list',
templateUrl: './domainList.component.html',
styleUrls: ['./domainList.component.scss'],
providers: [DomainListService],
})
export class DomainListComponent {
public static PATH = 'domain-list';
@@ -37,6 +37,8 @@ export class DomainListComponent {
'creationTime',
'registrationExpirationTime',
'statuses',
'registryLock',
'actions',
];
dataSource: MatTableDataSource<Domain> = new MatTableDataSource();
@@ -52,15 +54,16 @@ export class DomainListComponent {
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
constructor(
private backendService: BackendService,
private domainListService: DomainListService,
protected domainListService: DomainListService,
protected registrarService: RegistrarService,
protected registryLockService: RegistryLockService,
private _snackBar: MatSnackBar
) {
effect(() => {
this.pageNumber = 0;
this.totalResults = 0;
if (this.registrarService.registrarId()) {
this.loadLocks();
this.reloadData();
}
});
@@ -80,6 +83,25 @@ export class DomainListComponent {
this.searchTermSubject.complete();
}
openRegistryLock(domainName: string) {
this.domainListService.selectedDomain = domainName;
this.domainListService.activeActionComponent = RegistryLockComponent;
}
loadLocks() {
this.registryLockService.retrieveLocks().subscribe({
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.message);
},
});
}
isDomainLocked(domainName: string) {
return this.registryLockService.domainsLocks.some(
(d) => d.domainName === domainName
);
}
reloadData() {
this.isLoading = true;
this.domainListService
@@ -95,7 +117,7 @@ export class DomainListComponent {
this.isLoading = false;
},
next: (domainListResult) => {
this.dataSource.data = (domainListResult || {}).domains;
this.dataSource.data = this.domainListService.domainsList;
this.totalResults = (domainListResult || {}).totalResults || 0;
this.isLoading = false;
},
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { Injectable, Type } from '@angular/core';
import { tap } from 'rxjs';
import { RegistrarService } from '../registrar/registrar.service';
import { BackendService } from '../shared/services/backend.service';
@@ -35,9 +35,14 @@ export interface DomainListResult {
totalResults: number;
}
@Injectable()
@Injectable({
providedIn: 'root',
})
export class DomainListService {
checkpointTime?: string;
selectedDomain?: string;
public activeActionComponent: Type<any> | null = null;
public domainsList: Domain[] = [];
constructor(
private backendService: BackendService,
@@ -62,6 +67,7 @@ export class DomainListService {
.pipe(
tap((domainListResult: DomainListResult) => {
this.checkpointTime = domainListResult?.checkpointTime;
this.domainsList = domainListResult?.domains;
})
);
}
@@ -0,0 +1,81 @@
<div class="console-app__registry-lock">
<p>
<button
mat-icon-button
aria-label="Back to domains list"
(click)="goBack()"
>
<mat-icon>arrow_back</mat-icon>
</button>
</p>
@if(!registrarService.registrar()?.registryLockAllowed) {
<h1>
Sorry, your registrar hasn't enrolled in registry lock yet. To do so, please
contact {{ userDataService.userData()?.supportEmail }}.
</h1>
} @else if (isLocked()) {
<h1>Unlock the domain {{ domainListService.selectedDomain }}</h1>
<form (ngSubmit)="save(false)" [formGroup]="unlockDomain">
<p>
<mat-label for="password">Password: </mat-label>
<mat-form-field name="password" appearance="outline">
<input matInput type="text" formControlName="password" required />
</mat-form-field>
</p>
<p>
<mat-label for="relockTime"
>Automatically re-lock the domain after:</mat-label
>
<mat-radio-group
name="relockTime"
formControlName="relockTime"
aria-label="Automatically relock option"
>
@for (option of relockOptions; track option.name) {
<mat-radio-button [value]="option.duration">{{
option.name
}}</mat-radio-button>
}
</mat-radio-group>
</p>
<div class="console-app__registry-lock-notification">
<mat-icon>priority_high</mat-icon>Confirmation email will be sent to your
email address to confirm the unlock
</div>
<button
mat-flat-button
color="primary"
type="submit"
[disabled]="!unlockDomain.valid"
>
Save
</button>
</form>
} @else {
<h1>Lock the domain {{ domainListService.selectedDomain }}</h1>
<form (ngSubmit)="save(true)" [formGroup]="lockDomain">
<p>
<mat-label for="password">Password: </mat-label>
<mat-form-field name="password" appearance="outline">
<input matInput type="text" formControlName="password" required />
</mat-form-field>
</p>
<div class="console-app__registry-lock-notification">
<mat-icon>priority_high</mat-icon>The lock will not take effect until you
click the confirmation link that will be emailed to you. When it takes
effect, you will be billed the standard server status change billing cost.
</div>
<button
mat-flat-button
color="primary"
type="submit"
[disabled]="!lockDomain.valid"
>
Save
</button>
</form>
}
</div>
@@ -0,0 +1,20 @@
.console-app {
&__registry-lock {
mat-label {
display: block;
margin-bottom: 10px;
}
p {
margin-bottom: 40px;
}
}
&__registry-lock-notification {
padding: 20px;
border-radius: 10px;
background-color: var(--light-highlight);
margin-bottom: 20px;
width: max-content;
display: flex;
align-items: center;
}
}
@@ -0,0 +1,92 @@
// 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, computed } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { MatSnackBar } from '@angular/material/snack-bar';
import { RegistrarService } from '../registrar/registrar.service';
import { UserDataService } from '../shared/services/userData.service';
import { DomainListService } from './domainList.service';
import { RegistryLockService } from './registryLock.service';
@Component({
selector: 'app-registry-lock',
templateUrl: './registryLock.component.html',
styleUrls: ['./registryLock.component.scss'],
})
export class RegistryLockComponent {
readonly isLocked = computed(() =>
this.registryLockService.domainsLocks.some(
(dl) => dl.domainName === this.domainListService.selectedDomain
)
);
relockOptions = [
{ name: '1 hour', duration: 3600000 },
{ name: '6 hours', duration: 21600000 },
{ name: '24 hours', duration: 86400000 },
{ name: 'Never', duration: undefined },
];
lockDomain = new FormGroup({
password: new FormControl(''),
});
unlockDomain = new FormGroup({
password: new FormControl(''),
relockTime: new FormControl(undefined),
});
constructor(
protected registrarService: RegistrarService,
protected domainListService: DomainListService,
protected registryLockService: RegistryLockService,
protected userDataService: UserDataService,
private _snackBar: MatSnackBar
) {}
goBack() {
this.domainListService.selectedDomain = undefined;
this.domainListService.activeActionComponent = null;
}
save(isLock: boolean) {
let request;
if (!isLock) {
request = this.registryLockService.registryLockDomain(
this.domainListService.selectedDomain || '',
this.unlockDomain.value.password || '',
this.unlockDomain.value.relockTime || undefined,
isLock
);
} else {
request = this.registryLockService.registryLockDomain(
this.domainListService.selectedDomain || '',
this.lockDomain.value.password || '',
undefined,
isLock
);
}
request.subscribe({
complete: () => {
this.goBack();
},
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error);
},
});
}
}
@@ -0,0 +1,59 @@
// 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 { Injectable } from '@angular/core';
import { tap } from 'rxjs';
import { RegistrarService } from '../registrar/registrar.service';
import { BackendService } from '../shared/services/backend.service';
export interface DomainLocksResult {
domainName: string;
}
@Injectable({
providedIn: 'root',
})
export class RegistryLockService {
public domainsLocks: DomainLocksResult[] = [];
constructor(
private backendService: BackendService,
private registrarService: RegistrarService
) {}
retrieveLocks() {
return this.backendService
.getLocks(this.registrarService.registrarId())
.pipe(
tap((domainLocksResult) => {
this.domainsLocks = domainLocksResult;
})
);
}
registryLockDomain(
domainName: string,
password: string,
relockDurationMillis: number | undefined,
isLock: boolean
) {
return this.backendService.registryLockDomain(
domainName,
password,
relockDurationMillis,
this.registrarService.registrarId(),
isLock
);
}
}
@@ -34,7 +34,10 @@
</mat-card-actions>
</mat-card>
<mat-card appearance="outlined">
<mat-card
appearance="outlined"
[elementId]="getElementIdForRegistrarsBlock()"
>
<mat-card-content>
<h3>
<mat-icon class="secondary-text">account_circle</mat-icon>
@@ -18,6 +18,7 @@ import { DomainListComponent } from '../domains/domainList.component';
import { RegistrarComponent } from '../registrar/registrarsTable.component';
import SecurityComponent from '../settings/security/security.component';
import { SettingsComponent } from '../settings/settings.component';
import { RESTRICTED_ELEMENTS } from '../shared/directives/userLevelVisiblity.directive';
import { BreakPointObserverService } from '../shared/services/breakPoint.service';
@Component({
@@ -30,6 +31,9 @@ export class HomeComponent {
protected breakPointObserverService: BreakPointObserverService,
private router: Router
) {}
getElementIdForRegistrarsBlock() {
return RESTRICTED_ELEMENTS.REGISTRAR_ELEMENT;
}
viewRegistrars() {
this.router.navigate([RegistrarComponent.PATH], {
queryParamsHandling: 'merge',
@@ -0,0 +1,28 @@
@if (isLoading) {
<div class="console-app__registry-lock-verify-spinner">
<mat-spinner />
</div>
} @else if (domainName) {
<h1 class="mat-headline-4">Success!</h1>
<div class="console-app__registry-lock-content">
<div class="console-app__registry-lock-subhead">
The domain {{ domainName }} has been successfully {{ action }}ed.
</div>
</div>
<div>
<a
class="text-l"
routerLink="{{ DOMAIN_LIST_COMPONENT_PATH }}"
[queryParams]="{ registrarId: this.registrarService.registrarId() }"
>Return to the list of domains</a
>
</div>
} @else {
<h1 class="mat-headline-4">Failure</h1>
<div class="console-app__registry-lock-content">
<div class="console-app__registry-lock-subhead">
An error occurred: {{ errorMessage }}.<br /><br />Please double-check the
verification code and try again.
</div>
</div>
}
@@ -0,0 +1,9 @@
.console-app__registry-lock {
&-content {
margin-top: 30px;
}
&-subhead {
font-size: 20px;
margin-bottom: 20px;
}
}
@@ -0,0 +1,65 @@
// 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 { Component } from '@angular/core';
import { RegistrarService } from '../registrar/registrar.service';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { RegistryLockVerifyService } from './registryLockVerify.service';
import { HttpErrorResponse } from '@angular/common/http';
import { take } from 'rxjs';
import { DomainListComponent } from '../domains/domainList.component';
@Component({
selector: 'app-registry-lock-verify',
templateUrl: './registryLockVerify.component.html',
styleUrls: ['./registryLockVerify.component.scss'],
providers: [RegistryLockVerifyService],
})
export class RegistryLockVerifyComponent {
public static PATH = 'registry-lock-verify';
readonly DOMAIN_LIST_COMPONENT_PATH = `/${DomainListComponent.PATH}`;
isLoading = true;
domainName?: string;
action?: string;
errorMessage?: string;
constructor(
protected registrarService: RegistrarService,
protected registryLockVerifyService: RegistryLockVerifyService,
private route: ActivatedRoute
) {}
ngOnInit() {
this.route.queryParamMap.pipe(take(1)).subscribe((params: ParamMap) => {
this.registryLockVerifyService
.verifyRequest(params.get('lockVerificationCode') || '')
.subscribe({
error: (err: HttpErrorResponse) => {
this.isLoading = false;
this.errorMessage = err.error;
},
next: (verificationResponse) => {
this.domainName = verificationResponse.domainName;
this.action = verificationResponse.action;
this.registrarService.registrarId.set(
verificationResponse.registrarId
);
this.isLoading = false;
},
});
});
}
}
@@ -0,0 +1,31 @@
// 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 { Injectable } from '@angular/core';
import { BackendService } from '../shared/services/backend.service';
export interface RegistryLockVerificationResponse {
action: string;
domainName: string;
registrarId: string;
}
@Injectable()
export class RegistryLockVerifyService {
constructor(private backendService: BackendService) {}
verifyRequest(lockVerificationCode: string) {
return this.backendService.verifyRegistryLockRequest(lockVerificationCode);
}
}
@@ -0,0 +1,36 @@
<h1 class="mat-headline-4">Generate OT&E Accounts</h1>
<div class="console-app__new-ote">
@if (oteCreateResponseFormatted()) {
<h1>Generated Successfully</h1>
<mat-card appearance="outlined">
<mat-card-header>
<mat-card-title>Epp Credentials</mat-card-title>
<mat-card-subtitle
>Copy and paste this into an email to the registrars</mat-card-subtitle
>
</mat-card-header>
<mat-card-content>
<p>{{ oteCreateResponseFormatted() }}</p>
</mat-card-content>
</mat-card>
} @else {
<form (ngSubmit)="onSubmit()" [formGroup]="createOte">
<p>
<mat-form-field name="registrarId" appearance="outline">
<mat-label>Base Registrar Id: </mat-label>
<input matInput type="text" formControlName="registrarId" required />
</mat-form-field>
</p>
<p>
<mat-form-field name="registrarEmail" appearance="outline">
<mat-label>Contact Email: </mat-label>
<input matInput type="text" formControlName="registrarEmail" required />
<mat-hint
>Will be granted web-console access to the OTE registrars.</mat-hint
>
</mat-form-field>
</p>
<button mat-flat-button color="primary" type="submit">Save</button>
</form>
}
</div>
@@ -0,0 +1,11 @@
.console-app__new-ote {
max-width: 720px;
mat-card-content {
white-space: break-spaces;
padding: 20px;
}
mat-form-field {
width: 100%;
max-width: 350px;
}
}
@@ -0,0 +1,83 @@
// 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, computed, signal } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { MatSnackBar } from '@angular/material/snack-bar';
import { RegistrarService } from '../registrar/registrar.service';
import { MaterialModule } from '../material.module';
import { SnackBarModule } from '../snackbar.module';
export interface OteCreateResponse extends Map<string, string> {
password: string;
}
@Component({
selector: 'app-ote',
standalone: true,
imports: [MaterialModule, SnackBarModule],
templateUrl: './newOte.component.html',
styleUrls: ['./newOte.component.scss'],
})
export class NewOteComponent {
oteCreateResponse = signal<OteCreateResponse | undefined>(undefined);
readonly oteCreateResponseFormatted = computed(() => {
const oteCreateResponse = this.oteCreateResponse();
if (oteCreateResponse) {
const { password } = oteCreateResponse;
return Object.entries(oteCreateResponse)
.filter((entry) => entry[0] !== 'password')
.map(
([login, tld]) =>
`Login: ${login}\t\tPassword: ${password}\t\tTLD: ${tld}`
)
.join('\n');
}
return undefined;
});
createOte = new FormGroup({
registrarId: new FormControl('', [Validators.required]),
registrarEmail: new FormControl('', [Validators.required]),
});
constructor(
protected registrarService: RegistrarService,
private _snackBar: MatSnackBar
) {}
onSubmit() {
if (this.createOte.valid) {
const { registrarId, registrarEmail } = this.createOte.value;
this.registrarService
.generateOte(
{
registrarId,
registrarEmail,
},
registrarId || ''
)
.subscribe({
next: (oteCreateResponse: OteCreateResponse) => {
this.oteCreateResponse.set(oteCreateResponse);
},
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error || err.message);
},
});
}
}
}
@@ -0,0 +1,28 @@
<h1 class="mat-headline-4">OT&E Status Check</h1>
@if(registrarId() === null) {
<h1>Missing registrarId param</h1>
} @else if(isOte()) {
<h1 *ngIf="oteStatusResponse().length">
Status:
<span>{{ oteStatusUnfinished().length ? "Unfinished" : "Completed" }}</span>
</h1>
<div class="console-app__ote-status">
@if(oteStatusCompleted().length) {
<div class="console-app__ote-status_completed">
<h1>Completed</h1>
<div *ngFor="let entry of oteStatusCompleted()">
<mat-icon>check_box</mat-icon>{{ entry.description }}
</div>
</div>
} @if(oteStatusUnfinished().length) {
<div class="console-app__ote-status_unfinished">
<h1>Unfinished</h1>
<div *ngFor="let entry of oteStatusUnfinished()">
<mat-icon>check_box_outline_blank</mat-icon>{{ entry.description }}
</div>
</div>
}
</div>
} @else {
<h1>Registrar {{ registrarId() }} is not an OT&E registrar</h1>
}
@@ -0,0 +1,28 @@
.console-app__ote-status {
max-width: 730px;
display: flex;
flex-wrap: wrap;
&_completed,
&_unfinished {
border: 1px solid #ddd;
padding: 20px;
border-radius: 10px;
margin: 0 20px 30px 0;
div {
display: flex;
min-width: 300px;
align-items: flex-start;
max-width: 300px;
margin-bottom: 10px;
padding-bottom: 5px;
border-bottom: 1px solid #ddd;
&:last-child {
border: none;
}
}
mat-icon {
min-width: 30px;
}
}
}
@@ -0,0 +1,79 @@
// 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, computed, OnInit, signal } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { RegistrarService } from '../registrar/registrar.service';
import { MaterialModule } from '../material.module';
import { SnackBarModule } from '../snackbar.module';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { take } from 'rxjs';
export interface OteStatusResponse {
description: string;
requirement: number;
timesPerformed: number;
completed: boolean;
}
@Component({
selector: 'app-ote-status',
standalone: true,
imports: [MaterialModule, SnackBarModule, CommonModule],
templateUrl: './oteStatus.component.html',
styleUrls: ['./oteStatus.component.scss'],
})
export class OteStatusComponent implements OnInit {
registrarId = signal<string | null>(null);
oteStatusResponse = signal<OteStatusResponse[]>([]);
oteStatusCompleted = computed(() =>
this.oteStatusResponse().filter((v) => v.completed)
);
oteStatusUnfinished = computed(() =>
this.oteStatusResponse().filter((v) => !v.completed)
);
isOte = computed(
() =>
this.registrarService
.registrars()
.find((r) => r.registrarId === this.registrarId())
?.type?.toLowerCase() === 'ote'
);
constructor(
private route: ActivatedRoute,
protected registrarService: RegistrarService,
private _snackBar: MatSnackBar
) {}
ngOnInit(): void {
this.route.paramMap.pipe(take(1)).subscribe((params: ParamMap) => {
this.registrarId.set(params.get('registrarId'));
const registrarId = this.registrarId();
if (!registrarId) throw 'Missing registrarId param';
this.registrarService.oteStatus(registrarId).subscribe({
next: (oteStatusResponse: OteStatusResponse[]) => {
this.oteStatusResponse.set(oteStatusResponse);
},
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error || err.message);
},
});
});
}
}
@@ -142,7 +142,7 @@ JPY=billing-id-for-yen"
<mat-label>State/Region: </mat-label>
<input
matInput
[required]="false"
[required]="true"
[(ngModel)]="newRegistrar.localizedAddress.state"
[ngModelOptions]="{ standalone: true }"
/>
@@ -17,6 +17,8 @@ import { Observable, switchMap, tap } from 'rxjs';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router';
import { OteCreateResponse } from '../ote/newOte.component';
import { OteStatusResponse } from '../ote/oteStatus.component';
import { BackendService } from '../shared/services/backend.service';
import {
GlobalLoader,
@@ -69,6 +71,7 @@ export interface Registrar
registrarId: string;
registrarName: string;
registryLockAllowed?: boolean;
type?: string;
}
@Injectable({
@@ -149,4 +152,17 @@ export class RegistrarService implements GlobalLoader {
loadingTimeout() {
this._snackBar.open('Timeout loading registrars');
}
generateOte(
oteForm: Object,
registrarId: string
): Observable<OteCreateResponse> {
return this.backend
.generateOte(oteForm, registrarId)
.pipe(tap((_) => this.loadRegistrars()));
}
oteStatus(registrarId: string): Observable<OteStatusResponse[]> {
return this.backend.getOteStatus(registrarId);
}
}
@@ -8,6 +8,15 @@
</button>
<div class="spacer"></div>
@if(!inEdit && !registrarNotFound) {
<button
*ngIf="oteButtonVisible"
mat-stroked-button
(click)="checkOteStatus()"
aria-label="Check OT&E account"
[elementId]="getElementIdForOteBlock()"
>
Check OT&E Status
</button>
<button
mat-flat-button
color="primary"
@@ -18,8 +18,10 @@ import { MatChipInputEvent } from '@angular/material/chips';
import { MatSnackBar } from '@angular/material/snack-bar';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { RESTRICTED_ELEMENTS } from '../shared/directives/userLevelVisiblity.directive';
import { Registrar, RegistrarService } from './registrar.service';
import { RegistrarComponent, columns } from './registrarsTable.component';
import { environment } from '../../environments/environment';
@Component({
selector: 'app-registrar-details',
@@ -29,8 +31,9 @@ import { RegistrarComponent, columns } from './registrarsTable.component';
export class RegistrarDetailsComponent implements OnInit {
public static PATH = 'registrars/:id';
inEdit: boolean = false;
oteButtonVisible = environment.sandbox;
registrarInEdit!: Registrar;
registrarNotFound: boolean = false;
registrarNotFound: boolean = true;
columns = columns.filter((c) => !c.hiddenOnDetailsCard);
private subscription!: Subscription;
@@ -70,6 +73,16 @@ export class RegistrarDetailsComponent implements OnInit {
];
}
checkOteStatus() {
this.router.navigate(['ote-status/', this.registrarInEdit.registrarId], {
queryParamsHandling: 'merge',
});
}
getElementIdForOteBlock() {
return RESTRICTED_ELEMENTS.OTE;
}
removeTLD(tld: string) {
this.registrarInEdit.allowedTlds = this.registrarInEdit.allowedTlds?.filter(
(v) => v != tld
@@ -91,6 +104,6 @@ export class RegistrarDetailsComponent implements OnInit {
}
ngOnDestroy() {
this.subscription.unsubscribe();
this.subscription && this.subscription.unsubscribe();
}
}
@@ -20,6 +20,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MaterialModule } from '../material.module';
import { BackendService } from '../shared/services/backend.service';
import { RegistrarSelectorComponent } from './registrarSelector.component';
import { FormsModule } from '@angular/forms';
describe('RegistrarSelectorComponent', () => {
let component: RegistrarSelectorComponent;
@@ -28,7 +29,7 @@ describe('RegistrarSelectorComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [RegistrarSelectorComponent],
imports: [MaterialModule, BrowserAnimationsModule],
imports: [MaterialModule, BrowserAnimationsModule, FormsModule],
providers: [
BackendService,
provideHttpClient(),
@@ -4,7 +4,18 @@
<div class="console-app__registrars">
<div class="console-app__registrars-header">
<h1 class="mat-headline-4">Registrars</h1>
<div class="spacer"></div>
<button
mat-stroked-button
*ngIf="oteButtonVisible"
(click)="createOteAccount()"
aria-label="Generate OT&E accounts"
[elementId]="getElementIdForOteBlock()"
>
Create OT&E accounts
</button>
<button
class="console-app__registrars-new"
mat-flat-button
color="primary"
(click)="openNewRegistrar()"
@@ -10,6 +10,10 @@
min-width: $min-width !important;
}
&__registrars-new {
margin-left: 20px;
}
&__registrars-header {
display: flex;
justify-content: space-between;
@@ -17,7 +17,10 @@ import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router';
import { RESTRICTED_ELEMENTS } from '../shared/directives/userLevelVisiblity.directive';
import { Registrar, RegistrarService } from './registrar.service';
import { PATHS } from '../app-routing.module';
import { environment } from '../../environments/environment';
export const columns = [
{
@@ -80,7 +83,7 @@ export class RegistrarComponent {
public static PATH = 'registrars';
dataSource: MatTableDataSource<Registrar>;
columns = columns;
oteButtonVisible = environment.sandbox;
displayedColumns = this.columns.map((c) => c.columnDef);
@ViewChild(MatPaginator) paginator!: MatPaginator;
@@ -103,6 +106,14 @@ export class RegistrarComponent {
this.dataSource.sort = this.sort;
}
createOteAccount() {
this.router.navigate([PATHS.NewOteComponent]);
}
getElementIdForOteBlock() {
return RESTRICTED_ELEMENTS.OTE;
}
openDetails(registrarId: string) {
this.router.navigate(['registrars/', registrarId], {
queryParamsHandling: 'merge',
@@ -4,7 +4,7 @@
<div class="console-app__resources-subhead">Technical resources</div>
<a
class="text-l"
href="{{ userDataService.userData.technicalDocsUrl }}"
href="{{ userDataService.userData()?.technicalDocsUrl }}"
target="_blank"
>View onboarding FAQs, TLD information, and technical documentation on
Google Drive</a
@@ -18,7 +18,11 @@
<mat-icon>edit</mat-icon>
Edit
</button>
<button mat-icon-button aria-label="Delete Contact">
<button
mat-icon-button
aria-label="Delete Contact"
(click)="deleteContact()"
>
<mat-icon>delete</mat-icon>
</button>
}
@@ -50,6 +50,9 @@ export class ContactDetailsComponent {
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error);
},
complete: () => {
this.goBack();
},
});
}
}
@@ -20,20 +20,18 @@ import { FormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { of } from 'rxjs';
import { MaterialModule } from 'src/app/material.module';
import {
Registrar,
RegistrarService,
} from 'src/app/registrar/registrar.service';
import { 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';
import { SecurityService } from './security.service';
import SecurityEditComponent from './securityEdit.component';
import { MOCK_REGISTRAR_SERVICE } from 'src/testdata/registrar/registrar.service.mock';
describe('SecurityComponent', () => {
let component: SecurityComponent;
let fixture: ComponentFixture<SecurityComponent>;
let fetchSecurityDetailsSpy: Function;
let saveSpy: Function;
let dummyRegistrarService: RegistrarService;
beforeEach(async () => {
const securityServiceSpy = jasmine.createSpyObj(SecurityService, [
@@ -46,16 +44,13 @@ describe('SecurityComponent', () => {
saveSpy = securityServiceSpy.saveChanges;
dummyRegistrarService = {
registrar: { ipAddressAllowList: ['123.123.123.123'] },
} as RegistrarService;
await TestBed.configureTestingModule({
declarations: [SecurityComponent],
declarations: [SecurityEditComponent, SecurityComponent],
imports: [MaterialModule, BrowserAnimationsModule, FormsModule],
providers: [
BackendService,
{ provide: RegistrarService, useValue: dummyRegistrarService },
SecurityService,
{ provide: RegistrarService, useValue: MOCK_REGISTRAR_SERVICE },
provideHttpClient(),
provideHttpClientTesting(),
],
@@ -78,78 +73,65 @@ describe('SecurityComponent', () => {
expect(component).toBeTruthy();
});
it('should render ip allow list', waitForAsync(() => {
component.enableEdit();
it('should render security elements', waitForAsync(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(
Array.from(
fixture.nativeElement.querySelectorAll(
'.settings-security__ip-allowlist'
)
)
).toHaveSize(1);
expect(
fixture.nativeElement.querySelector('.settings-security__ip-allowlist')
.value
).toBe('123.123.123.123');
let listElems: Array<HTMLElement> = Array.from(
fixture.nativeElement.querySelectorAll('span.console-app__list-value')
);
expect(listElems).toHaveSize(8);
expect(listElems.map((e) => e.textContent)).toEqual([
'Change the password used for EPP logins',
'••••••••••••••',
'Restrict access to EPP production servers to the following IP/IPv6 addresses, or ranges like 1.1.1.0/24',
'123.123.123.123',
'X.509 PEM certificate for EPP production access',
'No client certificate on file.',
'X.509 PEM backup certificate for EPP production access',
'No failover certificate on file.',
]);
});
}));
it('should remove ip', waitForAsync(() => {
expect(
Array.from(
fixture.nativeElement.querySelectorAll(
'.settings-security__ip-allowlist'
)
)
).toHaveSize(1);
component.removeIpEntry(0);
component.dataSource.ipAddressAllowList =
component.dataSource.ipAddressAllowList?.splice(1);
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(
Array.from(
fixture.nativeElement.querySelectorAll(
'.settings-security__ip-allowlist'
)
)
).toHaveSize(0);
let listElems: Array<HTMLElement> = Array.from(
fixture.nativeElement.querySelectorAll('span.console-app__list-value')
);
expect(listElems.map((e) => e.textContent)).toContain(
'No IP addresses on file.'
);
});
}));
it('should toggle inEdit', () => {
expect(component.inEdit).toBeFalse();
component.enableEdit();
expect(component.inEdit).toBeTrue();
it('should toggle isEditingSecurity', () => {
expect(component.securityService.isEditingSecurity).toBeFalse();
component.editSecurity();
expect(component.securityService.isEditingSecurity).toBeTrue();
});
it('should create temporary data structure', () => {
expect(component.dataSource).toEqual(
apiToUiConverter(dummyRegistrarService.registrar)
);
component.removeIpEntry(0);
expect(component.dataSource).toEqual({ ipAddressAllowList: [] });
expect(dummyRegistrarService.registrar).toEqual({
ipAddressAllowList: ['123.123.123.123'],
} as Registrar);
component.cancel();
expect(component.dataSource).toEqual(
apiToUiConverter(dummyRegistrarService.registrar)
);
it('should toggle isEditingPassword', () => {
expect(component.securityService.isEditingPassword).toBeFalse();
component.editEppPassword();
expect(component.securityService.isEditingPassword).toBeTrue();
});
it('should call save', waitForAsync(async () => {
component.enableEdit();
fixture.detectChanges();
component.editSecurity();
await fixture.whenStable();
fixture.detectChanges();
const el = fixture.nativeElement.querySelector(
'.settings-security__clientCertificate'
'.console-app__clientCertificateValue'
);
el.value = 'test';
el.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
fixture.nativeElement
.querySelector('.settings-security__actions-save')
.querySelector('.settings-security__edit-save')
.click();
expect(saveSpy).toHaveBeenCalledOnceWith({
ipAddressAllowList: [{ value: '123.123.123.123' }],
@@ -35,6 +35,7 @@ export default class SecurityComponent {
if (this.registrarService.registrar()) {
this.dataSource = apiToUiConverter(this.registrarService.registrar());
this.securityService.isEditingSecurity = false;
this.securityService.isEditingPassword = false;
}
});
}
@@ -21,11 +21,13 @@ import { BackendService } from 'src/app/shared/services/backend.service';
import SecurityComponent from './security.component';
import {
SecurityService,
SecuritySettings,
SecuritySettingsBackendModel,
apiToUiConverter,
uiToApiConverter,
} from './security.service';
import {
SecuritySettings,
SecuritySettingsBackendModel,
} from 'src/app/registrar/registrar.service';
describe('SecurityService', () => {
const uiMockData: SecuritySettings = {
@@ -33,6 +33,7 @@
<p>X.509 PEM certificate for EPP production access.</p>
<mat-form-field appearance="outline">
<textarea
class="console-app__clientCertificateValue"
matInput
[(ngModel)]="dataSource.clientCertificate"
[ngModelOptions]="{ standalone: true }"
@@ -32,7 +32,14 @@ describe('WhoisComponent', () => {
imports: [MaterialModule, BrowserAnimationsModule],
providers: [
BackendService,
{ provide: RegistrarService, useValue: { registrar: {} } },
{
provide: RegistrarService,
useValue: {
registrar: function () {
return {};
},
},
},
provideHttpClient(),
provideHttpClientTesting(),
],
@@ -132,6 +132,18 @@
/>
</mat-form-field>
@if((userDataService.userData()?.globalRole || 'NONE') !== "NONE") {
<mat-form-field appearance="outline">
<mat-label>ICANN Referral Email: </mat-label>
<input
matInput
type="text"
[(ngModel)]="registrarInEdit.icannReferralEmail"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
}
<button mat-flat-button color="primary" type="submit">Save</button>
</form>
</div>
@@ -19,6 +19,7 @@ import {
Registrar,
RegistrarService,
} from 'src/app/registrar/registrar.service';
import { UserDataService } from 'src/app/shared/services/userData.service';
import { WhoisService } from './whois.service';
@Component({
@@ -30,6 +31,7 @@ export default class WhoisEditComponent {
registrarInEdit: Registrar | undefined;
constructor(
public userDataService: UserDataService,
public whoisService: WhoisService,
public registrarService: RegistrarService,
private _snackBar: MatSnackBar
@@ -12,43 +12,43 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Directive, ElementRef, Input, OnChanges } from '@angular/core';
import { Directive, ElementRef, Input, effect } from '@angular/core';
import { UserDataService } from '../services/userData.service';
export enum RESTRICTED_ELEMENTS {
REGISTRAR_ELEMENT,
OTE,
}
export const DISABLED_ELEMENTS_PER_ROLE = {
NONE: [RESTRICTED_ELEMENTS.REGISTRAR_ELEMENT],
NONE: [RESTRICTED_ELEMENTS.REGISTRAR_ELEMENT, RESTRICTED_ELEMENTS.OTE],
};
@Directive({
selector: '[elementId]',
})
export class UserLevelVisibility implements OnChanges {
export class UserLevelVisibility {
@Input() elementId!: RESTRICTED_ELEMENTS | null;
constructor(
private userDataService: UserDataService,
private el: ElementRef
) {}
ngOnChanges() {
this.processElement();
) {
effect(this.processElement.bind(this));
}
processElement() {
const globalRole = this.userDataService?.userData()?.globalRole || 'NONE';
if (this.elementId === null) {
return;
}
const globalRole = this.userDataService?.userData?.globalRole || 'NONE';
if (
// @ts-ignore
(DISABLED_ELEMENTS_PER_ROLE[globalRole] || []).includes(this.elementId)
) {
this.el.nativeElement.style.display = 'none';
} else {
this.el.nativeElement.style.display = '';
}
}
}
@@ -17,6 +17,10 @@ import { Injectable } from '@angular/core';
import { Observable, catchError, of, throwError } from 'rxjs';
import { DomainListResult } from 'src/app/domains/domainList.service';
import { DomainLocksResult } from 'src/app/domains/registryLock.service';
import { RegistryLockVerificationResponse } from 'src/app/lock/registryLockVerify.service';
import { OteCreateResponse } from 'src/app/ote/newOte.component';
import { OteStatusResponse } from 'src/app/ote/oteStatus.component';
import {
Registrar,
SecuritySettingsBackendModel,
@@ -169,4 +173,54 @@ export class BackendService {
whoisRegistrarFields
);
}
registryLockDomain(
domainName: string,
password: string | undefined,
relockDurationMillis: number | undefined,
registrarId: string,
isLock: boolean
) {
return this.http.post(
`/console-api/registry-lock?registrarId=${registrarId}`,
{
domainName,
password,
isLock,
relockDurationMillis,
}
);
}
getLocks(registrarId: string): Observable<DomainLocksResult[]> {
return this.http
.get<DomainLocksResult[]>(
`/console-api/registry-lock?registrarId=${registrarId}`
)
.pipe(catchError((err) => this.errorCatcher<DomainLocksResult[]>(err)));
}
generateOte(
oteForm: Object,
registrarId: string
): Observable<OteCreateResponse> {
return this.http.post<OteCreateResponse>(
`/console-api/ote?registrarId=${registrarId}`,
oteForm
);
}
getOteStatus(registrarId: string) {
return this.http
.get<OteStatusResponse[]>(`/console-api/ote?registrarId=${registrarId}`)
.pipe(catchError((err) => this.errorCatcher<OteStatusResponse[]>(err)));
}
verifyRegistryLockRequest(
lockVerificationCode: string
): Observable<RegistryLockVerificationResponse> {
return this.http.get<RegistryLockVerificationResponse>(
`/console-api/registry-lock-verify?lockVerificationCode=${lockVerificationCode}`
);
}
}
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { Injectable, signal } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Observable, tap } from 'rxjs';
import { BackendService } from './backend.service';
@@ -33,7 +33,7 @@ export interface UserData {
providedIn: 'root',
})
export class UserDataService implements GlobalLoader {
public userData!: UserData;
userData = signal<UserData | undefined>(undefined);
constructor(
private backend: BackendService,
protected globalLoader: GlobalLoaderService,
@@ -48,7 +48,7 @@ export class UserDataService implements GlobalLoader {
getUserData(): Observable<UserData> {
return this.backend.getUserData().pipe(
tap((userData: UserData) => {
this.userData = userData;
this.userData.set(userData);
})
);
}
@@ -17,9 +17,11 @@
For general purpose questions once you are integrated with our registry
system. If the issue is urgent, please put "Urgent" in the email title.
</p>
<a class="text-l" href="mailto:{{ userDataService.userData.supportEmail }}">{{
userDataService.userData.supportEmail
}}</a>
<a
class="text-l"
href="mailto:{{ userDataService.userData()?.supportEmail }}"
>{{ userDataService.userData()?.supportEmail }}</a
>
<p class="secondary-text">
Note: You may receive occasional service announcements via
registrar-announcement&#64;google.com. You will not be able to reply to
@@ -29,13 +31,13 @@
<p class="text-l">For general support inquiries 24/7:</p>
<a
class="text-l"
href="tel:{{ userDataService.userData.supportPhoneNumber }}"
>{{ userDataService.userData.supportPhoneNumber }}</a
href="tel:{{ userDataService.userData()?.supportPhoneNumber }}"
>{{ userDataService.userData()?.supportPhoneNumber }}</a
>
@if (userDataService.userData.passcode) {
@if (userDataService.userData()?.passcode) {
<p class="text-l">Your telephone passcode:</p>
<p class="text-l console-app__support-passcode">
{{ userDataService.userData.passcode }}
{{ userDataService.userData()?.passcode }}
</p>
<p class="secondary-text">
Note: Please be ready with your account name and telephone passcode when
@@ -14,4 +14,5 @@
export const environment = {
production: true,
sandbox: false,
};
@@ -0,0 +1,18 @@
// 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.
export const environment = {
production: false,
sandbox: true,
};
@@ -18,6 +18,7 @@
export const environment = {
production: false,
sandbox: false,
};
/*
+1 -1
View File
@@ -18,7 +18,7 @@ import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
if (environment.production || environment.sandbox) {
enableProdMode();
}
@@ -0,0 +1,21 @@
// 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 { RegistrarService } from 'src/app/registrar/registrar.service';
export const MOCK_REGISTRAR_SERVICE = {
registrar: function () {
return { ipAddressAllowList: ['123.123.123.123'] };
},
} as RegistrarService;
+2 -1
View File
@@ -29,5 +29,6 @@
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
},
"exclude": ["./src/testdata/*.*/*.ts"]
}
@@ -77,6 +77,7 @@ public class CloudTasksUtils implements Serializable {
@Config("projectId") String projectId,
@Config("locationId") String locationId,
@Config("oauthClientId") String oauthClientId,
// Note that this has to be a service account, due to limitations of the Cloud Tasks API.
@ApplicationDefaultCredential GoogleCredentialsBundle credential,
SerializableCloudTasksClient client) {
this.retrier = retrier;
@@ -64,7 +64,7 @@ import org.joda.time.Duration;
auth = Auth.AUTH_ADMIN)
public class DeleteProberDataAction implements Runnable {
// TODO(b/323026070): Add email alert on failure of this action
// TODO(b/346390641): Add email alert on failure of this action
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/**
@@ -1,17 +0,0 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<delete>
<domain:delete
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>%DOMAIN%</domain:name>
</domain:delete>
</delete>
<extension>
<metadata:metadata xmlns:metadata="urn:google:params:xml:ns:metadata-1.0">
<metadata:reason>Non-renewing domain has reached expiration date.</metadata:reason>
<metadata:requestedByRegistrar>false</metadata:requestedByRegistrar>
</metadata:metadata>
</extension>
<clTRID>ABC-12345</clTRID>
</command>
</epp>
@@ -119,6 +119,18 @@ public final class RegistryConfig {
return config.gcpProject.projectIdNumber;
}
@Provides
@Config("backendServiceIds")
public static Map<String, Long> provideBackendServiceIds(RegistryConfigSettings config) {
return config.gcpProject.backendServiceIds;
}
@Provides
@Config("baseDomain")
public static String provideBaseDomain(RegistryConfigSettings config) {
return config.gcpProject.baseDomain;
}
@Provides
@Config("locationId")
public static String provideLocationId(RegistryConfigSettings config) {
@@ -1259,12 +1271,6 @@ public final class RegistryConfig {
return config.auth.oauthClientId;
}
@Provides
@Config("fallbackOauthClientId")
public static String provideFallbackOauthClientId(RegistryConfigSettings config) {
return config.auth.fallbackOauthClientId;
}
/**
* Provides the OAuth scopes required for accessing Google APIs using the default credential.
*/
@@ -56,13 +56,14 @@ public class RegistryConfigSettings {
public String bsaServiceUrl;
public String toolsServiceUrl;
public String pubapiServiceUrl;
public Map<String, Long> backendServiceIds;
public String baseDomain;
}
/** Configuration options for authenticating users. */
public static class Auth {
public List<String> allowedServiceAccountEmails;
public String oauthClientId;
public String fallbackOauthClientId;
}
/** Configuration options for accessing Google APIs. */
@@ -1,5 +1,5 @@
# This is the default configuration file for Nomulus. Do not make changes to it
# unless you are writing new features that requires you to. To customize an
# unless you are writing new features that require you to. To customize an
# individual deployment or environment, create a nomulus-config.yaml file in the
# WEB-INF/ directory overriding only the values you wish to change. You may need
# to override some of these values to configure and enable some services used in
@@ -24,6 +24,17 @@ gcpProject:
toolsServiceUrl: https://tools.example.com
pubapiServiceUrl: https://pubapi.example.com
# The backend service IDs created when setting up GKE routes. They will be included in the
# audience field in the JWT that IAP creates.
# See: https://cloud.google.com/iap/docs/signed-headers-howto#verifying_the_jwt_payload
backendServiceIds:
frontend: 12345
backend: 12345
pubapi: 12345
console: 12345
# The base domain name of the registry service. Services are reachable at [service].baseDomain.
baseDomain: registry.test
gSuite:
# Publicly accessible domain name of the running G Suite instance.
@@ -328,25 +339,21 @@ caching:
# Note: Only allowedServiceAccountEmails and oauthClientId should be configured.
# Other fields are related to OAuth-based authentication and will be removed.
auth:
# Service accounts (e.g. default service account, account used by Cloud
# Service accounts (e.g., default service account, account used by Cloud
# Scheduler) allowed to send authenticated requests.
allowedServiceAccountEmails:
- default-service-account-email@email.com
- cloud-scheduler-email@email.com
# OAuth 2.0 client ID that will be used as the audience in OIDC ID tokens sent
# from clients (e.g. proxy, nomulus tool, cloud tasks) for authentication. The
# from clients (e.g., proxy, nomulus tool, cloud tasks) for authentication. The
# same ID is the only one accepted by the regular OIDC or IAP authentication
# mechanisms. In most cases we should use the client ID created for IAP here,
# mechanisms. In most cases, we should use the client ID created for IAP here,
# as it allows requests bearing a token with this audience to be accepted by
# both IAP or regular OIDC. The clientId value in proxy config file should be
# the same as this one.
oauthClientId: iap-oauth-clientid
# Same as above, but serve as a fallback, so we can switch the client ID of
# the proxy without downtime.
fallbackOauthClientId: fallback-oauth-clientid
credentialOAuth:
# OAuth scopes required for accessing Google APIs using the default
# credential.
@@ -36,7 +36,7 @@ import javax.inject.Inject;
/** Action that manually triggers refresh of DNS information. */
@Action(
service = Action.Service.BACKEND,
path = "/_dr/dnsRefresh",
path = "/_dr/task/dnsRefresh",
automaticallyPrintOk = true,
auth = Auth.AUTH_ADMIN)
public final class RefreshDnsAction implements Runnable {
@@ -165,7 +165,7 @@
<!-- Manually refreshes DNS information. -->
<servlet-mapping>
<servlet-name>backend-servlet</servlet-name>
<url-pattern>/_dr/dnsRefresh</url-pattern>
<url-pattern>/_dr/task/dnsRefresh</url-pattern>
</servlet-mapping>
<!-- Fans out a cron task over an adjustable range of TLDs. -->
@@ -266,7 +266,6 @@ public final class DomainCreateFlow implements MutatingFlow {
validateLaunchCreateNotice(launchCreate.get().getNotice(), domainLabel, isSuperuser, now);
}
boolean isSunriseCreate = hasSignedMarks && (tldState == START_DATE_SUNRISE);
// TODO(sarahbot@): Add check for valid EPP actions on the token
Optional<AllocationToken> allocationToken =
allocationTokenFlowUtils.verifyAllocationTokenCreateIfPresent(
command,
@@ -366,7 +365,7 @@ public final class DomainCreateFlow implements MutatingFlow {
createAutorenewBillingEvent(
domainHistoryId,
registrationExpirationTime,
getRenewalPriceInfo(isAnchorTenant, allocationToken, feesAndCredits));
getRenewalPriceInfo(isAnchorTenant, allocationToken));
PollMessage.Autorenew autorenewPollMessage =
createAutorenewPollMessage(domainHistoryId, registrationExpirationTime);
ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
@@ -689,9 +688,7 @@ public final class DomainCreateFlow implements MutatingFlow {
* AllocationToken} is 'SPECIFIED'.
*/
static RenewalPriceInfo getRenewalPriceInfo(
boolean isAnchorTenant,
Optional<AllocationToken> allocationToken,
FeesAndCredits feesAndCredits) {
boolean isAnchorTenant, Optional<AllocationToken> allocationToken) {
if (isAnchorTenant) {
allocationToken.ifPresent(
token ->
@@ -702,7 +699,7 @@ public final class DomainCreateFlow implements MutatingFlow {
} else if (allocationToken.isPresent()
&& allocationToken.get().getRenewalPriceBehavior() == RenewalPriceBehavior.SPECIFIED) {
return RenewalPriceInfo.create(
RenewalPriceBehavior.SPECIFIED, feesAndCredits.getCreateCost());
RenewalPriceBehavior.SPECIFIED, allocationToken.get().getRenewalPrice().get());
} else {
return RenewalPriceInfo.create(RenewalPriceBehavior.DEFAULT, null);
}
@@ -14,7 +14,6 @@
package google.registry.flows.domain;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
import static com.google.common.collect.Sets.symmetricDifference;
@@ -282,7 +281,7 @@ public final class DomainUpdateFlow implements MutatingFlow {
.removeContacts(remove.getContacts())
.addContacts(add.getContacts())
.setRegistrant(determineUpdatedRegistrant(change, domain))
.setAuthInfo(firstNonNull(change.getAuthInfo(), domain.getAuthInfo()));
.setAuthInfo(Optional.ofNullable(change.getAuthInfo()).orElse(domain.getAuthInfo()));
if (!add.getNameservers().isEmpty()) {
domainBuilder.addNameservers(add.getNameservers().stream().collect(toImmutableSet()));
@@ -32,7 +32,6 @@ 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;
@@ -48,9 +47,7 @@ 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;
@@ -271,29 +268,21 @@ public final class OteAccountBuilder {
Optional<String> groupEmailAddress, CloudTasksUtils cloudTasksUtils, IamClient iamClient) {
for (User user : users) {
User.grantIapPermission(
user.getEmailAddress(), groupEmailAddress, cloudTasksUtils, iamClient);
user.getEmailAddress(), groupEmailAddress, cloudTasksUtils, null, iamClient);
}
}
/** Saves all the OT&amp;E entities we created. */
private void saveAllEntities() {
ImmutableList<Tld> registries = ImmutableList.of(sunriseTld, gaTld, eapTld);
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(
() -> {
if (!replaceExisting) {
ImmutableMap<String, User> existingUsers =
tm().loadByEntitiesIfPresent(users).stream()
.collect(toImmutableMap(User::getEmailAddress, u -> u));
checkState(existingUsers.isEmpty(), "Found existing users: %s", existingUsers);
ImmutableList<VKey<? extends ImmutableObject>> keys =
Streams.concat(
registries.stream().map(tld -> Tld.createVKey(tld.getTldStr())),
@@ -317,16 +306,7 @@ public final class OteAccountBuilder {
tm().putAll(registrars);
});
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);
}
tm().transact(() -> tm().putAll(ImmutableList.copyOf(users)));
}
private Registrar addAllowedTld(Registrar registrar) {
@@ -63,7 +63,8 @@ public class FeatureFlag extends ImmutableObject implements Buildable {
public enum FeatureName {
TEST_FEATURE,
MINIMUM_DATASET_CONTACTS_OPTIONAL,
MINIMUM_DATASET_CONTACTS_PROHIBITED
MINIMUM_DATASET_CONTACTS_PROHIBITED,
NEW_CONSOLE
}
/** The name of the flag/feature. */
@@ -14,33 +14,36 @@
package google.registry.model.console;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.tools.server.UpdateUserGroupAction.GROUP_UPDATE_QUEUE;
import com.google.cloud.tasks.v2.Task;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import google.registry.batch.CloudTasksUtils;
import google.registry.persistence.VKey;
import google.registry.request.Action.Service;
import google.registry.tools.IamClient;
import google.registry.tools.ServiceConnection;
import google.registry.tools.server.UpdateUserGroupAction;
import google.registry.tools.server.UpdateUserGroupAction.Mode;
import google.registry.util.RegistryEnvironment;
import java.io.IOException;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Embeddable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.Table;
/** A console user, either a registry employee or a registrar partner. */
@Embeddable
@Entity
@Table(indexes = {@Index(columnList = "emailAddress", name = "user_email_address_idx")})
@Table
public class User extends UserBase {
public static final String IAP_SECURED_WEB_APP_USER_ROLE = "roles/iap.httpsResourceAccessor";
@@ -55,18 +58,30 @@ public class User extends UserBase {
public static void grantIapPermission(
String emailAddress,
Optional<String> groupEmailAddress,
CloudTasksUtils cloudTasksUtils,
@Nullable CloudTasksUtils cloudTasksUtils,
@Nullable ServiceConnection connection,
IamClient iamClient) {
if (RegistryEnvironment.isInTestServer()) {
return;
}
checkArgument(
cloudTasksUtils != null || connection != null,
"At least one of cloudTasksUtils or connection must be set");
checkArgument(
cloudTasksUtils == null || connection == null,
"Only one of cloudTasksUtils or connection can be set");
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);
if (cloudTasksUtils != null) {
modifyGroupMembershipAsync(
emailAddress, groupEmailAddress.get(), cloudTasksUtils, UpdateUserGroupAction.Mode.ADD);
} else {
modifyGroupMembershipSync(
emailAddress, groupEmailAddress.get(), connection, UpdateUserGroupAction.Mode.ADD);
}
}
}
@@ -79,18 +94,29 @@ public class User extends UserBase {
public static void revokeIapPermission(
String emailAddress,
Optional<String> groupEmailAddress,
CloudTasksUtils cloudTasksUtils,
@Nullable CloudTasksUtils cloudTasksUtils,
@Nullable ServiceConnection connection,
IamClient iamClient) {
if (RegistryEnvironment.isInTestServer()) {
return;
}
checkArgument(
cloudTasksUtils != null || connection != null,
"At least one of cloudTasksUtils or connection must be set");
checkArgument(
cloudTasksUtils == null || connection == null,
"Only one of cloudTasksUtils or connection can be set");
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);
if (cloudTasksUtils != null) {
modifyGroupMembershipAsync(
emailAddress, groupEmailAddress.get(), cloudTasksUtils, Mode.REMOVE);
} else {
modifyGroupMembershipSync(emailAddress, groupEmailAddress.get(), connection, Mode.REMOVE);
}
}
}
@@ -113,12 +139,30 @@ public class User extends UserBase {
cloudTasksUtils.enqueue(GROUP_UPDATE_QUEUE, task);
}
@Override
private static void modifyGroupMembershipSync(
String userEmailAddress, String groupEmailAddress, ServiceConnection connection, Mode mode) {
try {
connection.sendPostRequest(
UpdateUserGroupAction.PATH,
ImmutableMap.of(
"userEmailAddress",
userEmailAddress,
"groupEmailAddress",
groupEmailAddress,
"groupUpdateMode",
mode.name()),
MediaType.PLAIN_TEXT_UTF_8,
new byte[0]);
} catch (IOException e) {
throw new RuntimeException("Cannot send request to server", e);
}
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Override
@Access(AccessType.PROPERTY)
public Long getId() {
return super.getId();
public String getEmailAddress() {
return super.getEmailAddress();
}
@Override
@@ -128,7 +172,7 @@ public class User extends UserBase {
@Override
public VKey<User> createVKey() {
return VKey.create(User.class, getId());
return VKey.create(User.class, getEmailAddress());
}
/** Builder for constructing immutable {@link User} objects. */
@@ -49,12 +49,8 @@ public class UserBase extends UpdateAutoTimestampEntity implements Buildable {
private static final long serialVersionUID = 6936728603828566721L;
/** Autogenerated unique ID of this user. */
@Transient private Long id;
/** Email address of the user in question. */
@Column(nullable = false)
String emailAddress;
@Transient String emailAddress;
/** Optional external email address to use for registry lock confirmation emails. */
@Column String registryLockEmailAddress;
@@ -72,22 +68,17 @@ public class UserBase extends UpdateAutoTimestampEntity implements Buildable {
/** Randomly generated hash salt. */
String registryLockPasswordSalt;
public Long getId() {
return id;
}
/**
* Sets the user ID.
* Sets the user email address.
*
* <p>This should only be used for restoring the user id of an object being loaded in a PostLoad
* method (effectively, when it is still under construction by Hibernate). In all other cases, the
* object should be regarded as immutable and changes should go through a Builder.
* <p>This should only be used for restoring an object being loaded in a PostLoad method
* (effectively, when it is still under construction by Hibernate). In all other cases, the object
* should be regarded as immutable and changes should go through a Builder.
*
* <p>In addition to this special case use, this method must exist to satisfy Hibernate.
*/
@SuppressWarnings("unused")
void setId(Long id) {
this.id = id;
void setEmailAddress(String emailAddress) {
this.emailAddress = emailAddress;
}
public String getEmailAddress() {
@@ -1,54 +0,0 @@
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.console;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import java.util.Optional;
/** Data access object for {@link User} objects to simplify saving and retrieval. */
public class UserDao {
/** Retrieves the one user with this email address if it exists. */
public static Optional<User> loadUser(String emailAddress) {
return tm().transact(
() ->
tm().query("FROM User WHERE emailAddress = :emailAddress", User.class)
.setParameter("emailAddress", emailAddress)
.getResultStream()
.findFirst());
}
/** Saves the given user, checking that no existing user already exists with this email. */
public static void saveUser(User user) {
tm().transact(
() -> {
// Check for an existing user (the unique constraint protects us, but this gives a
// nicer exception)
Optional<User> maybeSavedUser = loadUser(user.getEmailAddress());
if (maybeSavedUser.isPresent()) {
User savedUser = maybeSavedUser.get();
checkArgument(
savedUser.getId().equals(user.getId()),
String.format(
"Attempted save of User with email address %s and ID %s, user with that"
+ " email already exists with ID %s",
user.getEmailAddress(), user.getId(), savedUser.getId()));
}
tm().put(user);
});
}
}
@@ -38,9 +38,8 @@ public class UserUpdateHistory extends ConsoleUpdateHistory {
UserBase user;
// This field exists so that it's populated in the SQL table
@Column(nullable = false, name = "userId")
Long id;
@Column(nullable = false, name = "emailAddress")
String emailAddress;
public UserBase getUser() {
return user;
@@ -48,7 +47,7 @@ public class UserUpdateHistory extends ConsoleUpdateHistory {
@PostLoad
void postLoad() {
user.setId(id);
user.setEmailAddress(emailAddress);
}
/** Creates a {@link VKey} instance for this entity. */
@@ -79,7 +78,7 @@ public class UserUpdateHistory extends ConsoleUpdateHistory {
public Builder setUser(User user) {
getInstance().user = user;
getInstance().id = user.getId();
getInstance().emailAddress = user.getEmailAddress();
return this;
}
}
@@ -144,6 +144,7 @@ public class DomainBase extends EppResource
@AttributeOverride(name = "pw.value", column = @Column(name = "auth_info_value")),
@AttributeOverride(name = "pw.repoId", column = @Column(name = "auth_info_repo_id")),
})
@Nullable
DomainAuthInfo authInfo;
/** Data used to construct DS records for this domain. */
@@ -620,6 +621,7 @@ public class DomainBase extends EppResource
return getAllContacts(true);
}
@Nullable
public DomainAuthInfo getAuthInfo() {
return authInfo;
}
@@ -139,7 +139,7 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda
/** Token saved on a TLD to use if no other token is passed from the client */
DEFAULT_PROMO(/* isOneTimeUse= */ false),
/** This is the old name for what is now BULK_PRICING. */
// TODO(sarahbot@): Remove this type once all tokens of this type have been scrubbed from the
// TODO(b/261763205): Remove this type once all tokens of this type have been scrubbed from the
// database
@Deprecated
PACKAGE(/* isOneTimeUse= */ false),
@@ -16,6 +16,7 @@ package google.registry.model.eppcommon;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import javax.annotation.Nullable;
import javax.persistence.Embeddable;
import javax.persistence.Embedded;
import javax.persistence.MappedSuperclass;
@@ -49,10 +50,12 @@ public abstract class AuthInfo extends ImmutableObject implements UnsafeSerializ
public static class PasswordAuth extends ImmutableObject implements UnsafeSerializable {
@XmlValue
@XmlJavaTypeAdapter(NormalizedStringAdapter.class)
@Nullable
String value;
@XmlAttribute(name = "roid")
@XmlJavaTypeAdapter(CollapsedStringAdapter.class)
@Nullable
String repoId;
public String getValue() {
@@ -63,14 +66,14 @@ public abstract class AuthInfo extends ImmutableObject implements UnsafeSerializ
return repoId;
}
public static PasswordAuth create(String value, String repoId) {
public static PasswordAuth create(@Nullable String value, @Nullable String repoId) {
PasswordAuth instance = new PasswordAuth();
instance.value = value;
instance.repoId = repoId;
return instance;
}
public static PasswordAuth create(String value) {
public static PasswordAuth create(@Nullable String value) {
return create(value, null);
}
}
@@ -233,6 +233,7 @@ public class RegistrarBase extends UpdateAutoTimestampEntity implements Buildabl
String registrarName;
/** The type of this registrar. */
@Expose
@Column(nullable = false)
@Enumerated(EnumType.STRING)
Type type;
@@ -25,8 +25,6 @@ import javax.persistence.AccessType;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Index;
import javax.persistence.Table;
/**
* A contact for a Registrar. Note, equality, hashCode and comparable have been overridden to only
@@ -37,7 +35,6 @@ import javax.persistence.Table;
* set to true.
*/
@Entity
@Table(indexes = @Index(columnList = "loginEmailAddress", name = "registrarpoc_login_email_idx"))
@IdClass(RegistrarPocId.class)
@Access(AccessType.FIELD)
public class RegistrarPoc extends RegistrarPocBase {
@@ -98,12 +98,7 @@ public class RegistrarPocBase extends ImmutableObject implements Jsonifiable, Un
/** The name of the contact. */
@Expose String name;
/**
* The contact email address of the contact.
*
* <p>This is different from the login email which is assgined to the regstrar and cannot be
* changed.
*/
/** The contact email address of the contact. */
@Expose @Transient String emailAddress;
@Expose @Transient public String registrarId;
@@ -123,9 +118,6 @@ public class RegistrarPocBase extends ImmutableObject implements Jsonifiable, Un
*/
@Expose Set<Type> types;
/** A GAIA email address that was assigned to the registrar for console login purpose. */
String loginEmailAddress;
/**
* Whether this contact is publicly visible in WHOIS registrar query results as an Admin contact.
*/
@@ -219,10 +211,6 @@ public class RegistrarPocBase extends ImmutableObject implements Jsonifiable, Un
return visibleInDomainWhoisAsAbuse;
}
public String getLoginEmailAddress() {
return loginEmailAddress;
}
public Builder<? extends RegistrarPocBase, ?> asBuilder() {
return new Builder<>(clone(this));
}
@@ -286,13 +274,6 @@ public class RegistrarPocBase extends ImmutableObject implements Jsonifiable, Un
+ "Registrar Abuse contact info: ")
.append(getVisibleInDomainWhoisAsAbuse() ? "Yes" : "No")
.append('\n');
result
.append("Registrar-Console access: ")
.append(getLoginEmailAddress() != null ? "Yes" : "No")
.append('\n');
if (getLoginEmailAddress() != null) {
result.append("Login Email Address: ").append(getLoginEmailAddress()).append('\n');
}
return result.toString();
}
@@ -310,7 +291,6 @@ public class RegistrarPocBase extends ImmutableObject implements Jsonifiable, Un
.put("visibleInDomainWhoisAsAbuse", visibleInDomainWhoisAsAbuse)
.put("allowedToSetRegistryLockPassword", allowedToSetRegistryLockPassword)
.put("registryLockAllowed", isRegistryLockAllowed())
.put("loginEmailAddress", loginEmailAddress)
.build();
}
@@ -411,11 +391,6 @@ public class RegistrarPocBase extends ImmutableObject implements Jsonifiable, Un
return thisCastToDerived();
}
public B setLoginEmailAddress(String loginEmailAddress) {
getInstance().loginEmailAddress = loginEmailAddress;
return thisCastToDerived();
}
public B setAllowedToSetRegistryLockPassword(boolean allowedToSetRegistryLockPassword) {
if (allowedToSetRegistryLockPassword) {
getInstance().registryLockPasswordSalt = null;
@@ -102,9 +102,11 @@ interface RegistryComponent {
class RegistryModule {
@Provides
static RequestHandler<RequestComponent> provideRequestHandler(
@Config("baseDomain") String baseDomain,
Provider<RequestComponent.Builder> componentProvider,
RequestAuthenticator requestAuthenticator) {
return RequestHandler.create(RequestComponent.class, componentProvider, requestAuthenticator);
return RequestHandler.create(
RequestComponent.class, baseDomain, componentProvider, requestAuthenticator);
}
}
}
@@ -113,6 +113,8 @@ import google.registry.ui.server.console.ConsoleDomainGetAction;
import google.registry.ui.server.console.ConsoleDomainListAction;
import google.registry.ui.server.console.ConsoleDumDownloadAction;
import google.registry.ui.server.console.ConsoleEppPasswordAction;
import google.registry.ui.server.console.ConsoleModule;
import google.registry.ui.server.console.ConsoleOteAction;
import google.registry.ui.server.console.ConsoleRegistryLockAction;
import google.registry.ui.server.console.ConsoleRegistryLockVerifyAction;
import google.registry.ui.server.console.ConsoleUpdateRegistrarAction;
@@ -121,15 +123,6 @@ import google.registry.ui.server.console.RegistrarsAction;
import google.registry.ui.server.console.settings.ContactAction;
import google.registry.ui.server.console.settings.SecurityAction;
import google.registry.ui.server.console.settings.WhoisRegistrarFieldsAction;
import google.registry.ui.server.registrar.ConsoleOteSetupAction;
import google.registry.ui.server.registrar.ConsoleRegistrarCreatorAction;
import google.registry.ui.server.registrar.ConsoleUiAction;
import google.registry.ui.server.registrar.OteStatusAction;
import google.registry.ui.server.registrar.RegistrarConsoleModule;
import google.registry.ui.server.registrar.RegistrarSettingsAction;
import google.registry.ui.server.registrar.RegistryLockGetAction;
import google.registry.ui.server.registrar.RegistryLockPostAction;
import google.registry.ui.server.registrar.RegistryLockVerifyAction;
import google.registry.whois.WhoisAction;
import google.registry.whois.WhoisHttpAction;
import google.registry.whois.WhoisModule;
@@ -142,6 +135,7 @@ import google.registry.whois.WhoisModule;
BillingModule.class,
CheckApiModule.class,
CloudDnsWriterModule.class,
ConsoleModule.class,
CronModule.class,
CustomLogicModule.class,
DnsCountQueryCoordinatorModule.class,
@@ -154,7 +148,6 @@ import google.registry.whois.WhoisModule;
LoadTestModule.class,
RdapModule.class,
RdeModule.class,
RegistrarConsoleModule.class,
ReportingModule.class,
RequestModule.class,
SheetModule.class,
@@ -186,16 +179,12 @@ interface RequestComponent {
ConsoleEppPasswordAction consoleEppPasswordAction();
ConsoleOteSetupAction consoleOteSetupAction();
ConsoleRegistrarCreatorAction consoleRegistrarCreatorAction();
ConsoleOteAction consoleOteAction();
ConsoleRegistryLockAction consoleRegistryLockAction();
ConsoleRegistryLockVerifyAction consoleRegistryLockVerifyAction();
ConsoleUiAction consoleUiAction();
ConsoleUpdateRegistrarAction consoleUpdateRegistrarAction();
ConsoleUserDataAction consoleUserDataAction();
@@ -254,8 +243,6 @@ interface RequestComponent {
NordnVerifyAction nordnVerifyAction();
OteStatusAction oteStatusAction();
PublishDnsUpdatesAction publishDnsUpdatesAction();
PublishInvoicesAction uploadInvoicesAction();
@@ -296,16 +283,8 @@ interface RequestComponent {
RefreshDnsOnHostRenameAction refreshDnsOnHostRenameAction();
RegistrarSettingsAction registrarSettingsAction();
RegistrarsAction registrarsAction();
RegistryLockGetAction registryLockGetAction();
RegistryLockPostAction registryLockPostAction();
RegistryLockVerifyAction registryLockVerifyAction();
RelockDomainAction relockDomainAction();
ResaveAllEppResourcesPipelineAction resaveAllEppResourcesPipelineAction();
@@ -29,6 +29,8 @@ import google.registry.ui.server.console.ConsoleDomainGetAction;
import google.registry.ui.server.console.ConsoleDomainListAction;
import google.registry.ui.server.console.ConsoleDumDownloadAction;
import google.registry.ui.server.console.ConsoleEppPasswordAction;
import google.registry.ui.server.console.ConsoleModule;
import google.registry.ui.server.console.ConsoleOteAction;
import google.registry.ui.server.console.ConsoleRegistryLockAction;
import google.registry.ui.server.console.ConsoleRegistryLockVerifyAction;
import google.registry.ui.server.console.ConsoleUpdateRegistrarAction;
@@ -41,7 +43,6 @@ import google.registry.ui.server.registrar.ConsoleOteSetupAction;
import google.registry.ui.server.registrar.ConsoleRegistrarCreatorAction;
import google.registry.ui.server.registrar.ConsoleUiAction;
import google.registry.ui.server.registrar.OteStatusAction;
import google.registry.ui.server.registrar.RegistrarConsoleModule;
import google.registry.ui.server.registrar.RegistrarSettingsAction;
import google.registry.ui.server.registrar.RegistryLockGetAction;
import google.registry.ui.server.registrar.RegistryLockPostAction;
@@ -54,7 +55,7 @@ import google.registry.ui.server.registrar.RegistryLockVerifyAction;
BatchModule.class,
DnsModule.class,
EppTlsModule.class,
RegistrarConsoleModule.class,
ConsoleModule.class,
RequestModule.class,
WhiteboxModule.class,
})
@@ -65,6 +66,8 @@ public interface FrontendRequestComponent {
ConsoleEppPasswordAction consoleEppPasswordAction();
ConsoleOteAction consoleOteAction();
ConsoleOteSetupAction consoleOteSetupAction();
ConsoleRegistrarCreatorAction consoleRegistrarCreatorAction();
@@ -82,7 +82,6 @@ public final class ActivityReportingQueryBuilder implements QueryBuilder {
// Convert reportingMonth into YYYYMMDD format for Bigquery table partition pattern-matching.
DateTimeFormatter logTableFormatter = DateTimeFormat.forPattern("yyyyMMdd");
// The monthly logs are a shared dependency for epp counts and whois metrics
String monthlyLogsQuery =
SqlTemplate.create(getQueryFromFile("monthly_logs.sql"))
.put("PROJECT_ID", projectId)
@@ -96,12 +95,10 @@ public final class ActivityReportingQueryBuilder implements QueryBuilder {
String eppQuery =
SqlTemplate.create(getQueryFromFile("epp_metrics.sql"))
.put("PROJECT_ID", projectId)
.put("ICANN_REPORTING_DATA_SET", icannReportingDataSet)
.put("MONTHLY_LOGS_TABLE", getTableName(MONTHLY_LOGS, yearMonth))
// All metadata logs for reporting come from google.registry.flows.FlowReporter.
.put(
"METADATA_LOG_PREFIX",
"google.registry.flows.FlowReporter recordToLogs: FLOW-LOG-SIGNATURE-METADATA")
.put("APPENGINE_LOGS_DATA_SET", "appengine_logs")
.put("APP_LOGS_TABLE", "_var_log_app_")
.put("FIRST_DAY_OF_MONTH", logTableFormatter.print(firstDayOfMonth))
.put("LAST_DAY_OF_MONTH", logTableFormatter.print(lastDayOfMonth))
.build();
queriesBuilder.put(getTableName(EPP_METRICS, yearMonth), eppQuery);
@@ -14,6 +14,7 @@
package google.registry.reporting.icann;
import static google.registry.request.RequestParameters.extractOptionalBooleanParameter;
import static google.registry.request.RequestParameters.extractOptionalParameter;
import static google.registry.request.RequestParameters.extractRequiredParameter;
import static google.registry.request.RequestParameters.extractSetOfEnumParameters;
@@ -42,6 +43,7 @@ public final class IcannReportingModule {
static final String PARAM_SUBDIR = "subdir";
static final String PARAM_REPORT_TYPES = "reportTypes";
static final String PARAM_SHOULD_UPLOAD = "shouldUpload";
static final String ICANN_REPORTING_DATA_SET = "icannReportingDataSet";
static final String MANIFEST_FILE_NAME = "MANIFEST.txt";
@@ -76,6 +78,12 @@ public final class IcannReportingModule {
return reportTypes.isEmpty() ? ImmutableSet.copyOf(ReportType.values()) : reportTypes;
}
@Provides
@Parameter(PARAM_SHOULD_UPLOAD)
static boolean provideShouldUpload(HttpServletRequest req) {
return extractOptionalBooleanParameter(req, PARAM_SHOULD_UPLOAD).orElse(true);
}
/**
* Constructs a BigqueryConnection with default settings.
*
@@ -16,6 +16,7 @@ package google.registry.reporting.icann;
import static com.google.common.base.Throwables.getRootCause;
import static google.registry.reporting.icann.IcannReportingModule.PARAM_REPORT_TYPES;
import static google.registry.reporting.icann.IcannReportingModule.PARAM_SHOULD_UPLOAD;
import static google.registry.reporting.icann.IcannReportingModule.PARAM_SUBDIR;
import static google.registry.request.Action.Method.POST;
import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
@@ -30,6 +31,7 @@ import google.registry.batch.CloudTasksUtils;
import google.registry.bigquery.BigqueryJobFailureException;
import google.registry.config.RegistryConfig.Config;
import google.registry.groups.GmailClient;
import google.registry.reporting.ReportingModule;
import google.registry.reporting.icann.IcannReportingModule.ReportType;
import google.registry.request.Action;
import google.registry.request.Action.Service;
@@ -79,6 +81,15 @@ public final class IcannReportingStagingAction implements Runnable {
@Inject YearMonth yearMonth;
@Inject @Parameter(PARAM_SUBDIR) Optional<String> overrideSubdir;
@Inject @Parameter(PARAM_REPORT_TYPES) ImmutableSet<ReportType> reportTypes;
@Inject
@Parameter(ReportingModule.SEND_EMAIL)
boolean sendEmail;
@Inject
@Parameter(PARAM_SHOULD_UPLOAD)
boolean shouldUpload;
@Inject IcannReportingStager stager;
@Inject Retrier retrier;
@Inject Response response;
@@ -106,28 +117,35 @@ public final class IcannReportingStagingAction implements Runnable {
stager.createAndUploadManifest(subdir, manifestedFiles);
logger.atInfo().log("Completed staging %d report files.", manifestedFiles.size());
gmailClient.sendEmail(
EmailMessage.newBuilder()
.setSubject("ICANN Monthly report staging summary [SUCCESS]")
.setBody(
String.format(
"Completed staging the following %d ICANN reports:\n%s",
manifestedFiles.size(), Joiner.on('\n').join(manifestedFiles)))
.addRecipient(recipient)
.build());
if (sendEmail) {
gmailClient.sendEmail(
EmailMessage.newBuilder()
.setSubject("ICANN Monthly report staging summary [SUCCESS]")
.setBody(
String.format(
"Completed staging the following %d ICANN reports:\n%s",
manifestedFiles.size(), Joiner.on('\n').join(manifestedFiles)))
.addRecipient(recipient)
.build());
} else {
logger.atInfo().log("Would have sent staging report summary to %s", recipient);
}
response.setStatus(SC_OK);
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
response.setPayload("Completed staging action.");
logger.atInfo().log("Enqueueing report upload.");
cloudTasksUtils.enqueue(
CRON_QUEUE,
cloudTasksUtils.createPostTaskWithDelay(
IcannReportingUploadAction.PATH,
Service.BACKEND,
null,
Duration.standardMinutes(2)));
if (shouldUpload) {
logger.atInfo().log("Enqueueing report upload.");
cloudTasksUtils.enqueue(
CRON_QUEUE,
cloudTasksUtils.createPostTaskWithDelay(
IcannReportingUploadAction.PATH,
Service.BACKEND,
null,
Duration.standardMinutes(2)));
} else {
logger.atInfo().log("Would have enqueued report upload");
}
return null;
},
BigqueryJobFailureException.class);
@@ -113,13 +113,9 @@ public final class TransactionsReportingQueryBuilder implements QueryBuilder {
SqlTemplate.create(getQueryFromFile("cloud_sql_attempted_adds.sql"))
.put("PROJECT_ID", projectId)
.put("APPENGINE_LOGS_DATA_SET", "appengine_logs")
.put("REQUEST_TABLE", "appengine_googleapis_com_request_log_")
.put("APP_LOGS_TABLE", "_var_log_app_")
.put("FIRST_DAY_OF_MONTH", logTableFormatter.print(earliestReportTime))
.put("LAST_DAY_OF_MONTH", logTableFormatter.print(latestReportTime))
// All metadata logs for reporting come from google.registry.flows.FlowReporter.
.put(
"METADATA_LOG_PREFIX",
"google.registry.flows.FlowReporter recordToLogs: FLOW-LOG-SIGNATURE-METADATA")
.build();
queriesBuilder.put(getTableName(ATTEMPTED_ADDS, yearMonth), attemptedAddsQuery);
@@ -14,6 +14,8 @@
package google.registry.request;
import static com.google.common.base.Preconditions.checkState;
import google.registry.request.auth.Auth;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -36,7 +38,6 @@ public @interface Action {
BACKEND("backend"),
PUBAPI("pubapi");
private final String serviceId;
Service(String serviceId) {
@@ -49,9 +50,33 @@ public @interface Action {
}
}
enum GkeService {
// This designation means that it defers to the GAE service, so we don't have to annotate EVERY
// action during the GKE migration.
SAME_AS_GAE("same_as_gae"),
FRONTEND("frontend"),
BACKEND("backend"),
PUBAPI("pubapi"),
CONSOLE("console");
private final String serviceId;
GkeService(String serviceId) {
this.serviceId = serviceId;
}
public String getServiceId() {
checkState(this != SAME_AS_GAE, "Cannot get service Id for SAME_AS_GAE");
return serviceId;
}
}
/** Which App Engine service this action lives on. */
Service service();
/** Which GKE service this action lives on. */
GkeService gkeService() default GkeService.SAME_AS_GAE;
/** HTTP path to serve the action from. The path components must be percent-escaped. */
String path();
@@ -72,4 +97,22 @@ public @interface Action {
/** Authentication settings. */
Auth auth();
// TODO(jianglai): Use Action.gkeService() directly once we are off GAE.
class ServiceGetter {
public static GkeService get(Action action) {
GkeService service = action.gkeService();
if (service != GkeService.SAME_AS_GAE) {
return service;
}
Service gaeService = action.service();
return switch (gaeService) {
case DEFAULT -> GkeService.FRONTEND;
case BACKEND -> GkeService.BACKEND;
case TOOLS -> GkeService.BACKEND;
case BSA -> GkeService.BACKEND;
case PUBAPI -> GkeService.PUBAPI;
};
}
}
}
@@ -22,14 +22,17 @@ import static jakarta.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED;
import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import com.google.common.flogger.FluentLogger;
import google.registry.request.Action.GkeService;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.RequestAuthenticator;
import google.registry.util.NonFinalForTesting;
import google.registry.util.RegistryEnvironment;
import google.registry.util.SystemClock;
import google.registry.util.TypeUtils.TypeInstantiator;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.inject.Provider;
@@ -69,6 +72,7 @@ public class RequestHandler<C> {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final Router router;
@Nullable private final String baseDomain;
private final Provider<? extends RequestComponentBuilder<C>> requestComponentBuilderProvider;
private final RequestAuthenticator requestAuthenticator;
private final SystemClock clock = new SystemClock();
@@ -91,22 +95,22 @@ public class RequestHandler<C> {
protected RequestHandler(
Provider<? extends RequestComponentBuilder<C>> requestComponentBuilderProvider,
RequestAuthenticator requestAuthenticator) {
this(null, requestComponentBuilderProvider, requestAuthenticator);
this(null, null, requestComponentBuilderProvider, requestAuthenticator);
}
/** Creates a new RequestHandler with an explicit component class for test purposes. */
public static <C> RequestHandler<C> create(
Class<C> component,
@Nullable String baseDomain,
Provider<? extends RequestComponentBuilder<C>> requestComponentBuilderProvider,
RequestAuthenticator requestAuthenticator) {
return new RequestHandler<>(
checkNotNull(component),
requestComponentBuilderProvider,
requestAuthenticator);
checkNotNull(component), baseDomain, requestComponentBuilderProvider, requestAuthenticator);
}
private RequestHandler(
@Nullable Class<C> component,
@Nullable String baseDomain,
Provider<? extends RequestComponentBuilder<C>> requestComponentBuilderProvider,
RequestAuthenticator requestAuthenticator) {
// If the component class isn't explicitly provided, infer it from the class's own typing.
@@ -114,6 +118,7 @@ public class RequestHandler<C> {
// preserved at runtime, so only expose that option via the protected constructor.
this.router = Router.create(
component != null ? component : new TypeInstantiator<C>(getClass()){}.getExactType());
this.baseDomain = baseDomain;
this.requestComponentBuilderProvider = checkNotNull(requestComponentBuilderProvider);
this.requestAuthenticator = checkNotNull(requestAuthenticator);
}
@@ -137,6 +142,17 @@ public class RequestHandler<C> {
rsp.sendError(SC_NOT_FOUND);
return;
}
if (RegistryEnvironment.isOnJetty()) {
GkeService service = Action.ServiceGetter.get(route.get().action());
String expectedDomain = String.format("%s.%s", service.getServiceId(), baseDomain);
String actualDomain = req.getServerName();
if (!Objects.equals(actualDomain, expectedDomain)) {
logger.atWarning().log(
"Actual domain %s does not match expected domain %s", actualDomain, expectedDomain);
rsp.sendError(SC_NOT_FOUND);
return;
}
}
if (!route.get().isMethodAllowed(method)) {
logger.atWarning().log("Method %s not allowed for: %s", method, path);
rsp.sendError(SC_METHOD_NOT_ALLOWED);
@@ -21,6 +21,7 @@ import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Streams;
import java.util.Comparator;
import java.util.Map;
/**
@@ -37,6 +38,7 @@ import java.util.Map;
* the content to be displayed. The columns are:
*
* <ol>
* <li>the GKE service this action lives on
* <li>the URL path which maps to this action (with a "(*)" after it if the prefix flag is set)
* <li>the simple name of the action class
* <li>the allowable HTTP methods
@@ -49,12 +51,13 @@ import java.util.Map;
*/
public class RouterDisplayHelper {
private static final String SERVICE = "service";
private static final String PATH = "path";
private static final String CLASS = "class";
private static final String METHODS = "methods";
private static final String MINIMUM_LEVEL = "minLevel";
private static final String FORMAT = "%%-%ds %%-%ds %%-%ds %%-2s %%-%ds %%s";
private static final String FORMAT = "%%-%ds %%-%ds %%-%ds %%-%ds %%-2s %%-%ds %%s";
/** Returns a string representation of the routing map in the specified component. */
public static String extractHumanReadableRoutesFromComponent(Class<?> componentClass) {
@@ -76,6 +79,7 @@ public class RouterDisplayHelper {
private static String getFormatString(Map<String, Integer> columnWidths) {
return String.format(
FORMAT,
columnWidths.get(SERVICE),
columnWidths.get(PATH),
columnWidths.get(CLASS),
columnWidths.get(METHODS),
@@ -84,18 +88,13 @@ public class RouterDisplayHelper {
private static String headerToString(String formatString) {
return String.format(
formatString,
"PATH",
"CLASS",
"METHODS",
"OK",
"MIN",
"USER_POLICY");
formatString, "SERVICE", "PATH", "CLASS", "METHODS", "OK", "MIN", "USER_POLICY");
}
private static String routeToString(Route route, String formatString) {
return String.format(
formatString,
Action.ServiceGetter.get(route.action()).name(),
route.action().isPrefix() ? (route.action().path() + "(*)") : route.action().path(),
route.actionClass().getSimpleName(),
Joiner.on(",").join(route.action().method()),
@@ -107,12 +106,17 @@ public class RouterDisplayHelper {
private static String formatRoutes(Iterable<Route> routes) {
// Use the column header length as a minimum.
int serviceWidth = 7;
int pathWidth = 4;
int classWidth = 5;
int methodsWidth = 7;
int minLevelWidth = 3;
for (Route route : routes) {
int len =
int len = Action.ServiceGetter.get(route.action()).name().length();
if (len > serviceWidth) {
serviceWidth = len;
}
len =
route.action().isPrefix()
? (route.action().path().length() + 3)
: route.action().path().length();
@@ -135,6 +139,7 @@ public class RouterDisplayHelper {
final String formatString =
getFormatString(
new ImmutableMap.Builder<String, Integer>()
.put(SERVICE, serviceWidth)
.put(PATH, pathWidth)
.put(CLASS, classWidth)
.put(METHODS, methodsWidth)
@@ -143,6 +148,9 @@ public class RouterDisplayHelper {
return headerToString(formatString)
+ String.format("%n")
+ Streams.stream(routes)
.sorted(
Comparator.comparing(
(Route route) -> Action.ServiceGetter.get(route.action()).ordinal()))
.map(route -> routeToString(route, formatString))
.collect(joining(String.format("%n")));
}
@@ -16,7 +16,6 @@ package google.registry.request.auth;
import static com.google.common.net.HttpHeaders.AUTHORIZATION;
import com.google.auth.oauth2.TokenVerifier;
import com.google.common.collect.ImmutableList;
import dagger.Module;
import dagger.Provides;
@@ -24,6 +23,10 @@ import google.registry.config.RegistryConfig.Config;
import google.registry.request.auth.OidcTokenAuthenticationMechanism.IapOidcAuthenticationMechanism;
import google.registry.request.auth.OidcTokenAuthenticationMechanism.RegularOidcAuthenticationMechanism;
import google.registry.request.auth.OidcTokenAuthenticationMechanism.TokenExtractor;
import google.registry.request.auth.OidcTokenAuthenticationMechanism.TokenVerifier;
import google.registry.util.RegistryEnvironment;
import java.util.Map;
import javax.annotation.Nullable;
import javax.inject.Qualifier;
import javax.inject.Singleton;
@@ -35,9 +38,10 @@ public class AuthModule {
// See https://cloud.google.com/iap/docs/signed-headers-howto#securing_iap_headers.
public static final String IAP_HEADER_NAME = "X-Goog-IAP-JWT-Assertion";
public static final String BEARER_PREFIX = "Bearer ";
// TODO: Change the IAP audience format once we are on GKE.
// TODO (jianglai): Only use GKE audience once we are fully migrated to GKE.
// See: https://cloud.google.com/iap/docs/signed-headers-howto#verifying_the_jwt_payload
private static final String IAP_AUDIENCE_FORMAT = "/projects/%d/apps/%s";
private static final String IAP_GAE_AUDIENCE_FORMAT = "/projects/%d/apps/%s";
private static final String IAP_GKE_AUDIENCE_FORMAT = "/projects/%d/global/backendServices/%d";
private static final String IAP_ISSUER_URL = "https://cloud.google.com/iap";
private static final String REGULAR_ISSUER_URL = "https://accounts.google.com";
@@ -62,24 +66,35 @@ public class AuthModule {
@IapOidc
@Singleton
TokenVerifier provideIapTokenVerifier(
@Config("projectId") String projectId, @Config("projectIdNumber") long projectIdNumber) {
String audience = String.format(IAP_AUDIENCE_FORMAT, projectIdNumber, projectId);
return TokenVerifier.newBuilder().setAudience(audience).setIssuer(IAP_ISSUER_URL).build();
@Config("projectId") String projectId,
@Config("projectIdNumber") long projectIdNumber,
@Config("backendServiceIds") Map<String, Long> backendServiceIds) {
com.google.auth.oauth2.TokenVerifier.Builder tokenVerifierBuilder =
com.google.auth.oauth2.TokenVerifier.newBuilder().setIssuer(IAP_ISSUER_URL);
return (String service, String token) -> {
String audience;
if (RegistryEnvironment.isOnJetty()) {
long backendServiceId = backendServiceIds.get(service);
audience = String.format(IAP_GKE_AUDIENCE_FORMAT, projectIdNumber, backendServiceId);
} else {
audience = String.format(IAP_GAE_AUDIENCE_FORMAT, projectIdNumber, projectId);
}
return tokenVerifierBuilder.setAudience(audience).build().verify(token);
};
}
@Provides
@RegularOidc
@Singleton
TokenVerifier provideRegularTokenVerifier(@Config("oauthClientId") String clientId) {
return TokenVerifier.newBuilder().setAudience(clientId).setIssuer(REGULAR_ISSUER_URL).build();
}
@Provides
@RegularOidcFallback
@Singleton
TokenVerifier provideFallbackRegularTokenVerifier(
@Config("fallbackOauthClientId") String clientId) {
return TokenVerifier.newBuilder().setAudience(clientId).setIssuer(REGULAR_ISSUER_URL).build();
com.google.auth.oauth2.TokenVerifier tokenVerifier =
com.google.auth.oauth2.TokenVerifier.newBuilder()
.setAudience(clientId)
.setIssuer(REGULAR_ISSUER_URL)
.build();
return (@Nullable String service, String token) -> {
return tokenVerifier.verify(token);
};
}
@Provides
@@ -15,18 +15,19 @@
package google.registry.request.auth;
import static com.google.common.base.Preconditions.checkState;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.api.client.json.webtoken.JsonWebSignature;
import com.google.auth.oauth2.TokenVerifier;
import com.google.auth.oauth2.TokenVerifier.VerificationException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.persistence.VKey;
import google.registry.request.auth.AuthModule.IapOidc;
import google.registry.request.auth.AuthModule.RegularOidc;
import google.registry.request.auth.AuthModule.RegularOidcFallback;
import google.registry.request.auth.AuthSettings.AuthLevel;
import google.registry.util.RegistryEnvironment;
import jakarta.servlet.http.HttpServletRequest;
@@ -50,27 +51,23 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
public static final FluentLogger logger = FluentLogger.forEnclosingClass();
// A workaround that allows "use" of the OIDC authenticator when running local testing, i.e.
// A workaround that allows "use" of the OIDC authenticator when running local testing, i.e.,
// the RegistryTestServer
private static AuthResult authResultForTesting = null;
protected final TokenVerifier tokenVerifier;
protected final Optional<TokenVerifier> fallbackTokenVerifier;
protected final TokenExtractor tokenExtractor;
protected final TokenVerifier tokenVerifier;
private final ImmutableSet<String> serviceAccountEmails;
protected OidcTokenAuthenticationMechanism(
ImmutableSet<String> serviceAccountEmails,
TokenVerifier tokenVerifier,
@Nullable TokenVerifier fallbackTokenVerifier,
TokenExtractor tokenExtractor) {
TokenExtractor tokenExtractor,
TokenVerifier tokenVerifier) {
this.serviceAccountEmails = serviceAccountEmails;
this.tokenVerifier = tokenVerifier;
this.fallbackTokenVerifier = Optional.ofNullable(fallbackTokenVerifier);
this.tokenExtractor = tokenExtractor;
this.tokenVerifier = tokenVerifier;
}
@Override
@@ -86,7 +83,12 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
}
JsonWebSignature token = null;
try {
token = tokenVerifier.verify(rawIdToken);
String service = null;
if (RegistryEnvironment.isOnJetty()) {
String hostname = request.getServerName();
service = Splitter.on('.').split(hostname).iterator().next();
}
token = tokenVerifier.verify(service, rawIdToken);
} catch (Exception e) {
logger.atInfo().withCause(e).log(
"Failed OIDC verification attempt:\n%s",
@@ -96,20 +98,7 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
}
if (token == null) {
if (fallbackTokenVerifier.isPresent()) {
try {
token = fallbackTokenVerifier.get().verify(rawIdToken);
} catch (Exception e) {
logger.atInfo().withCause(e).log(
"Failed OIDC fallback verification attempt:\n%s",
RegistryEnvironment.get().equals(RegistryEnvironment.PRODUCTION)
? "Raw token redacted in prod"
: rawIdToken);
return AuthResult.NOT_AUTHENTICATED;
}
} else {
return AuthResult.NOT_AUTHENTICATED;
}
return AuthResult.NOT_AUTHENTICATED;
}
String email = (String) token.getPayload().get("email");
@@ -117,7 +106,8 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
logger.atWarning().log("No email address from the OIDC token:\n%s", token.getPayload());
return AuthResult.NOT_AUTHENTICATED;
}
Optional<User> maybeUser = UserDao.loadUser(email);
Optional<User> maybeUser =
tm().transact(() -> tm().loadByKeyIfPresent(VKey.create(User.class, email)));
if (maybeUser.isPresent()) {
return AuthResult.createUser(maybeUser.get());
}
@@ -153,6 +143,12 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
String extract(HttpServletRequest request);
}
@FunctionalInterface
protected interface TokenVerifier {
@Nullable
JsonWebSignature verify(@Nullable String service, String rawToken) throws VerificationException;
}
/**
* A mechanism to authenticate HTTP requests that have gone through the GCP Identity-Aware Proxy.
*
@@ -169,9 +165,9 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
@Inject
protected IapOidcAuthenticationMechanism(
@Config("allowedServiceAccountEmails") ImmutableSet<String> serviceAccountEmails,
@IapOidc TokenVerifier tokenVerifier,
@IapOidc TokenExtractor tokenExtractor) {
super(serviceAccountEmails, tokenVerifier, null, tokenExtractor);
@IapOidc TokenExtractor tokenExtractor,
@IapOidc TokenVerifier tokenVerifier) {
super(serviceAccountEmails, tokenExtractor, tokenVerifier);
}
}
@@ -190,10 +186,9 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
@Inject
protected RegularOidcAuthenticationMechanism(
@Config("allowedServiceAccountEmails") ImmutableSet<String> serviceAccountEmails,
@RegularOidc TokenVerifier tokenVerifier,
@RegularOidcFallback TokenVerifier fallbackTokenVerifier,
@RegularOidc TokenExtractor tokenExtractor) {
super(serviceAccountEmails, tokenVerifier, fallbackTokenVerifier, tokenExtractor);
@RegularOidc TokenExtractor tokenExtractor,
@RegularOidc TokenVerifier tokenVerifier) {
super(serviceAccountEmails, tokenExtractor, tokenVerifier);
}
}
}
@@ -22,7 +22,6 @@ import com.google.common.collect.ImmutableMap;
import google.registry.model.console.GlobalRole;
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.tools.params.KeyValueMapParameter.StringToRegistrarRoleMap;
import java.util.Optional;
@@ -95,8 +94,6 @@ public abstract class CreateOrUpdateUserCommand extends ConfirmingCommand {
builder.setRegistryLockEmailAddress(registryLockEmailAddress);
}
}
User newUser = builder.build();
UserDao.saveUser(newUser);
tm().put(builder.build());
}
}
@@ -16,24 +16,24 @@ package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.model.console.User.grantIapPermission;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.beust.jcommander.Parameters;
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.persistence.VKey;
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 {
public class CreateUserCommand extends CreateOrUpdateUserCommand implements CommandWithConnection {
private ServiceConnection connection;
@Inject IamClient iamClient;
@Inject CloudTasksUtils cloudTasksUtils;
@Inject
@Config("gSuiteConsoleUserGroupEmailAddress")
Optional<String> maybeGroupEmailAddress;
@@ -41,14 +41,20 @@ public class CreateUserCommand extends CreateOrUpdateUserCommand {
@Nullable
@Override
User getExistingUser(String email) {
checkArgument(UserDao.loadUser(email).isEmpty(), "A user with email %s already exists", email);
checkArgument(
!tm().exists(VKey.create(User.class, email)), "A user with email %s already exists", email);
return null;
}
@Override
protected String execute() throws Exception {
String ret = super.execute();
grantIapPermission(email, maybeGroupEmailAddress, cloudTasksUtils, iamClient);
grantIapPermission(email, maybeGroupEmailAddress, null, connection, iamClient);
return ret;
}
@Override
public void setConnection(ServiceConnection connection) {
this.connection = connection;
}
}
@@ -14,28 +14,27 @@
package google.registry.tools;
import static com.google.api.client.util.Preconditions.checkArgument;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
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 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.persistence.VKey;
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 {
public class DeleteUserCommand extends ConfirmingCommand implements CommandWithConnection {
private ServiceConnection connection;
@Inject IamClient iamClient;
@Inject CloudTasksUtils cloudTasksUtils;
@Inject
@Config("gSuiteConsoleUserGroupEmailAddress")
Optional<String> maybeGroupEmailAddress;
@@ -47,7 +46,9 @@ public class DeleteUserCommand extends ConfirmingCommand {
@Override
protected String prompt() {
checkArgumentNotNull(email, "Email must be provided");
checkArgumentPresent(UserDao.loadUser(email), "Email does not correspond to a valid user");
checkArgument(
tm().transact(() -> tm().exists(VKey.create(User.class, email))),
"Email does not correspond to a valid user");
return String.format("Delete user with email %s?", email);
}
@@ -55,11 +56,16 @@ public class DeleteUserCommand extends ConfirmingCommand {
protected String execute() throws Exception {
tm().transact(
() -> {
Optional<User> optionalUser = UserDao.loadUser(email);
checkArgumentPresent(optionalUser, "Email no longer corresponds to a valid user");
tm().delete(optionalUser.get());
VKey<User> key = VKey.create(User.class, email);
checkArgument(tm().exists(key), "Email no longer corresponds to a valid user");
tm().delete(key);
});
User.revokeIapPermission(email, maybeGroupEmailAddress, cloudTasksUtils, iamClient);
User.revokeIapPermission(email, maybeGroupEmailAddress, null, connection, iamClient);
return String.format("Deleted user with email %s", email);
}
@Override
public void setConnection(ServiceConnection connection) {
this.connection = connection;
}
}
@@ -343,7 +343,7 @@ public final class DomainLockUtils {
.orElseThrow(
() ->
new IllegalArgumentException(
String.format("Invalid verification code %s", verificationCode)));
String.format("Invalid verification code \"%s\"", verificationCode)));
}
private void applyLockStatuses(RegistryLock lock, DateTime lockTime, boolean isAdmin) {
@@ -293,7 +293,7 @@ class GenerateAllocationTokensCommand implements Command {
// tokens should only be scheduled to end with a brief time period before the status
// transition occurs so that no new domains are registered using that token between when the
// status is scheduled and when the transition occurs.
// TODO(@sarahbot): Create a cleaner way to handle ending bulk pricing packages once we
// TODO(b/261763205): Create a cleaner way to handle ending bulk pricing packages once we
// actually have customers using them
boolean hasEnding =
tokenStatusTransitions.containsValue(TokenStatus.ENDED)
@@ -14,10 +14,12 @@
package google.registry.tools;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.persistence.VKey;
import java.util.List;
/** Command to display one or more users. */
@@ -29,11 +31,14 @@ public class GetUserCommand implements Command {
@Override
public void run() throws Exception {
for (String emailAddress : mainParameters) {
System.out.println(
UserDao.loadUser(emailAddress)
.map(User::toString)
.orElse(String.format("No user with email address %s", emailAddress)));
}
tm().transact(
() -> {
for (String emailAddress : mainParameters) {
System.out.println(
tm().loadByKeyIfPresent(VKey.create(User.class, emailAddress))
.map(User::toString)
.orElse(String.format("No user with email address %s", emailAddress)));
}
});
}
}
@@ -41,7 +41,6 @@ import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nullable;
@@ -87,12 +86,6 @@ final class RegistrarPocCommand extends MutatingCommand {
+ " and will be used as the console login email, if --login_email is not specified.")
String email;
@Nullable
@Parameter(
names = "--login_email",
description = "Console login email address. If not specified, --email will be used.")
String loginEmail;
@Nullable
@Parameter(
names = "--registry_lock_email",
@@ -115,13 +108,6 @@ final class RegistrarPocCommand extends MutatingCommand {
validateWith = OptionalPhoneNumberParameter.class)
private Optional<String> fax;
@Nullable
@Parameter(
names = "--allow_console_access",
description = "Enable or disable access to the registrar console for this contact.",
arity = 1)
Boolean allowConsoleAccess;
@Nullable
@Parameter(
names = "--visible_in_whois_as_admin",
@@ -173,7 +159,7 @@ final class RegistrarPocCommand extends MutatingCommand {
protected void init() throws Exception {
checkArgument(mainParameters.size() == 1,
"Must specify exactly one client identifier: %s", ImmutableList.copyOf(mainParameters));
String clientId = mainParameters.get(0);
String clientId = mainParameters.getFirst();
Registrar registrar =
checkArgumentPresent(
Registrar.loadByRegistrarId(clientId), "Registrar %s not found", clientId);
@@ -261,9 +247,6 @@ final class RegistrarPocCommand extends MutatingCommand {
}
builder.setTypes(nullToEmpty(contactTypes));
if (Objects.equals(allowConsoleAccess, Boolean.TRUE)) {
builder.setLoginEmailAddress(loginEmail == null ? email : loginEmail);
}
if (visibleInWhoisAsAdmin != null) {
builder.setVisibleInWhoisAsAdmin(visibleInWhoisAsAdmin);
}
@@ -308,13 +291,6 @@ final class RegistrarPocCommand extends MutatingCommand {
if (visibleInDomainWhoisAsAbuse != null) {
builder.setVisibleInDomainWhoisAsAbuse(visibleInDomainWhoisAsAbuse);
}
if (allowConsoleAccess != null) {
if (allowConsoleAccess.equals(Boolean.TRUE)) {
builder.setLoginEmailAddress(loginEmail == null ? email : loginEmail);
} else {
builder.setLoginEmailAddress(null);
}
}
if (allowedToSetRegistryLockPassword != null) {
builder.setAllowedToSetRegistryLockPassword(allowedToSetRegistryLockPassword);
}

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