1
0
mirror of https://github.com/google/nomulus synced 2026-05-19 06:11:49 +00:00

Compare commits

...

18 Commits

Author SHA1 Message Date
Ben McIlwain
8d22c2a8d8 Remove unnecessary if statements in CloudDnsWriter (#2071)
The condition they are guarding against cannot possibly occur.
2023-07-07 13:15:02 -04:00
gbrodman
fe19f0fe78 Clean up issues with RDAP redaction (#2067)
Instead of using REDACTED FOR PRIVACY everywhere we should just include
the empty string (this is what the spec says, what other gTLD registrars
do, and what the RDAP conformance tool at
https://github.com/icann/rdap-conformance-tool says to do.

In the contact VCards, we omit redacted fields entirely unless the spec
requires that they exist (the version number and an empty 'fn' field).
This also applies to the "handle" field.

Eventually we will probably want to add the redaction extension but
that's not RFCed yet and isn't required for the August RDAP conformance
deadline.
2023-07-06 14:48:51 -04:00
Ben McIlwain
599a55d5b1 Fix the output slightly when running nomulus update_premium_list (#2065)
It was previously calling toString() on an Optional<PremiumList> which was
unnecessarily verbose. The existing premium list is required to be present
anyway.
2023-07-06 13:46:28 -04:00
Pavlo Tkach
845f792044 Add better integration for console formatting check and apply (#2070) 2023-07-06 12:33:32 -04:00
Ben McIlwain
ad68052524 Add minor refactoring follow-up for RefreshDnsForAllDomainsAction (#2063)
This is a follow-on to comments in PR #2037. It makes the main loop cleaner and
also removes ambiguities around database handling when the first query is run
with the cursor still empty because no results have been found yet.
2023-07-05 15:09:20 -04:00
gbrodman
04c6652793 Fix minor RDAP typos (#2062)
See https://buganizer.corp.google.com/issues/252317192 for more info,
these are just the low-hanging fruit (removing a www and fixing a typo
in a status)
2023-06-30 12:24:39 -04:00
Lai Jiang
5658fbe8bd Remove stale references to App Engine in CloudTasksUtils (#2064)
<!-- Reviewable:start -->
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/google/nomulus/2064)
<!-- Reviewable:end -->
2023-06-29 16:48:44 -04:00
gbrodman
a4540a847a Add configurable discount on sunrise domain creates (#2056)
Previously we had a 15% discount applied at invoicing time. We got rid of
that inadvertently in 2022 and we want to add it back, but instead of
being applied at invoicing time we'll just apply it directly to the
creation cost when creating the billing events.

Note: previous behavior didn't care about standard vs premium pricing so
we don't either

https://buganizer.corp.google.com/issues/287070313 is a bug for the
issue, and
https://github.com/google/nomulus/pull/1710/files#diff-5097b0ef57578718444ea6b9d4c6cb32f655686a37e2ca3dd96ad2db86a77f06L151-L170
is the section of the pull request that inadvertently removed it
2023-06-27 18:58:44 -04:00
Lai Jiang
fdfbb9572d Refactor OIDC-based auth mechanism (#2049)
This PR changes the two flavors of OIDC authentication mechanisms to
verify the same audience. This allows the same token to pass both
mechanisms. Previously the regular OIDC flavor uses the project id as
its required audience, which does not work for local user credentials
(such as ones used by the nomulus tool), which requires a valid OAuth
client ID as audience when minting the token (project id is NOT a valid
OAuth client ID).

I considered allowing multiple audiences, but the result is not as clean
as just using the same everywhere, because the fall-through logic would
have generated a lot of noises for failed attempts.

This PR also changes the client side to solely use OIDC token whenever
possible, including the proxy, cloud scheduler and cloud tasks. The nomulus
tool still uses OAuth access token by default because it requires USER level
authentication, which in turn requires us to fill the User table with objects
corresponding to the email address of everyone needing access to the tool.

TESTED=verified each client is able to make authenticated calls on QA with or
without IAP.
2023-06-27 13:10:31 -04:00
sarahcaseybot
cf1a148208 Add Java changes for new breakglass_mode column on Tld (#2053)
* Add Java changes for new breakglass_mode column on Tld

* Add generated sql schema
2023-06-22 12:55:42 -04:00
sarahcaseybot
6b54b69163 Add batching to the RefreshDnsForAllDomainsAction (#2037)
* Add an includeDeleted option to RefreshDnsForAllDomainsAction

* Add batching to the query

* Some refactoring

* Make batch size configurable

* Set status to ok

* Combine into one transaction

* Remove smear mintes parameter

* Only pass in lastInPreviousBatch
2023-06-22 12:54:40 -04:00
Weimin Yu
a839ec434e Add CurlCommand option to connect to canary (#2060)
Add a --canary option (default to false) to the CurlCommand that allows
connection to the canary endpoints.

During canary analysis, only the DEFAULT-canary receives traffic. This
new flag allows use to test other canary services manually using the
curl command.
2023-06-22 11:20:41 -04:00
Pavlo Tkach
86b62ebe76 Add registrar selection functionality (#2054) 2023-06-14 16:51:54 -04:00
sarahcaseybot
952a92a5db Separate load and verify transaction from refresh transaction in RefreshDnsAction (#2055) 2023-06-13 18:12:08 -04:00
Pavlo Tkach
bc57f319e5 Add console /registrars GET endpoint (#2050) 2023-06-09 16:57:26 -04:00
Pavlo Tkach
a9aaa11801 Remove contacts with empty type from console GET /contacts response (#2052) 2023-06-09 15:11:05 -04:00
Pavlo Tkach
b319eff7cd Add console UI main layout, settings page and contact settings (#1989)
* Header initialized

* Added settings page

* switch history mode to hash

* Add eslint

* Add prettier and reformat

* Contact details in a bottom sheet for mobile devices

* Add contact details events abstraction

* Fix formatting issue and update deps versions
2023-06-09 14:20:08 -04:00
Weimin Yu
894d05ce4e Add Gmail Client and set up tests (#2048)
* Add Gmail Client and set up tests

Add a Gmail client and manually triggered email tests in
CannedScriptExecutionActon.

We will test Gmail with Google Workspace in Sandbox, since Alpha and
Crash are not properly set up for Google Workspace, and we have not
figured out why.
2023-06-09 13:06:21 -04:00
177 changed files with 6994 additions and 12322 deletions

View File

@@ -505,6 +505,7 @@ task javaIncrementalFormatCheck {
println("Omitting format check: not in a git directory.")
}
}
dependsOn('console-webapp:checkFormatting')
}
// Shows how modified lines in Java source files will change after formatting.
@@ -522,6 +523,7 @@ task javaIncrementalFormatApply {
doLast {
invokeJavaDiffFormatScript("format")
}
dependsOn('console-webapp:applyFormatting')
}
task javadoc(type: Javadoc) {
@@ -564,14 +566,18 @@ task deployCloudSchedulerAndQueue {
def env = environment
if (!prodOrSandboxEnv) {
exec {
workingDir "${rootDir}/release/builder/"
commandLine 'go', 'run',
"${rootDir}/release/builder/deployCloudSchedulerAndQueue.go",
"./deployCloudSchedulerAndQueue.go",
"${rootDir}/core/src/main/java/google/registry/config/files/nomulus-config-${env}.yaml",
"${rootDir}/core/src/main/java/google/registry/env/${env}/default/WEB-INF/cloud-scheduler-tasks.xml",
"domain-registry-${env}"
}
exec {
workingDir "${rootDir}/release/builder/"
commandLine 'go', 'run',
"${rootDir}/release/builder/deployCloudSchedulerAndQueue.go",
"./deployCloudSchedulerAndQueue.go",
"${rootDir}/core/src/main/java/google/registry/config/files/nomulus-config-${env}.yaml",
"${rootDir}/core/src/main/java/google/registry/env/common/default/WEB-INF/cloud-tasks-queue.xml",
"domain-registry-${env}"
}

View File

@@ -0,0 +1,47 @@
{
"root": true,
"ignorePatterns": [
"projects/**/*"
],
"overrides": [
{
"files": [
"*.ts"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "app",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "app",
"style": "kebab-case"
}
],
"eol-last": ["error", "always"]
}
},
{
"files": [
"*.html"
],
"extends": [
"plugin:@angular-eslint/template/recommended"
],
"rules": {}
}
]
}

View File

View File

@@ -0,0 +1 @@
{}

View File

@@ -102,8 +102,23 @@
],
"scripts": []
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
}
}
}
},
"cli": {
"analytics": false,
"schematicCollections": [
"@angular-eslint/schematics"
]
}
}

View File

@@ -50,5 +50,19 @@ task buildConsoleWebappProd(type: Exec) {
args 'run', 'build'
}
task applyFormatting(type: Exec) {
workingDir "${consoleDir}/"
executable 'npm'
args 'run', 'prettify'
}
task checkFormatting(type: Exec) {
workingDir "${consoleDir}/"
executable 'npm'
args 'run', 'prettify:check'
}
tasks.runConsoleWebappUnitTests.dependsOn(tasks.npmInstallDeps)
tasks.buildConsoleWebappProd.dependsOn(tasks.npmInstallDeps)
tasks.applyFormatting.dependsOn(tasks.npmInstallDeps)
tasks.checkFormatting.dependsOn(tasks.npmInstallDeps)

View File

@@ -1,7 +1,7 @@
{
"/registrar":
"/console-api":
{
"target": "http://localhost:8080/registrar",
"secure": false
"target": "http://localhost:8080",
"secure": true
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,10 @@
"watch": "ng build --watch --configuration development",
"test": "ng test --browsers=ChromeHeadless --watch=false",
"run:dev": "",
"start:dev": "concurrently \"./../gradlew :core:runTestServer\" \"ng serve --proxy-config dev-proxy.config.json\""
"prettify": "npx prettier --write ./src/",
"prettify:check": "npx prettier --check ./src/",
"start:dev": "concurrently \"./../gradlew :core:runTestServer\" \"ng serve --proxy-config dev-proxy.config.json\"",
"lint": "ng lint"
},
"private": true,
"dependencies": {
@@ -29,17 +32,26 @@
},
"devDependencies": {
"@angular-devkit/build-angular": "^15.2.4",
"@angular-eslint/builder": "15.2.1",
"@angular-eslint/eslint-plugin": "15.2.1",
"@angular-eslint/eslint-plugin-template": "15.2.1",
"@angular-eslint/schematics": "15.2.1",
"@angular-eslint/template-parser": "15.2.1",
"@angular/cli": "~15.2.4",
"@angular/compiler-cli": "^15.2.2",
"@types/jasmine": "~4.0.0",
"@types/node": "^18.11.18",
"@typescript-eslint/eslint-plugin": "5.48.2",
"@typescript-eslint/parser": "5.48.2",
"concurrently": "^7.6.0",
"eslint": "^8.33.0",
"jasmine-core": "~4.3.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.0.0",
"prettier": "2.8.7",
"typescript": "~4.9.4"
}
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -14,16 +14,62 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import {TldsComponent} from './tlds/tlds.component';
import {HomeComponent} from './home/home.component';
import { TldsComponent } from './tlds/tlds.component';
import { HomeComponent } from './home/home.component';
import { SettingsComponent } from './settings/settings.component';
import SettingsContactComponent from './settings/contact/contact.component';
import SettingsRegistrarsComponent from './settings/registrars/registrars.component';
import SettingsWhoisComponent from './settings/whois/whois.component';
import SettingsUsersComponent from './settings/users/users.component';
import SettingsSecurityComponent from './settings/security/security.component';
import { RegistrarGuard } from './registrar/registrar.guard';
import { RegistrarComponent } from './registrar/registrar.component';
const routes: Routes = [
{ path: 'home', component: HomeComponent },
{ path: 'tlds', component: TldsComponent },
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: 'registrars', component: RegistrarComponent },
{ path: 'home', component: HomeComponent, canActivate: [RegistrarGuard] },
{ path: 'tlds', component: TldsComponent, canActivate: [RegistrarGuard] },
{
path: 'settings',
component: SettingsComponent,
canActivate: [RegistrarGuard],
children: [
{
path: '',
redirectTo: 'contact',
pathMatch: 'full',
},
{
path: 'contact',
component: SettingsContactComponent,
},
{
path: 'whois',
component: SettingsWhoisComponent,
},
{
path: 'security',
component: SettingsSecurityComponent,
},
{
path: 'epp-password',
component: SettingsSecurityComponent,
},
{
path: 'users',
component: SettingsUsersComponent,
},
{
path: 'registrars',
component: SettingsRegistrarsComponent,
},
],
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
imports: [RouterModule.forRoot(routes, { useHash: true })],
exports: [RouterModule],
})
export class AppRoutingModule { }
export class AppRoutingModule {}

View File

@@ -1,14 +1,24 @@
<div class="toolbar" role="banner">
Nomulus Console
<div class="console-app">
<app-header (toggleNavOpen)="sidenav.toggle()"></app-header>
<mat-sidenav-container class="console-app__content-wrapper">
<mat-sidenav #sidenav class="console-app__sidebar">
<mat-nav-list>
<a mat-list-item [routerLink]="'/home'" routerLinkActive="active">
Home page
</a>
<a mat-list-item [routerLink]="'/tlds'" routerLinkActive="active">
TLDS
</a>
<a mat-list-item [routerLink]="'/settings'" routerLinkActive="active">
Settings
</a>
<a mat-list-item [routerLink]="'/registrars'" routerLinkActive="active">
Select Registrar
</a>
</mat-nav-list>
</mat-sidenav>
<mat-sidenav-content class="console-app__content">
<router-outlet></router-outlet>
</mat-sidenav-content>
</mat-sidenav-container>
</div>
<div class="content" role="main">
<nav>
<ul>
<li><a routerLink="/home" routerLinkActive="active" ariaCurrentWhenActive="page">Home page</a></li>
<li><a routerLink="/tlds" routerLinkActive="active" ariaCurrentWhenActive="page">TLDs</a></li>
</ul>
</nav>
</div>
<router-outlet></router-outlet>

View File

@@ -1,4 +1,4 @@
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
// Copyright 2023 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.
@@ -13,7 +13,8 @@
// limitations under the License.
:host {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 14px;
color: #333;
box-sizing: border-box;
@@ -21,15 +22,25 @@
-moz-osx-font-smoothing: grayscale;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 8px 0;
}
p {
margin: 0;
.console-app {
display: flex;
flex-direction: column;
height: 100%;
&__content-wrapper {
flex: 1;
margin-top: -12px;
padding-bottom: 36px;
}
&__sidebar {
min-width: 300px;
a::before {
background-color: transparent;
}
.active {
background: #eae1e1;
}
}
&__content {
margin: 12px 24px;
}
}

View File

@@ -19,12 +19,8 @@ import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
imports: [RouterTestingModule],
declarations: [AppComponent],
}).compileComponents();
});
@@ -33,5 +29,4 @@ describe('AppComponent', () => {
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
});

View File

@@ -17,8 +17,6 @@ import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.less']
styleUrls: ['./app.component.less'],
})
export class AppComponent {
}
export class AppComponent {}

View File

@@ -1,4 +1,4 @@
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -14,28 +14,46 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {MaterialModule} from './material.module';
import { MaterialModule } from './material.module';
import { BackendService } from './shared/services/backend.service';
import { HomeComponent } from './home/home.component';
import { TldsComponent } from './tlds/tlds.component';
import { HeaderComponent } from './header/header.component';
import { SettingsComponent } from './settings/settings.component';
import SettingsContactComponent, {
ContactDetailsDialogComponent,
} from './settings/contact/contact.component';
import { HttpClientModule } from '@angular/common/http';
import { RegistrarComponent } from './registrar/registrar.component';
import { RegistrarGuard } from './registrar/registrar.guard';
@NgModule({
declarations: [
AppComponent,
HomeComponent,
TldsComponent,
HeaderComponent,
SettingsComponent,
SettingsContactComponent,
ContactDetailsDialogComponent,
RegistrarComponent,
],
imports: [
HttpClientModule,
FormsModule,
MaterialModule,
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule
BrowserAnimationsModule,
],
providers: [],
bootstrap: [AppComponent]
providers: [BackendService, RegistrarGuard],
bootstrap: [AppComponent],
})
export class AppModule { }
export class AppModule {}

View File

@@ -0,0 +1,15 @@
<p>
<mat-toolbar color="primary">
<button mat-icon-button aria-label="Open menu" (click)="toggleNavPane()">
<mat-icon>menu</mat-icon>
</button>
<span>Google Registry</span>
<span class="spacer"></span>
<button mat-icon-button aria-label="Open FAQ">
<mat-icon>question_mark</mat-icon>
</button>
<button mat-icon-button aria-label="Open user info">
<mat-icon>person</mat-icon>
</button>
</mat-toolbar>
</p>

View File

@@ -0,0 +1,17 @@
// Copyright 2023 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.
.spacer {
flex: 1;
}

View File

@@ -0,0 +1,36 @@
// Copyright 2023 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 { ComponentFixture, TestBed } from '@angular/core/testing';
import { HeaderComponent } from './header.component';
describe('HeaderComponent', () => {
let component: HeaderComponent;
let fixture: ComponentFixture<HeaderComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [HeaderComponent],
}).compileComponents();
fixture = TestBed.createComponent(HeaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,31 @@
// Copyright 2023 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, EventEmitter, Output } from '@angular/core';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.less'],
})
export class HeaderComponent {
private isNavOpen = false;
@Output() toggleNavOpen = new EventEmitter<boolean>();
toggleNavPane() {
this.isNavOpen = !this.isNavOpen;
this.toggleNavOpen.emit(this.isNavOpen);
}
}

View File

@@ -1,14 +1,21 @@
<h3>Recent Activity</h3>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8 console-home__activity">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<table
mat-table
[dataSource]="dataSource"
class="mat-elevation-z8 console-home__activity"
>
<ng-container
*ngFor="let column of columns"
[matColumnDef]="column.columnDef"
>
<th mat-header-cell *matHeaderCellDef>
{{column.header}}
{{ column.header }}
</th>
<td mat-cell *matCellDef="let row">
{{column.cell(row)}}
{{ column.cell(row) }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>

View File

@@ -11,4 +11,3 @@
// 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.

View File

@@ -15,7 +15,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeComponent } from './home.component';
import {MaterialModule} from '../material.module';
import { MaterialModule } from '../material.module';
describe('HomeComponent', () => {
let component: HomeComponent;
@@ -24,9 +24,8 @@ describe('HomeComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MaterialModule],
declarations: [ HomeComponent ]
})
.compileComponents();
declarations: [HomeComponent],
}).compileComponents();
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;

View File

@@ -19,76 +19,213 @@ export interface ActivityRecord {
userName: string;
registrarName: string;
timestamp: string;
details: string
details: string;
}
const MOCK_DATA: ActivityRecord[] = [
{eventType: "Export DUMS", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Update Contact", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Delete Domain", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Export DUMS", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Update Contact", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Delete Domain", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Export DUMS", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Update Contact", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Delete Domain", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Export DUMS", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Update Contact", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Delete Domain", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Export DUMS", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Update Contact", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Delete Domain", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Export DUMS", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Update Contact", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Delete Domain", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Export DUMS", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Update Contact", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Delete Domain", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Export DUMS", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Update Contact", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{eventType: "Delete Domain", userName:"user3", registrarName: "registrar1", timestamp: "2022-03-15T19:46:39.007", details: "All Domains under management exported as .csv file" },
{
eventType: 'Export DUMS',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Update Contact',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Delete Domain',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Export DUMS',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Update Contact',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Delete Domain',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Export DUMS',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Update Contact',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Delete Domain',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Export DUMS',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Update Contact',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Delete Domain',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Export DUMS',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Update Contact',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Delete Domain',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Export DUMS',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Update Contact',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Delete Domain',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Export DUMS',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Update Contact',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Delete Domain',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Export DUMS',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Update Contact',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
{
eventType: 'Delete Domain',
userName: 'user3',
registrarName: 'registrar1',
timestamp: '2022-03-15T19:46:39.007',
details: 'All Domains under management exported as .csv file',
},
];
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.less']
styleUrls: ['./home.component.less'],
})
export class HomeComponent {
columns = [
{
columnDef: 'eventType',
header: 'Event Type',
cell:(record: ActivityRecord) => `${record.eventType}`,
cell: (record: ActivityRecord) => `${record.eventType}`,
},
{
columnDef: 'userName',
header: 'User',
cell: (record: ActivityRecord) => `${record.userName}`,
cell: (record: ActivityRecord) => `${record.userName}`,
},
{
columnDef: 'registrarName',
header: 'Registrar',
cell: (record: ActivityRecord) => `${record.registrarName}`,
cell: (record: ActivityRecord) => `${record.registrarName}`,
},
{
columnDef: 'timestamp',
header: 'Timestamp',
cell: (record: ActivityRecord) => `${record.timestamp}`,
cell: (record: ActivityRecord) => `${record.timestamp}`,
},
{
columnDef: 'details',
header: 'Details',
cell: (record: ActivityRecord) => `${record.details}`,
cell: (record: ActivityRecord) => `${record.details}`,
},
];
dataSource = MOCK_DATA;
displayedColumns = this.columns.map(c => c.columnDef);
constructor() {
}
displayedColumns = this.columns.map((c) => c.columnDef);
}

View File

@@ -1,4 +1,4 @@
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
// Copyright 2023 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.
@@ -12,15 +12,73 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import {NgModule} from '@angular/core';
import {MatCardModule} from '@angular/material/card';
import {MatTableModule} from '@angular/material/table';
import { NgModule } from '@angular/core';
import { A11yModule } from '@angular/cdk/a11y';
import { CdkTableModule } from '@angular/cdk/table';
import { CdkTreeModule } from '@angular/cdk/tree';
import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatBottomSheetModule } from '@angular/material/bottom-sheet';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialogModule } from '@angular/material/dialog';
import { MatDividerModule } from '@angular/material/divider';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list';
import { MatMenuModule } from '@angular/material/menu';
import { MatNativeDateModule, MatRippleModule } from '@angular/material/core';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatRadioModule } from '@angular/material/radio';
import { MatSelectModule } from '@angular/material/select';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatTreeModule } from '@angular/material/tree';
import { OverlayModule } from '@angular/cdk/overlay';
import { CdkMenuModule } from '@angular/cdk/menu';
import { DialogModule } from '@angular/cdk/dialog';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatSnackBarModule } from '@angular/material/snack-bar';
const MATERIAL_MODULES = [
MatCardModule,
MatTableModule,
];
@NgModule({imports: MATERIAL_MODULES, exports: MATERIAL_MODULES})
export class MaterialModule {
}
@NgModule({
exports: [
A11yModule,
CdkMenuModule,
CdkTableModule,
CdkTreeModule,
MatBadgeModule,
MatButtonModule,
MatButtonToggleModule,
MatBottomSheetModule,
MatCardModule,
MatCheckboxModule,
MatDialogModule,
MatDividerModule,
MatGridListModule,
MatIconModule,
MatInputModule,
MatListModule,
MatMenuModule,
MatNativeDateModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatRadioModule,
MatRippleModule,
MatSelectModule,
MatSidenavModule,
MatTableModule,
MatTabsModule,
MatToolbarModule,
MatTooltipModule,
MatTreeModule,
OverlayModule,
DialogModule,
MatSnackBarModule,
],
})
export class MaterialModule {}

View File

@@ -0,0 +1,17 @@
<div class="console-app__registrar">
<h4 class="console-app__title">
{{ registrarService.activeRegistrarId === "" ? "Select" : "Switch" }}
registrar:
</h4>
<mat-form-field>
<mat-label>Registrar</mat-label>
<mat-select [(ngModel)]="registrarService.activeRegistrarId">
<mat-option
*ngFor="let registrar of registrarService.registrars"
[value]="registrar"
>
{{ registrar }}
</mat-option>
</mat-select>
</mat-form-field>
</div>

View File

@@ -0,0 +1,11 @@
.console-app {
&__registrar {
display: flex;
justify-content: center;
align-items: center;
margin-top: 1rem;
}
&__title {
margin-right: 1rem;
}
}

View File

@@ -0,0 +1,44 @@
// Copyright 2023 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 { ComponentFixture, TestBed } from '@angular/core/testing';
import { RegistrarComponent } from './registrar.component';
import { BackendService } from '../shared/services/backend.service';
import { ActivatedRoute } from '@angular/router';
import { HttpClientTestingModule } from '@angular/common/http/testing';
describe('RegistrarComponent', () => {
let component: RegistrarComponent;
let fixture: ComponentFixture<RegistrarComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [RegistrarComponent],
imports: [HttpClientTestingModule],
providers: [
BackendService,
{ provide: ActivatedRoute, useValue: {} as ActivatedRoute },
],
}).compileComponents();
fixture = TestBed.createComponent(RegistrarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,45 @@
// Copyright 2023 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.service';
import { ActivatedRoute, Router } from '@angular/router';
@Component({
selector: 'app-registrar',
templateUrl: './registrar.component.html',
styleUrls: ['./registrar.component.less'],
})
export class RegistrarComponent {
private lastActiveRegistrarId: string;
constructor(
private route: ActivatedRoute,
protected registrarService: RegistrarService,
private router: Router
) {
this.lastActiveRegistrarId = registrarService.activeRegistrarId;
}
ngDoCheck() {
if (
this.registrarService.activeRegistrarId &&
this.registrarService.activeRegistrarId !== this.lastActiveRegistrarId &&
this.route.snapshot.paramMap.get('nextUrl')
) {
this.lastActiveRegistrarId = this.registrarService.activeRegistrarId;
this.router.navigate([this.route.snapshot.paramMap.get('nextUrl')]);
}
}
}

View File

@@ -0,0 +1,64 @@
// Copyright 2023 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 { TestBed } from '@angular/core/testing';
import { RegistrarGuard } from './registrar.guard';
import { Router, RouterStateSnapshot } from '@angular/router';
import { RegistrarService } from './registrar.service';
describe('RegistrarGuard', () => {
let guard: RegistrarGuard;
let dummyRegistrarService: RegistrarService;
let routeSpy: Router;
let dummyRoute: RouterStateSnapshot = {} as RouterStateSnapshot;
beforeEach(() => {
routeSpy = jasmine.createSpyObj<Router>('Router', ['navigate']);
dummyRegistrarService = { activeRegistrarId: '' } as RegistrarService;
TestBed.configureTestingModule({
providers: [
RegistrarGuard,
{ provide: Router, useValue: routeSpy },
{ provide: RegistrarService, useValue: dummyRegistrarService },
],
});
});
it('should not be able to activate when activeRegistrarId is empty', () => {
guard = TestBed.inject(RegistrarGuard);
const res = guard.canActivate(dummyRoute);
expect(res).toBeFalsy();
});
it('should be able to activate when activeRegistrarId is not empty', () => {
TestBed.overrideProvider(RegistrarService, {
useValue: { activeRegistrarId: 'value' },
});
guard = TestBed.inject(RegistrarGuard);
const res = guard.canActivate(dummyRoute);
expect(res).toBeTrue();
});
it('should navigate to registrars when activeRegistrarId is empty', () => {
const dummyRoute = { url: '/value' } as RouterStateSnapshot;
guard = TestBed.inject(RegistrarGuard);
guard.canActivate(dummyRoute);
expect(routeSpy.navigate).toHaveBeenCalledOnceWith([
'/registrars',
{ nextUrl: '/value' },
]);
});
});

View File

@@ -0,0 +1,34 @@
// Copyright 2023 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 { Router, RouterStateSnapshot } from '@angular/router';
import { RegistrarService } from './registrar.service';
@Injectable({
providedIn: 'root',
})
export class RegistrarGuard {
constructor(
private router: Router,
private registrarService: RegistrarService
) {}
canActivate(state: RouterStateSnapshot): Promise<boolean> | boolean {
if (this.registrarService.activeRegistrarId) {
return true;
}
return this.router.navigate([`/registrars`, { nextUrl: state.url }]);
}
}

View File

@@ -0,0 +1,35 @@
// Copyright 2023 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 { TestBed } from '@angular/core/testing';
import { RegistrarService } from './registrar.service';
import { BackendService } from '../shared/services/backend.service';
import { HttpClientTestingModule } from '@angular/common/http/testing';
describe('RegistrarService', () => {
let service: RegistrarService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [BackendService],
});
service = TestBed.inject(RegistrarService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,29 @@
// Copyright 2023 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';
@Injectable({
providedIn: 'root',
})
export class RegistrarService {
activeRegistrarId: string = '';
registrars: string[] = [];
constructor(private backend: BackendService) {
this.backend.getRegistrars().subscribe((r) => {
this.registrars = r;
});
}
}

View File

@@ -0,0 +1,104 @@
<h3 mat-dialog-title>Contact details</h3>
<div mat-dialog-content>
<form (ngSubmit)="saveAndClose($event)">
<div>
<mat-form-field class="contact-details__input">
<mat-label>Name: </mat-label>
<input
matInput
[required]="true"
[(ngModel)]="contact.name"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</div>
<div>
<mat-form-field class="contact-details__input">
<mat-label>Primary account email: </mat-label>
<input
type="email"
matInput
[email]="true"
[required]="true"
[(ngModel)]="contact.emailAddress"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</div>
<div>
<mat-form-field class="contact-details__input">
<mat-label>Phone: </mat-label>
<input
matInput
[(ngModel)]="contact.phoneNumber"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</div>
<div>
<mat-form-field class="contact-details__input">
<mat-label>Fax: </mat-label>
<input
matInput
[(ngModel)]="contact.faxNumber"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</div>
<div class="contact-details__group">
<label>Contact type:</label>
<div class="contact-details__group-content">
<mat-checkbox
*ngFor="let contactType of contactTypes"
[checked]="contact.types.includes(contactType.value)"
(change)="
$event.checked
? contact.types.push(contactType.value)
: contact.types.splice(
contact.types.indexOf(contactType.value),
1
)
"
[disabled]="
contact.types.length === 1 && contact.types[0] === contactType.value
"
>
{{ contactType.label }}
</mat-checkbox>
</div>
</div>
<section>
<mat-checkbox
[(ngModel)]="contact.visibleInWhoisAsAdmin"
[ngModelOptions]="{ standalone: true }"
>Show in Registrar WHOIS record as admin contact</mat-checkbox
>
</section>
<section>
<mat-checkbox
[(ngModel)]="contact.visibleInWhoisAsTech"
[ngModelOptions]="{ standalone: true }"
>Show in Registrar WHOIS record as technical contact</mat-checkbox
>
</section>
<section>
<mat-checkbox
[(ngModel)]="contact.visibleInDomainWhoisAsAbuse"
[ngModelOptions]="{ standalone: true }"
>Show Phone and Email in Domain WHOIS Record as registrar abuse contact
(per CL&D requirements)</mat-checkbox
>
</section>
<mat-dialog-actions>
<button mat-button (click)="onClose()">Cancel</button>
<button type="submit" mat-button>Save</button>
</mat-dialog-actions>
</form>
</div>

View File

@@ -0,0 +1,35 @@
<div *ngIf="loading" class="contact__loading">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
<div *ngIf="!loading">
<div *ngFor="let group of groupedData">
<div class="contact__cards-wrapper" *ngIf="group.contacts.length">
<h3>{{ group.label }}s</h3>
<mat-divider></mat-divider>
<div class="contact__cards">
<mat-card class="contact__card" *ngFor="let contact of group.contacts">
<mat-card-title>{{ contact.name }}</mat-card-title>
<p *ngIf="contact.phoneNumber">{{ contact.phoneNumber }}</p>
<p *ngIf="contact.emailAddress">{{ contact.emailAddress }}</p>
<mat-card-actions class="contact__card-actions">
<button
mat-button
color="primary"
(click)="openDetails($event, contact)"
>
<mat-icon>edit</mat-icon>Edit
</button>
<button mat-button color="accent" (click)="deleteContact(contact)">
<mat-icon>delete</mat-icon>Delete
</button>
</mat-card-actions>
</mat-card>
</div>
</div>
</div>
<div class="contact__actions">
<button mat-raised-button color="primary" (click)="openCreateNew($event)">
<mat-icon>add</mat-icon>Create a Contact
</button>
</div>
</div>

View File

@@ -0,0 +1,58 @@
// Copyright 2023 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.
.contact {
&__cards-wrapper {
margin-top: 22px;
}
&__card {
width: 400px;
padding: 15px;
}
&__card-actions {
padding: 0;
}
&__loading {
margin: 2rem 0;
}
&__cards {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-top: 20px;
}
&__actions {
display: flex;
justify-content: flex-start;
margin: 2rem 0;
}
}
.contact-details {
&__input {
width: 100%;
}
&__group {
margin: 20px 0;
display: flex;
align-items: baseline;
}
&__group-content {
display: flex;
flex-wrap: wrap;
max-width: 450px;
* {
min-width: 200px;
}
}
}

View File

@@ -0,0 +1,40 @@
// Copyright 2023 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 { ComponentFixture, TestBed } from '@angular/core/testing';
import ContactComponent from './contact.component';
import { MaterialModule } from 'src/app/material.module';
import { BackendService } from 'src/app/shared/services/backend.service';
import { HttpClientTestingModule } from '@angular/common/http/testing';
describe('ContactComponent', () => {
let component: ContactComponent;
let fixture: ComponentFixture<ContactComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ContactComponent],
imports: [HttpClientTestingModule, MaterialModule],
providers: [BackendService],
}).compileComponents();
fixture = TestBed.createComponent(ContactComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,204 @@
// Copyright 2023 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, Inject } from '@angular/core';
import {
MatDialog,
MAT_DIALOG_DATA,
MatDialogRef,
} from '@angular/material/dialog';
import {
MatBottomSheet,
MAT_BOTTOM_SHEET_DATA,
MatBottomSheetRef,
} from '@angular/material/bottom-sheet';
import { Contact, ContactService } from './contact.service';
import { BreakpointObserver } from '@angular/cdk/layout';
import { HttpErrorResponse } from '@angular/common/http';
import { MatSnackBar } from '@angular/material/snack-bar';
enum Operations {
DELETE,
ADD,
UPDATE,
}
interface GroupedContacts {
value: string;
label: string;
contacts: Array<Contact>;
}
let isMobile: boolean;
const contactTypes: Array<GroupedContacts> = [
{ value: 'ADMIN', label: 'Primary contact', contacts: [] },
{ value: 'ABUSE', label: 'Abuse contact', contacts: [] },
{ value: 'BILLING', label: 'Billing contact', contacts: [] },
{ value: 'LEGAL', label: 'Legal contact', contacts: [] },
{ value: 'MARKETING', label: 'Marketing contact', contacts: [] },
{ value: 'TECH', label: 'Technical contact', contacts: [] },
{ value: 'WHOIS', label: 'WHOIS-Inquiry contact', contacts: [] },
];
class ContactDetailsEventsResponder {
private ref?: MatDialogRef<any> | MatBottomSheetRef;
constructor() {
this.onClose = this.onClose.bind(this);
}
setRef(ref: MatDialogRef<any> | MatBottomSheetRef) {
this.ref = ref;
}
onClose() {
if (this.ref == undefined) {
throw "Reference to ContactDetailsDialogComponent hasn't been set. ";
}
if (this.ref instanceof MatBottomSheetRef) {
this.ref.dismiss();
} else if (this.ref instanceof MatDialogRef) {
this.ref.close();
}
}
}
@Component({
selector: 'app-contact-details-dialog',
templateUrl: 'contact-details.component.html',
styleUrls: ['./contact.component.less'],
})
export class ContactDetailsDialogComponent {
onClose!: Function;
contact: Contact;
contactTypes = contactTypes;
operation: Operations;
contactIndex: number;
constructor(
public contactService: ContactService,
private _snackBar: MatSnackBar,
@Inject(isMobile ? MAT_BOTTOM_SHEET_DATA : MAT_DIALOG_DATA)
public data: {
onClose: Function;
contact: Contact;
operation: Operations;
}
) {
this.onClose = data.onClose;
this.contactIndex = contactService.contacts.findIndex(
(c) => c === data.contact
);
this.contact = JSON.parse(JSON.stringify(data.contact));
this.operation = data.operation;
}
saveAndClose(e: any) {
e.preventDefault();
if (!e.target.checkValidity()) {
return;
}
let operationObservable;
if (this.operation === Operations.ADD) {
operationObservable = this.contactService.addContact(this.contact);
} else if (this.operation === Operations.UPDATE) {
operationObservable = this.contactService.updateContact(
this.contactIndex,
this.contact
);
} else {
throw 'Unknown operation type';
}
operationObservable.subscribe({
complete: this.onClose.bind(this),
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.statusText, undefined, {
duration: 1500,
});
},
});
}
}
@Component({
selector: 'app-contact',
templateUrl: './contact.component.html',
styleUrls: ['./contact.component.less'],
})
export default class ContactComponent {
loading: boolean = false;
constructor(
private dialog: MatDialog,
private bottomSheet: MatBottomSheet,
private breakpointObserver: BreakpointObserver,
public contactService: ContactService
) {
// TODO: Refactor to registrarId service
this.loading = true;
this.contactService.fetchContacts().subscribe(() => {
this.loading = false;
});
}
public get groupedData() {
return this.contactService.contacts.reduce((acc, contact) => {
contact.types.forEach((type) => {
acc
.find((group: GroupedContacts) => group.value === type)
?.contacts.push(contact);
});
return acc;
}, JSON.parse(JSON.stringify(contactTypes)));
}
deleteContact(contact: Contact) {
if (confirm(`Please confirm contact ${contact.name} delete`)) {
this.contactService.deleteContact(contact).subscribe();
}
}
openCreateNew(e: Event) {
const newContact: Contact = {
name: '',
phoneNumber: '',
emailAddress: '',
types: [contactTypes[0].value],
};
this.openDetails(e, newContact, Operations.ADD);
}
openDetails(
e: Event,
contact: Contact,
operation: Operations = Operations.UPDATE
) {
e.preventDefault();
// TODO: handle orientation change
isMobile = this.breakpointObserver.isMatched('(max-width: 599px)');
const responder = new ContactDetailsEventsResponder();
const config = { data: { onClose: responder.onClose, contact, operation } };
if (isMobile) {
const bottomSheetRef = this.bottomSheet.open(
ContactDetailsDialogComponent,
config
);
responder.setRef(bottomSheetRef);
} else {
const dialogRef = this.dialog.open(ContactDetailsDialogComponent, config);
responder.setRef(dialogRef);
}
}
}

View File

@@ -0,0 +1,80 @@
// Copyright 2023 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 { Observable, tap } from 'rxjs';
import { RegistrarService } from 'src/app/registrar/registrar.service';
import { BackendService } from 'src/app/shared/services/backend.service';
export interface Contact {
name: string;
phoneNumber: string;
emailAddress: string;
registrarId?: string;
faxNumber?: string;
types: Array<string>;
visibleInWhoisAsAdmin?: boolean;
visibleInWhoisAsTech?: boolean;
visibleInDomainWhoisAsAbuse?: boolean;
}
@Injectable({
providedIn: 'root',
})
export class ContactService {
contacts: Contact[] = [];
constructor(
private backend: BackendService,
private registrarService: RegistrarService
) {}
// TODO: Come up with a better handling for registrarId
fetchContacts(): Observable<Contact[]> {
return this.backend
.getContacts(this.registrarService.activeRegistrarId)
.pipe(
tap((contacts) => {
this.contacts = contacts;
})
);
}
saveContacts(contacts: Contact[]): Observable<Contact[]> {
return this.backend
.postContacts(this.registrarService.activeRegistrarId, contacts)
.pipe(
tap((_) => {
this.contacts = contacts;
})
);
}
updateContact(index: number, contact: Contact) {
const newContacts = this.contacts.map((c, i) =>
i === index ? contact : c
);
return this.saveContacts(newContacts);
}
addContact(contact: Contact) {
const newContacts = this.contacts.concat([contact]);
return this.saveContacts(newContacts);
}
deleteContact(contact: Contact) {
const newContacts = this.contacts.filter((c) => c !== contact);
return this.saveContacts(newContacts);
}
}

View File

@@ -0,0 +1 @@
<p>registrars component works!</p>

View File

@@ -0,0 +1,13 @@
// Copyright 2023 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.

View File

@@ -0,0 +1,36 @@
// Copyright 2023 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 { ComponentFixture, TestBed } from '@angular/core/testing';
import RegistrarsComponent from './registrars.component';
describe('RegistrarsComponent', () => {
let component: RegistrarsComponent;
let fixture: ComponentFixture<RegistrarsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [RegistrarsComponent],
}).compileComponents();
fixture = TestBed.createComponent(RegistrarsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,22 @@
// Copyright 2023 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';
@Component({
selector: 'app-registrars',
templateUrl: './registrars.component.html',
styleUrls: ['./registrars.component.less'],
})
export default class RegistrarsComponent {}

View File

@@ -0,0 +1 @@
<p>security works!</p>

View File

@@ -0,0 +1,13 @@
// Copyright 2023 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.

View File

@@ -0,0 +1,36 @@
// Copyright 2023 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 { ComponentFixture, TestBed } from '@angular/core/testing';
import SecurityComponent from './security.component';
describe('SecurityComponent', () => {
let component: SecurityComponent;
let fixture: ComponentFixture<SecurityComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [SecurityComponent],
}).compileComponents();
fixture = TestBed.createComponent(SecurityComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,22 @@
// Copyright 2023 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';
@Component({
selector: 'app-security',
templateUrl: './security.component.html',
styleUrls: ['./security.component.less'],
})
export default class SecurityComponent {}

View File

@@ -0,0 +1,24 @@
<div class="console-settings">
<h1>Settings</h1>
<nav mat-tab-nav-bar [tabPanel]="tabPanel">
<a mat-tab-link routerLink="contact" routerLinkActive="active-link"
>Contact Info</a
>
<a mat-tab-link routerLink="whois" routerLinkActive="active-link"
>WHOIS Info</a
>
<a mat-tab-link routerLink="security" routerLinkActive="active-link"
>Security</a
>
<a mat-tab-link routerLink="epp-password" routerLinkActive="active-link"
>EPP Password</a
>
<a mat-tab-link routerLink="users" routerLinkActive="active-link">Users</a>
<a mat-tab-link routerLink="registrars" routerLinkActive="active-link"
>Registrars</a
>
</nav>
<mat-tab-nav-panel #tabPanel>
<router-outlet></router-outlet>
</mat-tab-nav-panel>
</div>

View File

@@ -0,0 +1,24 @@
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
.console-settings {
.mdc-tab {
&.active-link {
border-bottom: 2px solid #673ab7;
.mdc-tab__text-label {
color: #673ab7;
}
}
}
}

View File

@@ -0,0 +1,36 @@
// Copyright 2023 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 { ComponentFixture, TestBed } from '@angular/core/testing';
import { SettingsComponent } from './settings.component';
describe('SettingsComponent', () => {
let component: SettingsComponent;
let fixture: ComponentFixture<SettingsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [SettingsComponent],
}).compileComponents();
fixture = TestBed.createComponent(SettingsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,23 @@
// Copyright 2023 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, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-settings',
templateUrl: './settings.component.html',
styleUrls: ['./settings.component.less'],
encapsulation: ViewEncapsulation.None,
})
export class SettingsComponent {}

View File

@@ -0,0 +1 @@
<p>users works!</p>

View File

@@ -0,0 +1,13 @@
// Copyright 2023 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.

View File

@@ -0,0 +1,36 @@
// Copyright 2023 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 { ComponentFixture, TestBed } from '@angular/core/testing';
import UsersComponent from './users.component';
describe('UsersComponent', () => {
let component: UsersComponent;
let fixture: ComponentFixture<UsersComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [UsersComponent],
}).compileComponents();
fixture = TestBed.createComponent(UsersComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,22 @@
// Copyright 2023 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';
@Component({
selector: 'app-users',
templateUrl: './users.component.html',
styleUrls: ['./users.component.less'],
})
export default class UsersComponent {}

View File

@@ -0,0 +1 @@
<p>whois works!</p>

View File

@@ -0,0 +1,13 @@
// Copyright 2023 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.

View File

@@ -0,0 +1,36 @@
// Copyright 2023 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 { ComponentFixture, TestBed } from '@angular/core/testing';
import WhoisComponent from './whois.component';
describe('WhoisComponent', () => {
let component: WhoisComponent;
let fixture: ComponentFixture<WhoisComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [WhoisComponent],
}).compileComponents();
fixture = TestBed.createComponent(WhoisComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,22 @@
// Copyright 2023 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';
@Component({
selector: 'app-whois',
templateUrl: './whois.component.html',
styleUrls: ['./whois.component.less'],
})
export default class WhoisComponent {}

View File

@@ -0,0 +1,66 @@
// Copyright 2023 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 { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, catchError, of } from 'rxjs';
import { Contact } from '../../settings/contact/contact.service';
@Injectable()
export class BackendService {
constructor(private http: HttpClient) {}
errorCatcher<Type>(
error: HttpErrorResponse,
mockData?: Type
): Observable<Type> {
if (error.error instanceof Error) {
// A client-side or network error occurred. Handle it accordingly.
console.error('An error occurred:', error.error.message);
} else {
// The backend returned an unsuccessful response code.
// The response body may contain clues as to what went wrong,
console.error(
`Backend returned code ${error.status}, body was: ${error.error}`
);
}
// return throwError(() => {throw "Failed"});
return of(<Type>mockData);
}
getContacts(registrarId: string): Observable<Contact[]> {
return this.http
.get<Contact[]>(
`/console-api/settings/contacts?registrarId=${registrarId}`
)
.pipe(catchError((err) => this.errorCatcher<Contact[]>(err)));
}
postContacts(
registrarId: string,
contacts: Contact[]
): Observable<Contact[]> {
return this.http.post<Contact[]>(
`/console-api/settings/contacts?registrarId=${registrarId}`,
{ contacts }
);
}
getRegistrars(): Observable<string[]> {
return this.http
.get<string[]>('/console-api/registrars')
.pipe(catchError((err) => this.errorCatcher<string[]>(err)));
}
}

View File

@@ -1,12 +1,23 @@
<div class="console-tlds__cards">
<mat-card class="console-tlds__card">
<mat-card-title>.how</mat-card-title>
<mat-card-subtitle>A place for thinkers, tinkerers, and knowledge seekers</mat-card-subtitle>
<mat-card-subtitle
>A place for thinkers, tinkerers, and knowledge seekers</mat-card-subtitle
>
<mat-card-actions class="console-tlds__card-links">
<a title="Onboarding Now" href="#" target="_blank" rel="noopener">Onboarding Now</a>
<a title="Marketing Materials" href="#" target="_blank" rel="noopener">Marketing Materials</a>
<a title="Visit get.how for more information" href="#" target="_blank" rel="noopener">Visit get.how for more information</a>
<a title="Onboarding Now" href="#" target="_blank" rel="noopener"
>Onboarding Now</a
>
<a title="Marketing Materials" href="#" target="_blank" rel="noopener"
>Marketing Materials</a
>
<a
title="Visit get.how for more information"
href="#"
target="_blank"
rel="noopener"
>Visit get.how for more information</a
>
</mat-card-actions>
</mat-card>
</div>
@@ -14,11 +25,23 @@
<div class="console-tlds__cards">
<mat-card class="console-tlds__card">
<mat-card-title>.soy</mat-card-title>
<mat-card-subtitle>A place for thinkers, tinkerers, and knowledge seekers</mat-card-subtitle>
<mat-card-subtitle
>A place for thinkers, tinkerers, and knowledge seekers</mat-card-subtitle
>
<mat-card-actions class="console-tlds__card-links">
<a title="Onboarding Now" href="#" target="_blank" rel="noopener">Onboarding Now</a>
<a title="Marketing Materials" href="#" target="_blank" rel="noopener">Marketing Materials</a>
<a title="Visit get.how for more information" href="#" target="_blank" rel="noopener">Visit iam.soy for more information</a>
<a title="Onboarding Now" href="#" target="_blank" rel="noopener"
>Onboarding Now</a
>
<a title="Marketing Materials" href="#" target="_blank" rel="noopener"
>Marketing Materials</a
>
<a
title="Visit get.how for more information"
href="#"
target="_blank"
rel="noopener"
>Visit iam.soy for more information</a
>
</mat-card-actions>
</mat-card>
</div>

View File

@@ -1,4 +1,4 @@
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
// Copyright 2023 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.

View File

@@ -1,4 +1,4 @@
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -15,7 +15,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TldsComponent } from './tlds.component';
import {MaterialModule} from '../material.module';
import { MaterialModule } from '../material.module';
describe('TldsComponent', () => {
let component: TldsComponent;
@@ -24,9 +24,8 @@ describe('TldsComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MaterialModule],
declarations: [ TldsComponent ]
})
.compileComponents();
declarations: [TldsComponent],
}).compileComponents();
fixture = TestBed.createComponent(TldsComponent);
component = fixture.componentInstance;

View File

@@ -1,4 +1,4 @@
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
// Copyright 2023 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.
@@ -12,18 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { Component } from '@angular/core';
@Component({
selector: 'app-tlds',
templateUrl: './tlds.component.html',
styleUrls: ['./tlds.component.less']
styleUrls: ['./tlds.component.less'],
})
export class TldsComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}
export class TldsComponent {}

View File

@@ -13,5 +13,5 @@
// limitations under the License.
export const environment = {
production: true
production: true,
};

View File

@@ -17,7 +17,7 @@
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false
production: false,
};
/*

View File

@@ -1,16 +1,22 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Nomulus Console</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body class="mat-typography">
<app-root></app-root>
</body>
<head>
<meta charset="utf-8" />
<title>Nomulus Console</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet"
/>
</head>
<body class="mat-typography">
<app-root></app-root>
</body>
</html>

View File

@@ -22,5 +22,6 @@ if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch((err) => console.error(err));

View File

@@ -45,8 +45,7 @@
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js'; // Included with Angular CLI.
import 'zone.js'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS

View File

@@ -12,7 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
html,
body {
height: 100%;
}
body {
margin: 0;
font-family: Roboto, "Helvetica Neue", sans-serif;
}

View File

@@ -4,11 +4,11 @@ import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
platformBrowserDynamicTesting,
} from '@angular/platform-browser-dynamic/testing';
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting(),
platformBrowserDynamicTesting()
);

View File

@@ -14,12 +14,26 @@
package google.registry.batch;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.request.Action.Method.POST;
import static google.registry.util.RegistrarUtils.normalizeRegistrarId;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig.Config;
import google.registry.groups.GmailClient;
import google.registry.groups.GroupsConnection;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.request.Action;
import google.registry.request.auth.Auth;
import google.registry.util.EmailMessage;
import java.io.IOException;
import java.util.Set;
import javax.inject.Inject;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
/**
* Action that executes a canned script specified by the caller.
@@ -42,19 +56,80 @@ import javax.inject.Inject;
public class CannedScriptExecutionAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final GroupsConnection groupsConnection;
private final GmailClient gmailClient;
private final InternetAddress senderAddress;
private final InternetAddress recipientAddress;
private final String gSuiteDomainName;
@Inject
CannedScriptExecutionAction() {
logger.atInfo().log("Received request to run scripts.");
CannedScriptExecutionAction(
GroupsConnection groupsConnection,
GmailClient gmailClient,
@Config("projectId") String projectId,
@Config("gSuiteDomainName") String gSuiteDomainName,
@Config("alertRecipientEmailAddress") InternetAddress recipientAddress) {
this.groupsConnection = groupsConnection;
this.gmailClient = gmailClient;
this.gSuiteDomainName = gSuiteDomainName;
try {
this.senderAddress = new InternetAddress(String.format("%s@%s", projectId, gSuiteDomainName));
} catch (AddressException e) {
throw new RuntimeException(e);
}
this.recipientAddress = recipientAddress;
logger.atInfo().log("Sender:%s; Recipient: %s.", this.senderAddress, this.recipientAddress);
}
@Override
public void run() {
try {
// Invoke canned scripts here.
checkGroupApi();
EmailMessage message = createEmail();
this.gmailClient.sendEmail(message);
logger.atInfo().log("Finished running scripts.");
} catch (Throwable t) {
logger.atWarning().withCause(t).log("Error executing scripts.");
throw new RuntimeException("Execution failed.");
}
}
// Checks if Directory and GroupSettings still work after GWorkspace changes.
void checkGroupApi() {
ImmutableList<Registrar> registrars =
Streams.stream(Registrar.loadAllCached())
.filter(registrar -> registrar.isLive() && registrar.getType() == Registrar.Type.REAL)
.collect(toImmutableList());
logger.atInfo().log("Found %s registrars.", registrars.size());
for (Registrar registrar : registrars) {
for (final RegistrarPoc.Type type : RegistrarPoc.Type.values()) {
String groupKey =
String.format(
"%s-%s-contacts@%s",
normalizeRegistrarId(registrar.getRegistrarId()),
type.getDisplayName(),
gSuiteDomainName);
try {
Set<String> currentMembers = groupsConnection.getMembersOfGroup(groupKey);
logger.atInfo().log("%s has %s members.", groupKey, currentMembers.size());
} catch (IOException e) {
logger.atWarning().withCause(e).log("Failed to check %s", groupKey);
}
}
}
logger.atInfo().log("Finished checking GroupApis.");
}
EmailMessage createEmail() {
return EmailMessage.newBuilder()
.setFrom(senderAddress)
.setSubject("Test: Please ignore<eom>.")
.setRecipients(ImmutableList.of(recipientAddress))
.setBody("Sent from Nomulus through Google Workspace.")
.build();
}
}

View File

@@ -20,8 +20,6 @@ import static google.registry.tools.ServiceConnection.getServer;
import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.api.gax.rpc.ApiException;
import com.google.cloud.tasks.v2.AppEngineHttpRequest;
import com.google.cloud.tasks.v2.AppEngineRouting;
import com.google.cloud.tasks.v2.CloudTasksClient;
import com.google.cloud.tasks.v2.HttpMethod;
import com.google.cloud.tasks.v2.HttpRequest;
@@ -39,10 +37,12 @@ import com.google.common.net.MediaType;
import com.google.common.net.UrlEscapers;
import com.google.protobuf.ByteString;
import com.google.protobuf.util.Timestamps;
import google.registry.config.CredentialModule.ApplicationDefaultCredential;
import google.registry.config.RegistryConfig.Config;
import google.registry.request.Action.Service;
import google.registry.util.Clock;
import google.registry.util.CollectionUtils;
import google.registry.util.GoogleCredentialsBundle;
import google.registry.util.Retrier;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
@@ -52,7 +52,6 @@ import java.util.Random;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import javax.inject.Inject;
import org.joda.time.Duration;
@@ -67,9 +66,8 @@ public class CloudTasksUtils implements Serializable {
private final Clock clock;
private final String projectId;
private final String locationId;
// defaultServiceAccount and iapClientId are nullable because Optional isn't serializable
@Nullable private final String defaultServiceAccount;
@Nullable private final String iapClientId;
private final String oauthClientId;
private final GoogleCredentialsBundle credential;
private final SerializableCloudTasksClient client;
@Inject
@@ -78,15 +76,15 @@ public class CloudTasksUtils implements Serializable {
Clock clock,
@Config("projectId") String projectId,
@Config("locationId") String locationId,
@Config("defaultServiceAccount") Optional<String> defaultServiceAccount,
@Config("iapClientId") Optional<String> iapClientId,
@Config("oauthClientId") String oauthClientId,
@ApplicationDefaultCredential GoogleCredentialsBundle credential,
SerializableCloudTasksClient client) {
this.retrier = retrier;
this.clock = clock;
this.projectId = projectId;
this.locationId = locationId;
this.defaultServiceAccount = defaultServiceAccount.orElse(null);
this.iapClientId = iapClientId.orElse(null);
this.oauthClientId = oauthClientId;
this.credential = credential;
this.client = client;
}
@@ -94,10 +92,7 @@ public class CloudTasksUtils implements Serializable {
return retrier.callWithRetry(
() -> {
logger.atInfo().log(
"Enqueuing queue='%s' endpoint='%s' service='%s'",
queue,
task.getAppEngineHttpRequest().getRelativeUri(),
task.getAppEngineHttpRequest().getAppEngineRouting().getService());
"Enqueuing queue='%s' endpoint='%s'", queue, task.getHttpRequest().getUrl());
return client.enqueue(projectId, locationId, queue, task);
},
ApiException.class);
@@ -124,7 +119,7 @@ public class CloudTasksUtils implements Serializable {
*
* @return the resulting path (unchanged for POST requests, with params added for GET requests)
*/
private String processRequestParameters(
private static String processRequestParameters(
String path,
HttpMethod method,
Multimap<String, String> params,
@@ -152,43 +147,17 @@ public class CloudTasksUtils implements Serializable {
return path;
}
/**
* Creates a {@link Task} that does not use AppEngine for submission.
*
* <p>This uses the standard Cloud Tasks auth format to create and send an OIDC ID token set to
* the default service account. That account must have permission to submit tasks to Cloud Tasks.
*/
private Task createNonAppEngineTask(
String path, HttpMethod method, Service service, Multimap<String, String> params) {
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder().setHttpMethod(method);
path =
processRequestParameters(
path, method, params, requestBuilder::putHeaders, requestBuilder::setBody);
OidcToken.Builder oidcTokenBuilder =
OidcToken.newBuilder().setServiceAccountEmail(defaultServiceAccount);
// If the service is using IAP, add that as the audience for the token so the request can be
// appropriately authed. Otherwise, use the project name.
if (iapClientId != null) {
oidcTokenBuilder.setAudience(iapClientId);
} else {
oidcTokenBuilder.setAudience(projectId);
}
requestBuilder.setOidcToken(oidcTokenBuilder.build());
String totalPath = String.format("%s%s", getServer(service), path);
requestBuilder.setUrl(totalPath);
return Task.newBuilder().setHttpRequest(requestBuilder.build()).build();
}
/**
* Create a {@link Task} to be enqueued.
*
* <p>This uses the standard Cloud Tasks auth format to create and send an OIDC ID token with the
* default service account as the principal. That account must have permission to submit tasks to
* Cloud Tasks.
*
* @param path the relative URI (staring with a slash and ending without one).
* @param method the HTTP method to be used for the request, only GET and POST are supported.
* @param service the App Engine service to route the request to. Note that with App Engine Task
* Queue API if no service is specified, the service which enqueues the task will be used to
* process the task. Cloud Tasks API does not support this feature so the service will always
* needs to be explicitly specified.
* @param params a multi-map of URL query parameters. Duplicate keys are saved as is, and it is up
* @param service the App Engine service to route the request to.
* @param params a multimap of URL query parameters. Duplicate keys are saved as is, and it is up
* to the server to process the duplicate keys.
* @return the enqueued task.
* @see <a
@@ -204,21 +173,18 @@ public class CloudTasksUtils implements Serializable {
method.equals(HttpMethod.GET) || method.equals(HttpMethod.POST),
"HTTP method %s is used. Only GET and POST are allowed.",
method);
// If the default service account is configured, send a standard non-AppEngine HTTP request
if (defaultServiceAccount != null) {
return createNonAppEngineTask(path, method, service, params);
} else {
AppEngineHttpRequest.Builder requestBuilder =
AppEngineHttpRequest.newBuilder()
.setHttpMethod(method)
.setAppEngineRouting(
AppEngineRouting.newBuilder().setService(service.toString()).build());
path =
processRequestParameters(
path, method, params, requestBuilder::putHeaders, requestBuilder::setBody);
requestBuilder.setRelativeUri(path);
return Task.newBuilder().setAppEngineHttpRequest(requestBuilder.build()).build();
}
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder().setHttpMethod(method);
path =
processRequestParameters(
path, method, params, requestBuilder::putHeaders, requestBuilder::setBody);
OidcToken.Builder oidcTokenBuilder =
OidcToken.newBuilder()
.setServiceAccountEmail(credential.serviceAccount())
.setAudience(oauthClientId);
requestBuilder.setOidcToken(oidcTokenBuilder.build());
String totalPath = String.format("%s%s", getServer(service), path);
requestBuilder.setUrl(totalPath);
return Task.newBuilder().setHttpRequest(requestBuilder.build()).build();
}
/**
@@ -226,11 +192,8 @@ public class CloudTasksUtils implements Serializable {
*
* @param path the relative URI (staring with a slash and ending without one).
* @param method the HTTP method to be used for the request, only GET and POST are supported.
* @param service the App Engine service to route the request to. Note that with App Engine Task
* Queue API if no service is specified, the service which enqueues the task will be used to
* process the task. Cloud Tasks API does not support this feature so the service will always
* needs to be explicitly specified.
* @param params a multi-map of URL query parameters. Duplicate keys are saved as is, and it is up
* @param service the App Engine service to route the request to.
* @param params a multimap of URL query parameters. Duplicate keys are saved as is, and it is up
* to the server to process the duplicate keys.
* @param jitterSeconds the number of seconds that a task is randomly delayed up to.
* @return the enqueued task.
@@ -260,11 +223,8 @@ public class CloudTasksUtils implements Serializable {
*
* @param path the relative URI (staring with a slash and ending without one).
* @param method the HTTP method to be used for the request, only GET and POST are supported.
* @param service the App Engine service to route the request to. Note that with App Engine Task
* Queue API if no service is specified, the service which enqueues the task will be used to
* process the task. Cloud Tasks API does not support this feature so the service will always
* needs to be explicitly specified.
* @param params a multi-map of URL query parameters. Duplicate keys are saved as is, and it is up
* @param service the App Engine service to route the request to.
* @param params a multimap of URL query parameters. Duplicate keys are saved as is, and it is up
* to the server to process the duplicate keys.
* @param delay the amount of time that a task needs to delayed for.
* @return the enqueued task.
@@ -330,6 +290,9 @@ public class CloudTasksUtils implements Serializable {
}
public abstract static class SerializableCloudTasksClient implements Serializable {
private static final long serialVersionUID = 7872861868968535498L;
public abstract Task enqueue(String projectId, String locationId, String queueName, Task task);
}

View File

@@ -102,6 +102,8 @@ public final class RegistryConfig {
@Module
public static final class ConfigModule {
private ConfigModule() {}
@Provides
@Config("projectId")
public static String provideProjectId(RegistryConfigSettings config) {
@@ -120,17 +122,6 @@ public final class RegistryConfig {
return config.gcpProject.locationId;
}
@Provides
@Config("serviceAccountEmails")
public static ImmutableList<String> provideServiceAccountEmails(RegistryConfigSettings config) {
return ImmutableList.copyOf(config.gcpProject.serviceAccountEmails);
}
@Provides
@Config("defaultServiceAccount")
public static Optional<String> provideDefaultServiceAccount(RegistryConfigSettings config) {
return Optional.ofNullable(config.gcpProject.defaultServiceAccount);
}
/**
* The filename of the logo to be displayed in the header of the registrar console.
@@ -257,7 +248,7 @@ public final class RegistryConfig {
@Provides
@Config("databaseRetention")
public static Duration provideDatabaseRetention() {
return RegistryConfig.getDatabaseRetention();
return getDatabaseRetention();
}
/**
@@ -304,7 +295,7 @@ public final class RegistryConfig {
* The maximum number of domain and host updates to batch together to send to
* PublishDnsUpdatesAction, to avoid exceeding HTTP request timeout limits.
*
* @see google.registry.dns.ReadDnsRefreshRequestsAction
* @see ReadDnsRefreshRequestsAction
*/
@Provides
@Config("dnsTldUpdateBatchSize")
@@ -1144,7 +1135,7 @@ public final class RegistryConfig {
@Provides
@Config("availableOauthScopes")
public static ImmutableSet<String> provideAvailableOauthScopes(RegistryConfigSettings config) {
return ImmutableSet.copyOf(config.oAuth.availableOauthScopes);
return ImmutableSet.copyOf(config.auth.availableOauthScopes);
}
/**
@@ -1157,27 +1148,38 @@ public final class RegistryConfig {
* API, which requires at least one of:
*
* <ul>
* <li>https://www.googleapis.com/auth/appengine.apis
* <li>https://www.googleapis.com/auth/cloud-platform
* <li>{@code https://www.googleapis.com/auth/appengine.apis}
* <li>{@code https://www.googleapis.com/auth/cloud-platform}
* </ul>
*/
@Provides
@Config("requiredOauthScopes")
public static ImmutableSet<String> provideRequiredOauthScopes(RegistryConfigSettings config) {
return ImmutableSet.copyOf(config.oAuth.requiredOauthScopes);
return ImmutableSet.copyOf(config.auth.requiredOauthScopes);
}
/**
* Provides service account email addresses allowed to authenticate with the app at {@link
* google.registry.request.auth.AuthSettings.AuthLevel#APP} level.
*/
@Provides
@Config("allowedServiceAccountEmails")
public static ImmutableSet<String> provideAllowedServiceAccountEmails(
RegistryConfigSettings config) {
return ImmutableSet.copyOf(config.auth.allowedServiceAccountEmails);
}
/** Provides the allowed OAuth client IDs (could be multibinding). */
@Provides
@Config("allowedOauthClientIds")
public static ImmutableSet<String> provideAllowedOauthClientIds(RegistryConfigSettings config) {
return ImmutableSet.copyOf(config.oAuth.allowedOauthClientIds);
return ImmutableSet.copyOf(config.auth.allowedOauthClientIds);
}
@Provides
@Config("iapClientId")
public static Optional<String> provideIapClientId(RegistryConfigSettings config) {
return Optional.ofNullable(config.oAuth.iapClientId);
@Config("oauthClientId")
public static String provideOauthClientId(RegistryConfigSettings config) {
return config.auth.oauthClientId;
}
/**
@@ -1253,7 +1255,7 @@ public final class RegistryConfig {
toImmutableSortedMap(
naturalOrder(),
e ->
e.getKey().equals("START_OF_TIME")
"START_OF_TIME".equals(e.getKey())
? START_OF_TIME
: DateTime.parse(e.getKey()),
Entry::getValue));
@@ -1374,6 +1376,12 @@ public final class RegistryConfig {
public static String providePackageDomainLimitUpgradeEmailBody(RegistryConfigSettings config) {
return config.packageMonitoring.packageDomainLimitUpgradeEmailBody;
}
private static String formatComments(String text) {
return Splitter.on('\n').omitEmptyStrings().trimResults().splitToList(text).stream()
.map(s -> "# " + s)
.collect(Collectors.joining("\n"));
}
}
/** Returns the App Engine project ID, which is based off the environment name. */
@@ -1539,9 +1547,9 @@ public final class RegistryConfig {
* one single INSERT statement which can dramatically increase speed in situations with many
* inserts.
*
* <p>Hibernate docs, i.e.
* https://docs.jboss.org/hibernate/orm/5.6/userguide/html_single/Hibernate_User_Guide.html,
* recommend between 10 and 50.
* <p><a
* href="https://docs.jboss.org/hibernate/orm/5.6/userguide/html_single/Hibernate_User_Guide.html">Hibernate
* User Guide</a> recommends between 10 and 50.
*/
public static int getHibernateJdbcBatchSize() {
return CONFIG_SETTINGS.get().hibernate.jdbcBatchSize;
@@ -1568,6 +1576,11 @@ public final class RegistryConfig {
return Duration.standardDays(CONFIG_SETTINGS.get().registryPolicy.contactAutomaticTransferDays);
}
/** A discount for all sunrise domain creates, between 0.0 (no discount) and 1.0 (free). */
public static double getSunriseDomainCreateDiscount() {
return CONFIG_SETTINGS.get().registryPolicy.sunriseDomainCreateDiscount;
}
/**
* Memoizes loading of the {@link RegistryConfigSettings} POJO.
*
@@ -1578,11 +1591,7 @@ public final class RegistryConfig {
public static final Supplier<RegistryConfigSettings> CONFIG_SETTINGS =
memoize(RegistryConfig::getConfigSettings);
private static String formatComments(String text) {
return Splitter.on('\n').omitEmptyStrings().trimResults().splitToList(text).stream()
.map(s -> "# " + s)
.collect(Collectors.joining("\n"));
}
private static InternetAddress parseEmailAddress(String email) {
try {

View File

@@ -23,7 +23,7 @@ public class RegistryConfigSettings {
public GcpProject gcpProject;
public GSuite gSuite;
public OAuth oAuth;
public Auth auth;
public CredentialOAuth credentialOAuth;
public RegistryPolicy registryPolicy;
public Hibernate hibernate;
@@ -54,16 +54,15 @@ public class RegistryConfigSettings {
public String backendServiceUrl;
public String toolsServiceUrl;
public String pubapiServiceUrl;
public List<String> serviceAccountEmails;
public String defaultServiceAccount;
}
/** Configuration options for OAuth settings for authenticating users. */
public static class OAuth {
/** Configuration options for authenticating users. */
public static class Auth {
public List<String> availableOauthScopes;
public List<String> requiredOauthScopes;
public List<String> allowedOauthClientIds;
public String iapClientId;
public List<String> allowedServiceAccountEmails;
public String oauthClientId;
}
/** Configuration options for accessing Google APIs. */
@@ -108,6 +107,7 @@ public class RegistryConfigSettings {
public String registryName;
public List<String> spec11WebResources;
public boolean requireSslCertificates;
public double sunriseDomainCreateDiscount;
}
/** Configuration for Hibernate. */

View File

@@ -18,18 +18,10 @@ gcpProject:
# whether to use local/test credentials when connecting to the servers
isLocal: true
# URLs of the services for the project.
defaultServiceUrl: https://localhost
backendServiceUrl: https://localhost
toolsServiceUrl: https://localhost
pubapiServiceUrl: https://localhost
# Service accounts eligible for authorization (e.g. default service account,
# account used by Cloud Scheduler) to send authenticated requests.
serviceAccountEmails:
- default-service-account-email@email.com
- cloud-scheduler-email@email.com
# The default service account with which the service is running. For example,
# on GAE this would be {project-id}@appspot.gserviceaccount.com
defaultServiceAccount: null
defaultServiceUrl: https://default.example.com
backendServiceUrl: https://backend.example.com
toolsServiceUrl: https://tools.example.com
pubapiServiceUrl: https://pubapi.example.com
gSuite:
# Publicly accessible domain name of the running G Suite instance.
@@ -188,6 +180,11 @@ registryPolicy:
# should generally be true for production environments, for added security.
requireSslCertificates: true
# A fractional discount, if any, to be provided to all sunrise domain creates.
# 0 means no discount will be applied, and 1 means that all sunrise creates
# will be free.
sunriseDomainCreateDiscount: 0.15
hibernate:
# Make 'SERIALIZABLE' the default isolation level to ensure correctness.
#
@@ -295,24 +292,41 @@ caching:
# long duration is acceptable because claims lists don't change frequently.
claimsListCachingSeconds: 21600 # six hours
oAuth:
# Note: Only allowedServiceAccountEmails and oauthClientId should be configured.
# Other fields are related to OAuth-based authentication and will be removed.
auth:
# Deprecated: Use OIDC-based auth instead. This field is for OAuth-based auth.
# OAuth scopes to detect on access tokens. Superset of requiredOauthScopes.
availableOauthScopes:
- https://www.googleapis.com/auth/userinfo.email
# Deprecated: Use OIDC-based auth instead. This field is for OAuth-based auth.
# OAuth scopes required for authenticating. Subset of availableOauthScopes.
requiredOauthScopes:
- https://www.googleapis.com/auth/userinfo.email
# Deprecated: Use OIDC-based auth instead. This field is for OAuth-based auth.
# OAuth client IDs that are allowed to authenticate and communicate with
# backend services, e. g. nomulus tool, EPP proxy, etc. The client_id value
# used in registryTool.clientId field for associated tooling should be included
# in this list. Client IDs are typically of the format
# backend services, e.g. nomulus tool, EPP proxy, etc. The value in
# registryTool.clientId field should be included in this list. Client IDs are
# typically of the format
# numbers-alphanumerics.apps.googleusercontent.com
allowedOauthClientIds: []
# GCP Identity-Aware Proxy client ID, if set up (note: this requires manual setup
# of User objects in the database for Nomulus tool users)
iapClientId: null
# 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
# 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,
# 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
credentialOAuth:
# OAuth scopes required for accessing Google APIs using the default
@@ -334,6 +348,8 @@ credentialOAuth:
- https://www.googleapis.com/auth/admin.directory.group
# View and manage group settings in Group Settings API.
- https://www.googleapis.com/auth/apps.groups.settings
# Send email through Gmail.
- https://www.googleapis.com/auth/gmail.send
# OAuth scopes required to create a credential locally in for the nomulus tool.
localCredentialOauthScopes:
# View and manage data in all Google Cloud APIs.

View File

@@ -140,25 +140,13 @@ public final class TldFanoutAction implements Runnable {
for (String tld : tlds) {
Task task = createTask(tld, flowThruParams);
Task createdTask = cloudTasksUtils.enqueue(queue, task);
if (createdTask.hasAppEngineHttpRequest()) {
outputPayload.append(
String.format(
"- Task: '%s', tld: '%s', endpoint: '%s'\n",
createdTask.getName(),
tld,
createdTask.getAppEngineHttpRequest().getRelativeUri()));
logger.atInfo().log(
"Task: '%s', tld: '%s', endpoint: '%s'.",
createdTask.getName(), tld, createdTask.getAppEngineHttpRequest().getRelativeUri());
} else {
outputPayload.append(
String.format(
"- Task: '%s', tld: '%s', endpoint: '%s'\n",
createdTask.getName(), tld, createdTask.getHttpRequest().getUrl()));
logger.atInfo().log(
"Task: '%s', tld: '%s', endpoint: '%s'.",
createdTask.getName(), tld, createdTask.getHttpRequest().getUrl());
}
outputPayload.append(
String.format(
"- Task: '%s', tld: '%s', endpoint: '%s'\n",
createdTask.getName(), tld, createdTask.getHttpRequest().getUrl()));
logger.atInfo().log(
"Task: '%s', tld: '%s', endpoint: '%s'.",
createdTask.getName(), tld, createdTask.getHttpRequest().getUrl());
}
response.setContentType(PLAIN_TEXT_UTF_8);
response.setPayload(outputPayload.toString());

View File

@@ -60,21 +60,18 @@ public final class RefreshDnsAction implements Runnable {
if (!domainOrHostName.contains(".")) {
throw new BadRequestException("URL parameter 'name' must be fully qualified");
}
tm().transact(
() -> {
switch (type) {
case DOMAIN:
loadAndVerifyExistence(Domain.class, domainOrHostName);
requestDomainDnsRefresh(domainOrHostName);
break;
case HOST:
verifyHostIsSubordinate(loadAndVerifyExistence(Host.class, domainOrHostName));
requestHostDnsRefresh(domainOrHostName);
break;
default:
throw new BadRequestException("Unsupported type: " + type);
}
});
switch (type) {
case DOMAIN:
loadAndVerifyExistence(Domain.class, domainOrHostName);
tm().transact(() -> requestDomainDnsRefresh(domainOrHostName));
break;
case HOST:
verifyHostIsSubordinate(loadAndVerifyExistence(Host.class, domainOrHostName));
tm().transact(() -> requestHostDnsRefresh(domainOrHostName));
break;
default:
throw new BadRequestException("Unsupported type: " + type);
}
}
private <T extends EppResource & ForeignKeyedEppResource>

View File

@@ -144,15 +144,13 @@ public class CloudDnsWriter extends BaseDnsWriter {
dsRrData.add(ds.toRrData());
}
if (!dsRrData.isEmpty()) {
domainRecords.add(
new ResourceRecordSet()
.setName(absoluteDomainName)
.setTtl((int) tld.getDnsDsTtl().orElse(defaultDsTtl).getStandardSeconds())
.setType("DS")
.setKind("dns#resourceRecordSet")
.setRrdatas(ImmutableList.copyOf(dsRrData)));
}
domainRecords.add(
new ResourceRecordSet()
.setName(absoluteDomainName)
.setTtl((int) tld.getDnsDsTtl().orElse(defaultDsTtl).getStandardSeconds())
.setType("DS")
.setKind("dns#resourceRecordSet")
.setRrdatas(ImmutableList.copyOf(dsRrData)));
}
// Construct NS records (if any).
@@ -169,15 +167,13 @@ public class CloudDnsWriter extends BaseDnsWriter {
}
}
if (!nsRrData.isEmpty()) {
domainRecords.add(
new ResourceRecordSet()
.setName(absoluteDomainName)
.setTtl((int) tld.getDnsNsTtl().orElse(defaultNsTtl).getStandardSeconds())
.setType("NS")
.setKind("dns#resourceRecordSet")
.setRrdatas(ImmutableList.copyOf(nsRrData)));
}
domainRecords.add(
new ResourceRecordSet()
.setName(absoluteDomainName)
.setTtl((int) tld.getDnsNsTtl().orElse(defaultNsTtl).getStandardSeconds())
.setType("NS")
.setKind("dns#resourceRecordSet")
.setRrdatas(ImmutableList.copyOf(nsRrData)));
}
desiredRecords.put(absoluteDomainName, domainRecords.build());

View File

@@ -337,7 +337,8 @@ public final class DomainCreateFlow implements TransactionalFlow {
Optional<FeeCreateCommandExtension> feeCreate =
eppInput.getSingleExtension(FeeCreateCommandExtension.class);
FeesAndCredits feesAndCredits =
pricingLogic.getCreatePrice(tld, targetId, now, years, isAnchorTenant, allocationToken);
pricingLogic.getCreatePrice(
tld, targetId, now, years, isAnchorTenant, isSunriseCreate, allocationToken);
validateFeeChallenge(feeCreate, feesAndCredits, defaultTokenUsed);
Optional<SecDnsCreateExtension> secDnsCreate =
validateSecDnsExtension(eppInput.getSingleExtension(SecDnsCreateExtension.class));

View File

@@ -690,6 +690,7 @@ public class DomainFlowUtils {
now,
years,
isAnchorTenant(domainName, allocationToken, Optional.empty()),
isSunrise,
allocationToken)
.getFees();
}

View File

@@ -22,6 +22,7 @@ import static google.registry.util.DomainNameUtils.getTldFromDomainName;
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
import com.google.common.net.InternetDomainName;
import google.registry.config.RegistryConfig;
import google.registry.flows.EppException;
import google.registry.flows.EppException.CommandUseErrorException;
import google.registry.flows.custom.DomainPricingCustomLogic;
@@ -72,26 +73,33 @@ public final class DomainPricingLogic {
DateTime dateTime,
int years,
boolean isAnchorTenant,
boolean isSunriseCreate,
Optional<AllocationToken> allocationToken)
throws EppException {
CurrencyUnit currency = tld.getCurrency();
BaseFee createFeeOrCredit;
BaseFee createFee;
// Domain create cost is always zero for anchor tenants
if (isAnchorTenant) {
createFeeOrCredit = Fee.create(zeroInCurrency(currency), FeeType.CREATE, false);
createFee = Fee.create(zeroInCurrency(currency), FeeType.CREATE, false);
} else {
DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime);
Money domainCreateCost =
getDomainCreateCostWithDiscount(domainPrices, years, allocationToken);
createFeeOrCredit =
// Apply a sunrise discount if configured and applicable
if (isSunriseCreate) {
domainCreateCost =
domainCreateCost.multipliedBy(
1.0d - RegistryConfig.getSunriseDomainCreateDiscount(), RoundingMode.HALF_EVEN);
}
createFee =
Fee.create(domainCreateCost.getAmount(), FeeType.CREATE, domainPrices.isPremium());
}
// Create fees for the cost and the EAP fee, if any.
Fee eapFee = tld.getEapFeeFor(dateTime);
FeesAndCredits.Builder feesBuilder =
new FeesAndCredits.Builder().setCurrency(currency).addFeeOrCredit(createFeeOrCredit);
new FeesAndCredits.Builder().setCurrency(currency).addFeeOrCredit(createFee);
// Don't charge anchor tenants EAP fees.
if (!isAnchorTenant && !eapFee.hasZeroCost()) {
feesBuilder.addFeeOrCredit(eapFee);

View File

@@ -0,0 +1,120 @@
// Copyright 2023 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.groups;
import static com.google.common.collect.Iterables.toArray;
import com.google.api.services.gmail.Gmail;
import com.google.api.services.gmail.model.Message;
import com.google.common.net.MediaType;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import google.registry.util.EmailMessage;
import google.registry.util.EmailMessage.Attachment;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Properties;
import javax.inject.Inject;
import javax.mail.Address;
import javax.mail.BodyPart;
import javax.mail.Message.RecipientType;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Session;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import org.apache.commons.codec.binary.Base64;
/** Sends {@link EmailMessage EmailMessages} through Google Workspace using {@link Gmail}. */
public final class GmailClient {
private final Gmail gmail;
@Inject
GmailClient(Gmail gmail) {
this.gmail = gmail;
}
/**
* Sends {@code emailMessage} using {@link Gmail}.
*
* <p>If the sender as specified by {@link EmailMessage#from} differs from the caller's identity,
* the caller must have delegated `send` authority to the sender.
*/
@CanIgnoreReturnValue
public Message sendEmail(EmailMessage emailMessage) {
Message message = toGmailMessage(toMimeMessage(emailMessage));
try {
return gmail.users().messages().send("me", message).execute();
} catch (IOException e) {
throw new EmailException(e);
}
}
static Message toGmailMessage(MimeMessage message) {
try {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
message.writeTo(buffer);
byte[] rawMessageBytes = buffer.toByteArray();
String encodedEmail = Base64.encodeBase64URLSafeString(rawMessageBytes);
Message gmailMessage = new Message();
gmailMessage.setRaw(encodedEmail);
return gmailMessage;
} catch (MessagingException | IOException e) {
throw new EmailException(e);
}
}
static MimeMessage toMimeMessage(EmailMessage emailMessage) {
try {
MimeMessage msg =
new MimeMessage(Session.getDefaultInstance(new Properties(), /* authenticator= */ null));
msg.setFrom(emailMessage.from());
msg.addRecipients(
RecipientType.TO, toArray(emailMessage.recipients(), InternetAddress.class));
msg.setSubject(emailMessage.subject());
Multipart multipart = new MimeMultipart();
BodyPart bodyPart = new MimeBodyPart();
bodyPart.setContent(
emailMessage.body(),
emailMessage.contentType().orElse(MediaType.PLAIN_TEXT_UTF_8).toString());
multipart.addBodyPart(bodyPart);
if (emailMessage.attachment().isPresent()) {
Attachment attachment = emailMessage.attachment().get();
BodyPart attachmentPart = new MimeBodyPart();
attachmentPart.setContent(attachment.content(), attachment.contentType().toString());
attachmentPart.setFileName(attachment.filename());
multipart.addBodyPart(attachmentPart);
}
msg.addRecipients(RecipientType.BCC, toArray(emailMessage.bccs(), Address.class));
msg.addRecipients(RecipientType.CC, toArray(emailMessage.ccs(), Address.class));
msg.setContent(multipart);
msg.saveChanges();
return msg;
} catch (MessagingException e) {
throw new EmailException(e);
}
}
static class EmailException extends RuntimeException {
public EmailException(Throwable cause) {
super(cause);
}
}
}

View File

@@ -0,0 +1,41 @@
// Copyright 2023 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.groups;
import com.google.api.services.gmail.Gmail;
import dagger.Module;
import dagger.Provides;
import google.registry.config.CredentialModule.AdcDelegatedCredential;
import google.registry.config.RegistryConfig.Config;
import google.registry.util.GoogleCredentialsBundle;
import javax.inject.Singleton;
/** Dagger module providing {@link Gmail} API. */
@Module
public class GmailModule {
@Provides
@Singleton
Gmail provideGmail(
@AdcDelegatedCredential GoogleCredentialsBundle credentialsBundle,
@Config("projectId") String projectId) {
return new Gmail.Builder(
credentialsBundle.getHttpTransport(),
credentialsBundle.getJsonFactory(),
credentialsBundle.getHttpRequestInitializer())
.setApplicationName(projectId)
.build();
}
}

View File

@@ -22,6 +22,8 @@ public enum ConsolePermission {
EDIT_REGISTRAR_DETAILS,
/** Add, update, or remove other console users. */
MANAGE_USERS,
/** View registrars. */
VIEW_REGISTRARS,
/** Add, update, or remove registrars. */
MANAGE_REGISTRARS,
/** Manage related registrars, e.g. when one registrar owns another. */

View File

@@ -27,6 +27,7 @@ public class ConsoleRoleDefinitions {
/** Permissions for a registry support agent. */
static final ImmutableSet<ConsolePermission> SUPPORT_AGENT_PERMISSIONS =
ImmutableSet.of(
ConsolePermission.VIEW_REGISTRARS,
ConsolePermission.VIEW_REGISTRAR_DETAILS,
ConsolePermission.EDIT_REGISTRAR_DETAILS,
ConsolePermission.MANAGE_USERS,

View File

@@ -475,6 +475,9 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
/** An allowlist of hosts allowed to be used on domains on this TLD (ignored if empty). */
@Nullable Set<String> allowedFullyQualifiedHostNames;
@Column(nullable = false)
boolean breakglassMode = false;
/**
* References to allocation tokens that can be used on the TLD if no other token is passed in on a
* domain create.
@@ -701,6 +704,10 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
return nullToEmptyImmutableCopy(idnTables);
}
public boolean getBreakglassMode() {
return breakglassMode;
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
@@ -1004,6 +1011,11 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
return this;
}
public Builder setBreakglassMode(boolean breakglassMode) {
getInstance().breakglassMode = breakglassMode;
return this;
}
@Override
public Tld build() {
final Tld instance = getInstance();

View File

@@ -28,6 +28,7 @@ import google.registry.export.sheet.SheetsServiceModule;
import google.registry.flows.ServerTridProviderModule;
import google.registry.flows.custom.CustomLogicFactoryModule;
import google.registry.groups.DirectoryModule;
import google.registry.groups.GmailModule;
import google.registry.groups.GroupsModule;
import google.registry.groups.GroupssettingsModule;
import google.registry.keyring.KeyringModule;
@@ -64,6 +65,7 @@ import javax.inject.Singleton;
DirectoryModule.class,
DummyKeyringModule.class,
DriveModule.class,
GmailModule.class,
GroupsModule.class,
GroupssettingsModule.class,
JSchModule.class,

View File

@@ -26,6 +26,7 @@ import google.registry.request.RequestComponentBuilder;
import google.registry.request.RequestModule;
import google.registry.request.RequestScope;
import google.registry.ui.server.console.ConsoleDomainGetAction;
import google.registry.ui.server.console.RegistrarsAction;
import google.registry.ui.server.console.settings.ContactAction;
import google.registry.ui.server.registrar.ConsoleOteSetupAction;
import google.registry.ui.server.registrar.ConsoleRegistrarCreatorAction;
@@ -67,6 +68,8 @@ interface FrontendRequestComponent {
ContactAction contactAction();
RegistrarsAction registrarsAction();
@Subcomponent.Builder
abstract class Builder implements RequestComponentBuilder<FrontendRequestComponent> {
@Override public abstract Builder requestModule(RequestModule requestModule);

View File

@@ -367,7 +367,7 @@ final class RdapDataStructures {
PENDING_RESTORE("pending restore"),
REDEMPTION_PERIOD("redemption period"),
RENEW_PERIOD("renew period"),
SERVER_DELETE_PROHIBITED("server deleted prohibited"),
SERVER_DELETE_PROHIBITED("server delete prohibited"),
SERVER_RENEW_PROHIBITED("server renew prohibited"),
SERVER_TRANSFER_PROHIBITED("server transfer prohibited"),
SERVER_UPDATE_PROHIBITED("server update prohibited"),

View File

@@ -54,12 +54,11 @@ public class RdapIcannStandardInformation {
private static final Notice INACCURACY_COMPLAINT_FORM_NOTICE =
Notice.builder()
.setTitle("RDDS Inaccuracy Complaint Form")
.setDescription(
"URL of the ICANN RDDS Inaccuracy Complaint Form: https://www.icann.org/wicf")
.setDescription("URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf")
.addLink(
Link.builder()
.setRel("alternate")
.setHref("https://www.icann.org/wicf")
.setHref("https://icann.org/wicf")
.setType("text/html")
.build())
.build();
@@ -150,14 +149,6 @@ public class RdapIcannStandardInformation {
.build())
.build();
/**
* String that replaces GDPR redacted values.
*
* <p>GTLD Registration Data Temp Spec 17may18, Appendix A, 2.2: Fields required to be "redacted"
* MUST privide in the value section text similar to "REDACTED FOR PRIVACY"
*/
static final String CONTACT_REDACTED_VALUE = "REDACTED FOR PRIVACY";
/**
* Included in ALL contact responses, even if the user is authorized.
*

View File

@@ -21,7 +21,6 @@ import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap;
import static google.registry.model.EppResourceUtils.isLinked;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import static google.registry.rdap.RdapIcannStandardInformation.CONTACT_REDACTED_VALUE;
import static google.registry.util.CollectionUtils.union;
import com.google.common.annotations.VisibleForTesting;
@@ -39,7 +38,6 @@ import com.google.gson.JsonArray;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.EppResource;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactAddress;
import google.registry.model.contact.ContactPhoneNumber;
import google.registry.model.contact.PostalInfo;
import google.registry.model.domain.DesignatedContact;
@@ -519,69 +517,68 @@ public class RdapJsonFormatter {
boolean isAuthorized =
rdapAuthorization.isAuthorizedForRegistrar(contact.getCurrentSponsorRegistrarId());
// ROID needs to be redacted if we aren't authorized, so we can't have a self-link for
// unauthorized users
VcardArray.Builder vcardBuilder = VcardArray.builder();
if (isAuthorized) {
contactBuilder.linksBuilder().add(makeSelfLink("entity", contact.getRepoId()));
}
// Only show the "summary data remark" if the user is authorized to see this data - because
// unauthorized users don't have a self link meaning they can't navigate to the full data.
if (outputDataType != OutputDataType.FULL && isAuthorized) {
contactBuilder.remarksBuilder().add(RdapIcannStandardInformation.SUMMARY_DATA_REMARK);
}
// GTLD Registration Data Temp Spec 17may18, Appendix A, 2.3, 2.4 and RDAP Response Profile
// 2.7.4.1, 2.7.4.2 - the following fields must be redacted:
// for REGISTRANT:
// handle (ROID), FN (name), TEL (telephone/fax and extension), street, city, postal code
// for ADMIN, TECH:
// handle (ROID), FN (name), TEL (telephone/fax and extension), Organization, street, city,
// state/province, postal code, country
//
// Note that in theory we have to show the Organization and state/province and country for the
// REGISTRANT. For now, we won't do that until we make sure it's really OK for GDPR
//
if (!isAuthorized) {
fillRdapContactEntityWhenAuthorized(contactBuilder, vcardBuilder, contact, outputDataType);
} else {
// GTLD Registration Data Temp Spec 17may18, Appendix A, 2.3, 2.4 and RDAP Response Profile
// 2.7.4.1, 2.7.4.2 - the following fields must be redacted:
// for REGISTRANT:
// handle (ROID), FN (name), TEL (telephone/fax and extension), street, city, postal code
// for ADMIN, TECH:
// handle (ROID), FN (name), TEL (telephone/fax and extension), Organization, street, city,
// state/province, postal code, country
//
// Note that in theory we have to show the Organization and state/province and country for the
// REGISTRANT. For now, we won't do that until we make sure it's really OK for GDPR
//
// RDAP Response Profile 2.7.4.3: if we redact values from the contact, we MUST include a
// remark
contactBuilder
.remarksBuilder()
.add(RdapIcannStandardInformation.CONTACT_PERSONAL_DATA_HIDDEN_DATA_REMARK);
// to make sure we don't accidentally display data we shouldn't - we replace the
// contact with a safe resource. Then we can add any information we need (e.g. the
// Organization / state / country of the registrant), although we currently don't do that.
contact =
new Contact.Builder()
.setRepoId(CONTACT_REDACTED_VALUE)
.setVoiceNumber(
new ContactPhoneNumber.Builder().setPhoneNumber(CONTACT_REDACTED_VALUE).build())
.setFaxNumber(
new ContactPhoneNumber.Builder().setPhoneNumber(CONTACT_REDACTED_VALUE).build())
.setInternationalizedPostalInfo(
new PostalInfo.Builder()
.setName(CONTACT_REDACTED_VALUE)
.setOrg(CONTACT_REDACTED_VALUE)
.setType(PostalInfo.Type.INTERNATIONALIZED)
.setAddress(
new ContactAddress.Builder()
.setStreet(ImmutableList.of(CONTACT_REDACTED_VALUE))
.setCity(CONTACT_REDACTED_VALUE)
.setState(CONTACT_REDACTED_VALUE)
.setZip(CONTACT_REDACTED_VALUE)
.setCountryCode("XX")
.build())
.build())
.build();
contactBuilder.setHandle("");
// The VCard format requires a "fn" entry even if it is empty (redacted)
vcardBuilder.add(Vcard.create("fn", "text", ""));
}
// RDAP Response Profile 2.7.3 - we MUST provide a handle set with the ROID, subject to the
// redaction above.
contactBuilder.setHandle(contact.getRepoId());
contactBuilder.setVcardArray(vcardBuilder.build());
contactBuilder.rolesBuilder().addAll(roles);
// RDAP Response Profile doesn't mention status for contacts, so we only show it if we're both
// FULL and Authorized.
if (outputDataType == OutputDataType.FULL && isAuthorized) {
// RDAP Response Profile 2.7.5.1, 2.7.5.3:
// email MUST be omitted, and we MUST have a Remark saying so
contactBuilder
.remarksBuilder()
.add(RdapIcannStandardInformation.CONTACT_EMAIL_REDACTED_FOR_DOMAIN);
if (outputDataType != OutputDataType.INTERNAL) {
// Rdap Response Profile 2.7.6 must have "last update of RDAP database" response. But this is
// only for direct query responses and not for internal objects. I'm not sure why it's in that
// section at all...
contactBuilder.setLastUpdateOfRdapDatabaseEvent(
Event.builder()
.setEventAction(EventAction.LAST_UPDATE_OF_RDAP_DATABASE)
.setEventDate(getRequestTime())
.build());
}
return contactBuilder.build();
}
private void fillRdapContactEntityWhenAuthorized(
RdapContactEntity.Builder contactBuilder,
VcardArray.Builder vcardBuilder,
Contact contact,
OutputDataType outputDataType) {
// ROID needs to be redacted if we aren't authorized, so we can't have a self-link for
// unauthorized users
contactBuilder.linksBuilder().add(makeSelfLink("entity", contact.getRepoId()));
// RDAP Response Profile 2.7.3 - we MUST provide a handle set with the ROID, subject to
// redaction.
contactBuilder.setHandle(contact.getRepoId());
if (outputDataType.equals(OutputDataType.FULL)) {
// RDAP Response Profile doesn't mention status for contacts, so we only show it if we're both
// FULL and Authorized.
contactBuilder
.statusBuilder()
.addAll(
@@ -591,12 +588,18 @@ public class RdapJsonFormatter {
: contact.getStatusValues(),
false,
contact.getDeletionTime().isBefore(getRequestTime())));
// If we are outputting all data (not just summary data), also add events taken from the
// history entries. This isn't strictly required.
//
// We also only add it for authorized users because millisecond times can fingerprint a user
// just as much as the handle can.
contactBuilder.eventsBuilder().addAll(makeOptionalEvents(contact));
} else {
// Only show the "summary data remark" if the user is authorized to see this data - because
// unauthorized users don't have a self link meaning they can't navigate to the full data.
contactBuilder.remarksBuilder().add(RdapIcannStandardInformation.SUMMARY_DATA_REMARK);
}
contactBuilder.rolesBuilder().addAll(roles);
VcardArray.Builder vcardBuilder = VcardArray.builder();
// Adding the VCard members subject to the redaction above.
// Adding the VCard members when not redacted.
//
// RDAP Response Profile 2.7.3 - we MUST have FN, ADR, TEL, EMAIL.
//
@@ -622,33 +625,6 @@ public class RdapJsonFormatter {
if (faxPhoneNumber != null) {
vcardBuilder.add(makePhoneEntry(PHONE_TYPE_FAX, makePhoneString(faxPhoneNumber)));
}
// RDAP Response Profile 2.7.5.1, 2.7.5.3:
// email MUST be omitted, and we MUST have a Remark saying so
contactBuilder
.remarksBuilder()
.add(RdapIcannStandardInformation.CONTACT_EMAIL_REDACTED_FOR_DOMAIN);
contactBuilder.setVcardArray(vcardBuilder.build());
if (outputDataType != OutputDataType.INTERNAL) {
// Rdap Response Profile 2.7.6 must have "last update of RDAP database" response. But this is
// only for direct query responses and not for internal objects. I'm not sure why it's in that
// section at all...
contactBuilder.setLastUpdateOfRdapDatabaseEvent(
Event.builder()
.setEventAction(EventAction.LAST_UPDATE_OF_RDAP_DATABASE)
.setEventDate(getRequestTime())
.build());
}
// If we are outputting all data (not just summary data), also add events taken from the history
// entries. This isn't strictly required.
//
// We also only add it for authorized users because millisecond times can fingerprint a user
// just as much as the handle can.
if (outputDataType == OutputDataType.FULL && isAuthorized) {
contactBuilder.eventsBuilder().addAll(makeOptionalEvents(contact));
}
return contactBuilder.build();
}
/**
@@ -1000,7 +976,7 @@ public class RdapJsonFormatter {
// Gustavo Lozano of ICANN, the one we should use is an embedded array of street address lines
// if there is more than one line:
//
// RFC7095 provides two examples of structured addresses, and one of the examples shows a
// RFC 7095 provides two examples of structured addresses, and one of the examples shows a
// street JSON element that contains several data elements. The example showing (see below)
// several data elements is the expected output when two or more <contact:street> elements
// exists in the contact object.

View File

@@ -44,7 +44,7 @@ public class AuthModule {
// 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_ISSUER_URL = "https://cloud.google.com/iap";
private static final String SA_ISSUER_URL = "https://accounts.google.com";
private static final String REGULAR_ISSUER_URL = "https://accounts.google.com";
/** Provides the custom authentication mechanisms (including OAuth and OIDC). */
@Provides
@@ -82,8 +82,8 @@ public class AuthModule {
@Provides
@RegularOidc
@Singleton
TokenVerifier provideRegularTokenVerifier(@Config("projectId") String projectId) {
return TokenVerifier.newBuilder().setAudience(projectId).setIssuer(SA_ISSUER_URL).build();
TokenVerifier provideRegularTokenVerifier(@Config("oauthClientId") String clientId) {
return TokenVerifier.newBuilder().setAudience(clientId).setIssuer(REGULAR_ISSUER_URL).build();
}
@Provides

View File

@@ -32,7 +32,7 @@ import javax.servlet.http.HttpServletRequest;
/**
* OAuth authentication mechanism, using the OAuthService interface.
*
* Only OAuth version 2 is supported.
* <p>Only OAuth version 2 is supported.
*/
public class OAuthAuthenticationMechanism implements AuthenticationMechanism {

View File

@@ -19,7 +19,7 @@ import static google.registry.request.auth.AuthSettings.AuthLevel.APP;
import com.google.api.client.json.webtoken.JsonWebSignature;
import com.google.auth.oauth2.TokenVerifier;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryEnvironment;
@@ -57,10 +57,10 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
protected final TokenExtractor tokenExtractor;
private final ImmutableList<String> serviceAccountEmails;
private final ImmutableSet<String> serviceAccountEmails;
protected OidcTokenAuthenticationMechanism(
ImmutableList<String> serviceAccountEmails,
ImmutableSet<String> serviceAccountEmails,
TokenVerifier tokenVerifier,
TokenExtractor tokenExtractor) {
this.serviceAccountEmails = serviceAccountEmails;
@@ -83,7 +83,11 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
try {
token = tokenVerifier.verify(rawIdToken);
} catch (Exception e) {
logger.atInfo().withCause(e).log("Error when verifying access token");
logger.atInfo().withCause(e).log(
"Failed OIDC verification attempt:\n%s",
RegistryEnvironment.get().equals(RegistryEnvironment.PRODUCTION)
? "Raw token redacted in prod"
: rawIdToken);
return AuthResult.NOT_AUTHENTICATED;
}
String email = (String) token.getPayload().get("email");
@@ -95,6 +99,7 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
if (maybeUser.isPresent()) {
return AuthResult.create(AuthLevel.USER, UserAuthInfo.create(maybeUser.get()));
}
// TODO: implement caching so we don't have to look up the database for every request.
logger.atInfo().log("No end user found for email address %s", email);
if (serviceAccountEmails.stream().anyMatch(e -> e.equals(email))) {
return AuthResult.create(APP);
@@ -136,7 +141,7 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
@Inject
protected IapOidcAuthenticationMechanism(
@Config("serviceAccountEmails") ImmutableList<String> serviceAccountEmails,
@Config("allowedServiceAccountEmails") ImmutableSet<String> serviceAccountEmails,
@IapOidc TokenVerifier tokenVerifier,
@IapOidc TokenExtractor tokenExtractor) {
super(serviceAccountEmails, tokenVerifier, tokenExtractor);
@@ -164,7 +169,7 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
@Inject
protected RegularOidcAuthenticationMechanism(
@Config("serviceAccountEmails") ImmutableList<String> serviceAccountEmails,
@Config("allowedServiceAccountEmails") ImmutableSet<String> serviceAccountEmails,
@RegularOidc TokenVerifier tokenVerifier,
@RegularOidc TokenExtractor tokenExtractor) {
super(serviceAccountEmails, tokenVerifier, tokenExtractor);

View File

@@ -75,6 +75,11 @@ class CurlCommand implements CommandWithConnection {
required = true)
private Service service;
@Parameter(
names = {"--canary"},
description = "If set, use the canary end-point; otherwise use the regular end-point.")
private Boolean canary = Boolean.FALSE;
@Override
public void setConnection(ServiceConnection connection) {
this.connection = connection;
@@ -90,7 +95,7 @@ class CurlCommand implements CommandWithConnection {
throw new IllegalArgumentException("You may not specify a body for a get method.");
}
ServiceConnection connectionToService = connection.withService(service);
ServiceConnection connectionToService = connection.withService(service, canary);
String response =
(method == Method.GET)
? connectionToService.sendGetRequest(path, ImmutableMap.<String, String>of())

View File

@@ -14,23 +14,17 @@
package google.registry.tools;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import static com.google.common.net.HttpHeaders.PROXY_AUTHORIZATION;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.UrlEncodedContent;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.util.GenericData;
import com.google.auth.oauth2.UserCredentials;
import dagger.Module;
import dagger.Provides;
import google.registry.config.CredentialModule.ApplicationDefaultCredential;
import google.registry.config.RegistryConfig;
import google.registry.config.RegistryConfig.Config;
import google.registry.util.GoogleCredentialsBundle;
import java.io.IOException;
import java.net.URI;
import java.util.Optional;
import google.registry.util.OidcTokenUtils;
/**
* Module for providing the HttpRequestFactory.
@@ -39,25 +33,16 @@ import java.util.Optional;
* connections in that they don't require OAuth2 credentials, but instead require a special cookie.
*/
@Module
class RequestFactoryModule {
final class RequestFactoryModule {
static final int REQUEST_TIMEOUT_MS = 10 * 60 * 1000;
/**
* Server to use if we want to manually request an IAP ID token
*
* <p>If we need to have an IAP-enabled audience, we can use the existing refresh token and the
* IAP client ID audience to request an IAP-enabled ID token. This token is read and used by
* {@link IapHeaderAuthenticationMechanismMechanism}, and it requires that the user have a {@link
* google.registry.model.console.User} object present in the database.
*/
private static final GenericUrl TOKEN_SERVER_URL =
new GenericUrl(URI.create("https://oauth2.googleapis.com/token"));
private RequestFactoryModule() {}
@Provides
static HttpRequestFactory provideHttpRequestFactory(
@ApplicationDefaultCredential GoogleCredentialsBundle credentialsBundle,
@Config("iapClientId") Optional<String> iapClientId) {
@Config("oauthClientId") String oauthClientId) {
if (RegistryConfig.areServersLocal()) {
return new NetHttpTransport()
.createRequestFactory(
@@ -71,14 +56,13 @@ class RequestFactoryModule {
request -> {
// Use the standard credential initializer to set the Authorization header
credentialsBundle.getHttpRequestInitializer().initialize(request);
// If using IAP, use the refresh token to acquire an IAP-enabled ID token and use
// that for authentication.
if (iapClientId.isPresent()) {
String idToken = getIdToken(credentialsBundle, iapClientId.get());
// Set the Proxy-Authentication header so that IAP can read from it, see
// https://cloud.google.com/iap/docs/authentication-howto#authenticating_from_proxy-authorization_header
request.getHeaders().set("Proxy-Authorization", "Bearer " + idToken);
}
// Set OIDC token as the alternative bearer token.
request
.getHeaders()
.set(
PROXY_AUTHORIZATION,
"Bearer "
+ OidcTokenUtils.createOidcToken(credentialsBundle, oauthClientId));
// GAE request times out after 10 min, so here we set the timeout to 10 min. This is
// needed to support some nomulus commands like updating premium lists that take
// a lot of time to complete.
@@ -89,32 +73,4 @@ class RequestFactoryModule {
});
}
}
/**
* Uses the saved desktop-app refresh token to acquire an IAP ID token.
*
* <p>This is lifted mostly from the Google Auth Library's {@link UserCredentials}
* "doRefreshAccessToken" method (which is private and thus inaccessible) while adding in the
* audience of the IAP client ID. That addition of the audience is what allows us to satisfy IAP
* auth. See
* https://cloud.google.com/iap/docs/authentication-howto#authenticating_from_a_desktop_app for
* more details.
*/
private static String getIdToken(GoogleCredentialsBundle credentialsBundle, String iapClientId)
throws IOException {
UserCredentials credentials = (UserCredentials) credentialsBundle.getGoogleCredentials();
GenericData tokenRequest = new GenericData();
tokenRequest.set("client_id", credentials.getClientId());
tokenRequest.set("client_secret", credentials.getClientSecret());
tokenRequest.set("refresh_token", credentials.getRefreshToken());
tokenRequest.set("audience", iapClientId);
tokenRequest.set("grant_type", "refresh_token");
UrlEncodedContent content = new UrlEncodedContent(tokenRequest);
HttpRequestFactory requestFactory = credentialsBundle.getHttpTransport().createRequestFactory();
HttpRequest request = requestFactory.buildPostRequest(TOKEN_SERVER_URL, content);
request.setParser(credentialsBundle.getJsonFactory().createJsonObjectParser());
HttpResponse response = request.execute();
return response.parseAs(GenericData.class).get("id_token").toString();
}
}

View File

@@ -15,6 +15,8 @@
package google.registry.tools;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.base.Verify.verify;
import static com.google.common.net.HttpHeaders.X_REQUESTED_WITH;
import static com.google.common.net.MediaType.JSON_UTF_8;
import static google.registry.security.JsonHttp.JSON_SAFETY_PREFIX;
@@ -26,6 +28,7 @@ import com.google.api.client.http.HttpHeaders;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpResponse;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.CharStreams;
@@ -36,6 +39,7 @@ import google.registry.config.RegistryConfig;
import google.registry.request.Action.Service;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
import javax.annotation.Nullable;
@@ -55,20 +59,23 @@ public class ServiceConnection {
@Inject HttpRequestFactory requestFactory;
private final Service service;
private final boolean useCanary;
@Inject
ServiceConnection() {
service = Service.TOOLS;
useCanary = false;
}
private ServiceConnection(Service service, HttpRequestFactory requestFactory) {
private ServiceConnection(Service service, HttpRequestFactory requestFactory, boolean useCanary) {
this.service = service;
this.requestFactory = requestFactory;
this.useCanary = useCanary;
}
/** Returns a copy of this connection that talks to a different service. */
public ServiceConnection withService(Service service) {
return new ServiceConnection(service, requestFactory);
/** Returns a copy of this connection that talks to a different service endpoint. */
public ServiceConnection withService(Service service, boolean isCanary) {
return new ServiceConnection(service, requestFactory, isCanary);
}
/** Returns the contents of the title tag in the given HTML, or null if not found. */
@@ -85,7 +92,7 @@ public class ServiceConnection {
private String internalSend(
String endpoint, Map<String, ?> params, MediaType contentType, @Nullable byte[] payload)
throws IOException {
GenericUrl url = new GenericUrl(String.format("%s%s", getServer(service), endpoint));
GenericUrl url = new GenericUrl(String.format("%s%s", getServer(), endpoint));
url.putAll(params);
HttpRequest request =
(payload != null)
@@ -120,6 +127,20 @@ public class ServiceConnection {
}
}
@VisibleForTesting
URL getServer() {
URL url = getServer(service);
if (useCanary) {
verify(!isNullOrEmpty(url.getHost()), "Null host in url");
try {
return new URL(url.getProtocol(), "nomulus-dot-" + url.getHost(), url.getFile());
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
return url;
}
public String sendPostRequest(
String endpoint, Map<String, ?> params, MediaType contentType, byte[] payload)
throws IOException {

View File

@@ -24,7 +24,6 @@ import google.registry.model.tld.label.PremiumList;
import google.registry.model.tld.label.PremiumListDao;
import google.registry.model.tld.label.PremiumListUtils;
import java.nio.file.Files;
import java.util.Optional;
/** Command to safely update {@link PremiumList} in Database for a given TLD. */
@Parameters(separators = " =", commandDescription = "Update a PremiumList in Database.")
@@ -33,16 +32,19 @@ class UpdatePremiumListCommand extends CreateOrUpdatePremiumListCommand {
@Override
protected String prompt() throws Exception {
name = Strings.isNullOrEmpty(name) ? convertFilePathToName(inputFile) : name;
Optional<PremiumList> list = PremiumListDao.getLatestRevision(name);
checkArgument(
list.isPresent(),
String.format("Could not update premium list %s because it doesn't exist.", name));
PremiumList existingList =
PremiumListDao.getLatestRevision(name)
.orElseThrow(
() ->
new IllegalArgumentException(
String.format(
"Could not update premium list %s because it doesn't exist", name)));
inputData = Files.readAllLines(inputFile, UTF_8);
checkArgument(!inputData.isEmpty(), "New premium list data cannot be empty");
currency = list.get().getCurrency();
currency = existingList.getCurrency();
PremiumList updatedPremiumList = PremiumListUtils.parseToPremiumList(name, currency, inputData);
return String.format(
"Update premium list for %s?\n Old List: %s\n New List: %s",
name, list, updatedPremiumList);
name, existingList, updatedPremiumList);
}
}

View File

@@ -15,20 +15,27 @@
package google.registry.tools.server;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.Iterables.getLast;
import static google.registry.dns.DnsUtils.requestDomainDnsRefresh;
import static google.registry.model.tld.Tlds.assertTldsExist;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.RequestParameters.PARAM_TLDS;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import google.registry.request.Action;
import google.registry.request.Parameter;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.util.Clock;
import java.util.Optional;
import java.util.Random;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.persistence.TypedQuery;
import org.apache.arrow.util.VisibleForTesting;
import org.apache.http.HttpStatus;
import org.joda.time.Duration;
@@ -43,10 +50,8 @@ import org.joda.time.Duration;
* run internally, or by pretending to be internal by setting the X-AppEngine-QueueName header,
* which only admin users can do.
*
* <p>You must pass in a number of {@code smearMinutes} as a URL parameter so that the DNS queue
* doesn't get overloaded. A rough rule of thumb for Cloud DNS is 1 minute per every 1,000 domains.
* This smears the updates out over the next N minutes. For small TLDs consisting of fewer than
* 1,000 domains, passing in 1 is fine (which will execute all the updates immediately).
* <p>You may pass in a {@code batchSize} for the batched read of domains from the database. This is
* recommended to be somewhere between 200 and 500. The default value is 250.
*/
@Action(
service = Action.Service.TOOLS,
@@ -56,47 +61,81 @@ public class RefreshDnsForAllDomainsAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@Inject Response response;
private static final int DEFAULT_BATCH_SIZE = 250;
private final Response response;
private final ImmutableSet<String> tlds;
// Recommended value for batch size is between 200 and 500
private final int batchSize;
private final Random random;
@Inject
@Parameter(PARAM_TLDS)
ImmutableSet<String> tlds;
@Inject
@Parameter("smearMinutes")
int smearMinutes;
@Inject Clock clock;
@Inject Random random;
@Inject
RefreshDnsForAllDomainsAction() {}
RefreshDnsForAllDomainsAction(
Response response,
@Parameter(PARAM_TLDS) ImmutableSet<String> tlds,
@Parameter("batchSize") Optional<Integer> batchSize,
Random random) {
this.response = response;
this.tlds = tlds;
this.batchSize = batchSize.orElse(DEFAULT_BATCH_SIZE);
this.random = random;
}
@Override
public void run() {
assertTldsExist(tlds);
checkArgument(smearMinutes > 0, "Must specify a positive number of smear minutes");
tm().transact(
() ->
tm().query(
"SELECT domainName FROM Domain "
+ "WHERE tld IN (:tlds) "
+ "AND deletionTime > :now",
String.class)
.setParameter("tlds", tlds)
.setParameter("now", clock.nowUtc())
.getResultStream()
.forEach(
domainName -> {
try {
// Smear the task execution time over the next N minutes.
requestDomainDnsRefresh(
domainName, Duration.standardMinutes(random.nextInt(smearMinutes)));
} catch (Throwable t) {
logger.atSevere().withCause(t).log(
"Error while enqueuing DNS refresh for domain '%s'.", domainName);
response.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR);
}
}));
checkArgument(batchSize > 0, "Must specify a positive number for batch size");
int smearMinutes = tm().transact(this::calculateSmearMinutes);
ImmutableList<String> domainsBatch;
@Nullable String lastInPreviousBatch = null;
do {
Optional<String> lastInPreviousBatchOpt = Optional.ofNullable(lastInPreviousBatch);
domainsBatch = tm().transact(() -> refreshBatch(lastInPreviousBatchOpt, smearMinutes));
lastInPreviousBatch = domainsBatch.isEmpty() ? null : getLast(domainsBatch);
} while (domainsBatch.size() == batchSize);
}
/**
* Calculates the number of smear minutes to enqueue refreshes so that the DNS queue does not get
* overloaded.
*/
private int calculateSmearMinutes() {
Long activeDomains =
tm().query(
"SELECT COUNT(*) FROM Domain WHERE tld IN (:tlds) AND deletionTime = :endOfTime",
Long.class)
.setParameter("tlds", tlds)
.setParameter("endOfTime", END_OF_TIME)
.getSingleResult();
return Math.max(activeDomains.intValue() / 1000, 1);
}
private ImmutableList<String> getBatch(Optional<String> lastInPreviousBatch) {
String sql =
String.format(
"SELECT domainName FROM Domain WHERE tld IN (:tlds) AND"
+ " deletionTime = :endOfTime %s ORDER BY domainName ASC",
lastInPreviousBatch.isPresent() ? "AND domainName > :lastInPreviousBatch" : "");
TypedQuery<String> query =
tm().query(sql, String.class)
.setParameter("tlds", tlds)
.setParameter("endOfTime", END_OF_TIME);
lastInPreviousBatch.ifPresent(l -> query.setParameter("lastInPreviousBatch", l));
return query.setMaxResults(batchSize).getResultStream().collect(toImmutableList());
}
@VisibleForTesting
ImmutableList<String> refreshBatch(Optional<String> lastInPreviousBatch, int smearMinutes) {
ImmutableList<String> domainBatch = getBatch(lastInPreviousBatch);
try {
// Smear the task execution time over the next N minutes.
requestDomainDnsRefresh(domainBatch, Duration.standardMinutes(random.nextInt(smearMinutes)));
} catch (Throwable t) {
logger.atSevere().withCause(t).log("Error while enqueuing DNS refresh batch");
response.setStatus(HttpStatus.SC_OK);
}
return domainBatch;
}
}

View File

@@ -16,6 +16,7 @@ package google.registry.tools.server;
import static com.google.common.base.Strings.emptyToNull;
import static google.registry.request.RequestParameters.extractIntParameter;
import static google.registry.request.RequestParameters.extractOptionalIntParameter;
import static google.registry.request.RequestParameters.extractOptionalParameter;
import static google.registry.request.RequestParameters.extractRequiredParameter;
@@ -76,8 +77,8 @@ public class ToolsServerModule {
}
@Provides
@Parameter("smearMinutes")
static int provideSmearMinutes(HttpServletRequest req) {
return extractIntParameter(req, "smearMinutes");
@Parameter("batchSize")
static Optional<Integer> provideBatchSize(HttpServletRequest req) {
return extractOptionalIntParameter(req, "batchSize");
}
}

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