1
0
mirror of https://github.com/google/nomulus synced 2026-05-18 05:41:51 +00:00

Compare commits

...

15 Commits

Author SHA1 Message Date
Weimin Yu
bd0d8af7b3 Make sure unsafe names can be sent in emails (#2169)
Surround the dot in unsafe domain names with a square bracket. This
is suggested by Gmail abuse-detection and allows outgoing messages
to pass Gmail's check. This should also help with recipients' checks.
2023-10-05 11:19:31 -04:00
Lai Jiang
2da8ea0185 Replace JacksonFactory with GsonFactory (#2173)
JacksonFactory is deprecated and GsonFactory is the recommended
replacement.
2023-10-04 17:02:13 -04:00
Lai Jiang
7a84844000 Remove the GAIA ID field from User (#2170)
It is not used and it is not possible to derive the GAIA ID when
creating a new User from the email address alone.
2023-10-04 15:32:03 -04:00
Weimin Yu
1580555d30 Throttle outgoing emails (#2168)
Adds a delay between emails sent in a tight loop. This helps avoid
triggering Gmail abuse detections.

Also updated the recipient address for billing alerts.
2023-10-04 11:16:56 -04:00
Pavlo Tkach
4fb8a1b50b Add dark theme support to the console (#2167) 2023-10-03 15:54:25 -04:00
Pavlo Tkach
e07f25000d Add console registrars paging, fix empty registrars mobile (#2162) 2023-10-03 15:51:48 -04:00
sarahcaseybot
cc1777af0c Add custom YAML serializer for Duration (#2161)
* Add custom YAML serializer for Duration

This addresses b/301119144. This changes the YAML representation of a TLD to show Duration fields as a String reperesntation using the Java Duration object's toString() format. This eliminates the previous ambiguity over the time unit that is being used for each duration.

* change standardSeconds to standardMinutes in test

* Add custom serializer to the entire mapper
2023-10-03 13:46:19 -04:00
Lai Jiang
87e54c001f Remove unused fields to make the linter happy (#2165) 2023-10-03 13:25:07 -04:00
Pavlo Tkach
2dc87d42b4 Fix console nextUrl stacking routes (#2164) 2023-10-02 17:38:03 -04:00
Lai Jiang
1eed9c82dc Deprecate the OAuth header in Nomulus tool (#2160)
Unless an --oauth flag is used, the nomulus tool will only send the OIDC
header. The server still accepts both headers and the user should use
`create_user` command to create an admin User (with the --oauth flag on), which
will then allow one to use the nomulus tool without the --oauth flag.

The --oauth flag and the server's ability to support OAuth-based
authentication will be removed soon. Users are urged to create the User
object in time to avoid service interruption.

TESTED=verified on alpha.
2023-10-02 15:50:30 -04:00
gbrodman
cf43de7755 Open resources link in new tab (#2163)
We want to do this because it takes the user to an external site, which
could potentially lead to confusion if they tried to use the back button
without a new tab.
2023-10-02 15:06:33 -04:00
Weimin Yu
f54bec7553 Add docs for Cloud Build status notification (#2157)
Add documentation that describes the current Cloud Build status notification
to Google Chat, as well as how to update the configuration and the
notifier service.
2023-09-29 10:49:15 -04:00
gbrodman
cf698c2586 Add page for WHOIS-editable fields in the console (#2155)
This isn't the prettiest thing, but it replicates the type of view /
edit functionality that we had in the original console.

Of note: this doesn't include input field validation, which would
probably be a good idea to add at some point.
2023-09-28 22:46:18 -04:00
Lai Jiang
cb240a8f03 Use equals() method to compare equality (#2158)
It will call equalsImmutableObject(), which seems the right thing to do.
We only care if the two Tld objects have the same fields, not if they
are the same object. ErrorProne complained about comparison by identity.
2023-09-28 13:27:36 -04:00
gbrodman
0801679173 Close sidenav on click (#2156)
It shouldn't stick around after we've clicked on one of the links
2023-09-25 14:43:07 -04:00
71 changed files with 1032 additions and 272 deletions

View File

@@ -41,8 +41,8 @@
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",

View File

@@ -37,7 +37,7 @@
background-color: transparent;
}
.active {
background: #eae1e1;
background-color: var(--secondary);
}
}
&__content-wrapper {

View File

@@ -12,10 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { Component, ViewChild } from '@angular/core';
import { RegistrarService } from './registrar/registrar.service';
import { UserDataService } from './shared/services/userData.service';
import { GlobalLoaderService } from './shared/services/globalLoader.service';
import { NavigationEnd, Router } from '@angular/router';
import { MatSidenav } from '@angular/material/sidenav';
@Component({
selector: 'app-root',
@@ -24,10 +26,15 @@ import { GlobalLoaderService } from './shared/services/globalLoader.service';
})
export class AppComponent {
renderRouter: boolean = true;
@ViewChild('sidenav')
sidenav!: MatSidenav;
constructor(
protected registrarService: RegistrarService,
protected userDataService: UserDataService,
protected globalLoader: GlobalLoaderService
protected globalLoader: GlobalLoaderService,
protected router: Router
) {
registrarService.activeRegistrarIdChange.subscribe(() => {
this.renderRouter = false;
@@ -36,4 +43,12 @@ export class AppComponent {
}, 400);
});
}
ngAfterViewInit() {
this.router.events.subscribe((event) => {
if (event instanceof NavigationEnd) {
this.sidenav.close();
}
});
}
}

View File

@@ -47,6 +47,7 @@ import { BillingWidgetComponent } from './home/widgets/billing-widget.component'
import { DomainsWidgetComponent } from './home/widgets/domains-widget.component';
import { SettingsWidgetComponent } from './home/widgets/settings-widget.component';
import { UserDataService } from './shared/services/userData.service';
import WhoisComponent from './settings/whois/whois.component';
@NgModule({
declarations: [
@@ -69,6 +70,7 @@ import { UserDataService } from './shared/services/userData.service';
SettingsWidgetComponent,
TldsComponent,
TldsWidgetComponent,
WhoisComponent,
],
imports: [
AppRoutingModule,

View File

@@ -1,24 +1,30 @@
<p>
<p class="console-app__header">
<mat-toolbar color="primary">
<button mat-icon-button aria-label="Open menu" (click)="toggleNavPane()">
<mat-icon>menu</mat-icon>
</button>
<span>
<a
[routerLink]="'/home'"
routerLinkActive="active"
class="console-app__logo"
>
Google Registry
</a>
</span>
<a
[routerLink]="'/home'"
routerLinkActive="active"
class="console-app__logo"
>
Google Registry
</a>
<span class="spacer"></span>
<app-registrar-selector />
<button mat-icon-button aria-label="Open FAQ">
<mat-icon>question_mark</mat-icon>
</button>
<button mat-icon-button aria-label="Open user info">
<button
mat-icon-button
[matMenuTriggerFor]="menu"
#menuTrigger
aria-label="Open user info"
>
<mat-icon>person</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="logOut()">Log out</button>
</mat-menu>
</mat-toolbar>
</p>

View File

@@ -17,6 +17,21 @@
color: inherit;
text-decoration: none;
}
&__header {
@media (max-width: 599px) {
.mat-toolbar {
padding: 0;
}
.console-app__logo {
font-size: 16px;
}
button {
padding-left: 0;
padding-right: 0;
width: 30px;
}
}
}
}
.spacer {
flex: 1;

View File

@@ -28,4 +28,8 @@ export class HeaderComponent {
this.isNavOpen = !this.isNavOpen;
this.toggleNavOpen.emit(this.isNavOpen);
}
logOut() {
window.open('/console?gcp-iap-mode=CLEAR_LOGIN_COOKIE', '_self');
}
}

View File

@@ -29,4 +29,9 @@
}
}
}
@media (max-width: 510px) {
.console-app__widget-wrapper__wide {
grid-column: initial;
}
}
}

View File

@@ -4,6 +4,7 @@
<a
class="console-app__widget_left"
href="{{ userDataService.userData?.technicalDocsUrl }}"
target="_blank"
>
<mat-icon class="console-app__widget-icon">menu_book</mat-icon>
<h1 class="console-app__widget-title">Resources</h1>

View File

@@ -8,6 +8,7 @@
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
white-space: nowrap;
&-icon {
transform: scale(3);

View File

@@ -1,5 +1,14 @@
<div class="console-app__registrar">
<div>
<button
mat-button
[routerLink]="'/settings/registrars'"
routerLinkActive="active"
*ngIf="isMobile; else desktop"
>
{{ registrarService.activeRegistrarId || "Select registrar" }}
<mat-icon>open_in_new</mat-icon>
</button>
<ng-template #desktop>
<mat-form-field class="mat-form-field-density-5" appearance="fill">
<mat-label>Registrar</mat-label>
<mat-select
@@ -14,5 +23,5 @@
</mat-option>
</mat-select>
</mat-form-field>
</div>
</ng-template>
</div>

View File

@@ -12,14 +12,35 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { RegistrarService } from './registrar.service';
import { BreakpointObserver } from '@angular/cdk/layout';
import { distinctUntilChanged } from 'rxjs';
const MOBILE_LAYOUT_BREAKPOINT = '(max-width: 599px)';
@Component({
selector: 'app-registrar-selector',
templateUrl: './registrar-selector.component.html',
styleUrls: ['./registrar-selector.component.scss'],
})
export class RegistrarSelectorComponent {
constructor(protected registrarService: RegistrarService) {}
export class RegistrarSelectorComponent implements OnInit {
protected isMobile: boolean = false;
readonly breakpoint$ = this.breakpointObserver
.observe([MOBILE_LAYOUT_BREAKPOINT])
.pipe(distinctUntilChanged());
constructor(
protected registrarService: RegistrarService,
protected breakpointObserver: BreakpointObserver
) {}
ngOnInit(): void {
this.breakpoint$.subscribe(() => this.breakpointChanged());
}
private breakpointChanged() {
this.isMobile = this.breakpointObserver.isMatched(MOBILE_LAYOUT_BREAKPOINT);
}
}

View File

@@ -13,7 +13,11 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import {
ActivatedRouteSnapshot,
Router,
RouterStateSnapshot,
} from '@angular/router';
import { RegistrarService } from './registrar.service';
@@ -26,13 +30,16 @@ export class RegistrarGuard {
private registrarService: RegistrarService
) {}
canActivate(): Promise<boolean> | boolean {
canActivate(
_: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Promise<boolean> | boolean {
if (this.registrarService.activeRegistrarId) {
return true;
}
// Get the full URL including any nested children (skip the initial '#/')
// NB: an empty nextUrl takes the user to the home page
const nextUrl = location.hash.split('#/')[1] || '';
return this.router.navigate([`/empty-registrar`, { nextUrl }]);
return this.router.navigate([
`/empty-registrar`,
{ nextUrl: state.url || '' },
]);
}
}

View File

@@ -13,15 +13,16 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { BackendService } from '../shared/services/backend.service';
import { Observable, Subject, tap } from 'rxjs';
import { BackendService } from '../shared/services/backend.service';
import {
GlobalLoader,
GlobalLoaderService,
} from '../shared/services/globalLoader.service';
import { MatSnackBar } from '@angular/material/snack-bar';
interface Address {
export interface Address {
street?: string[];
city?: string;
countryCode?: string;
@@ -31,16 +32,20 @@ interface Address {
export interface Registrar {
allowedTlds?: string[];
ipAddressAllowList?: string[];
emailAddress?: string;
billingAccountMap?: object;
driveFolderId?: string;
emailAddress?: string;
faxNumber?: string;
ianaIdentifier?: number;
icannReferralEmail?: string;
ipAddressAllowList?: string[];
localizedAddress?: Address;
phoneNumber?: string;
registrarId: string;
registrarName: string;
registryLockAllowed?: boolean;
url?: string;
whoisServer?: string;
}
@Injectable({

View File

@@ -1,22 +1,21 @@
<div class="console-app__registrars">
<table
mat-table
[dataSource]="registrarService.registrars"
<mat-table
[dataSource]="dataSource"
class="mat-elevation-z8"
class="console-app__registrars-table"
matSort
>
<ng-container
*ngFor="let column of columns"
[matColumnDef]="column.columnDef"
>
<th mat-header-cell *matHeaderCellDef>
{{ column.header }}
</th>
<td mat-cell *matCellDef="let row" [innerHTML]="column.cell(row)"></td>
<mat-header-cell *matHeaderCellDef> {{ column.header }} </mat-header-cell>
<mat-cell *matCellDef="let row" [innerHTML]="column.cell(row)"></mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
<mat-paginator
class="mat-elevation-z8"
[pageSizeOptions]="[5, 10, 20]"

View File

@@ -1,5 +1,27 @@
.console-app {
$min-width: 756px;
&__registrars {
margin-top: 1.5rem;
width: 100%;
overflow: auto;
}
&__registrars-table {
min-width: $min-width !important;
}
.mat-mdc-paginator {
min-width: $min-width !important;
}
.mat-column {
&-driveId {
min-width: 200px;
word-break: break-all;
}
&-registryLockAllowed {
max-width: 80px;
}
}
}

View File

@@ -12,16 +12,21 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { Component, ViewChild, ViewEncapsulation } from '@angular/core';
import { Registrar, RegistrarService } from './registrar.service';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
@Component({
selector: 'app-registrar',
templateUrl: './registrarsTable.component.html',
styleUrls: ['./registrarsTable.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export class RegistrarComponent {
public static PATH = 'registrars';
dataSource: MatTableDataSource<Registrar>;
columns = [
{
columnDef: 'registrarId',
@@ -72,5 +77,18 @@ export class RegistrarComponent {
},
];
displayedColumns = this.columns.map((c) => c.columnDef);
constructor(protected registrarService: RegistrarService) {}
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
constructor(protected registrarService: RegistrarService) {
this.dataSource = new MatTableDataSource<Registrar>(
registrarService.registrars
);
}
ngAfterViewInit() {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
}

View File

@@ -1,7 +1,7 @@
<h3 mat-dialog-title>Contact details</h3>
<div mat-dialog-content>
<form (ngSubmit)="saveAndClose($event)">
<div>
<p>
<mat-form-field class="contact-details__input">
<mat-label>Name: </mat-label>
<input
@@ -11,9 +11,9 @@
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</div>
</p>
<div>
<p>
<mat-form-field class="contact-details__input">
<mat-label>Primary account email: </mat-label>
<input
@@ -25,9 +25,9 @@
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</div>
</p>
<div>
<p>
<mat-form-field class="contact-details__input">
<mat-label>Phone: </mat-label>
<input
@@ -36,9 +36,9 @@
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</div>
</p>
<div>
<p>
<mat-form-field class="contact-details__input">
<mat-label>Fax: </mat-label>
<input
@@ -47,7 +47,7 @@
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</div>
</p>
<div class="contact-details__group">
<label>Contact type:</label>

View File

@@ -15,9 +15,9 @@
.console-settings {
.mdc-tab {
&.active-link {
border-bottom: 2px solid #673ab7;
border-bottom: 2px solid var(--primary);
.mdc-tab__text-label {
color: #673ab7;
color: var(--primary);
}
}
}

View File

@@ -1 +1,250 @@
<p>whois works!</p>
<div class="settings-whois">
<h2>WHOIS settings</h2>
<h3>
General registrar information for your WHOIS record. This information is
always visible in WHOIS.
</h3>
<div *ngIf="loading" class="settings-whois__loading">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-description">
<h3>Name:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
matInput
type="text"
[(ngModel)]="registrar.registrarName"
disabled
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-description">
<h3>IANA Identifier:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
matInput
type="text"
[(ngModel)]="registrar.ianaIdentifier"
disabled
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-description">
<h3>ICANN Referral Email:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
matInput
type="email"
[(ngModel)]="registrar.icannReferralEmail"
disabled
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-description">
<h3>WHOIS server:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
matInput
type="text"
[(ngModel)]="registrar.whoisServer"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-description">
<h3>Referral URL:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
matInput
type="text"
[(ngModel)]="registrar.url"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-description">
<h3>Email:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
matInput
type="email"
[(ngModel)]="registrar.emailAddress"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-description">
<h3>Phone::</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
matInput
type="text"
[(ngModel)]="registrar.phoneNumber"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-description">
<h3>Fax:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
matInput
type="text"
[(ngModel)]="registrar.faxNumber"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-address">
<div class="settings-whois__section-description">
<h3>Address Line 1:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
*ngIf="registrar.localizedAddress?.street"
matInput
type="text"
[(ngModel)]="(registrar.localizedAddress?.street)![0]"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section-address">
<div class="settings-whois__section-description">
<h3>City:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
*ngIf="registrar.localizedAddress"
matInput
type="text"
[(ngModel)]="registrar.localizedAddress.city"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-address">
<div class="settings-whois__section-description">
<h3>Address Line 2:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
*ngIf="registrar.localizedAddress?.street"
matInput
type="text"
[(ngModel)]="(registrar.localizedAddress?.street)![1]"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section-address">
<div class="settings-whois__section-description">
<h3>State/Region:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
*ngIf="registrar.localizedAddress"
matInput
type="text"
[(ngModel)]="registrar.localizedAddress.state"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
</div>
<div class="settings-whois__section">
<div class="settings-whois__section-address">
<div class="settings-whois__section-description">
<h3>Address Line 3:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
*ngIf="registrar.localizedAddress?.street"
matInput
type="text"
[(ngModel)]="(registrar.localizedAddress?.street)![2]"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
<div class="settings-whois__section-address">
<div class="settings-whois__section-description">
<h3>Country Code:</h3>
</div>
<div class="settings-whois__section-form">
<mat-form-field>
<input
*ngIf="registrar.localizedAddress"
matInput
type="text"
[(ngModel)]="registrar.localizedAddress.countryCode"
[disabled]="!inEdit"
/>
</mat-form-field>
</div>
</div>
</div>
<div class="settings-whois__actions">
<ng-template [ngIf]="inEdit" [ngIfElse]="inView">
<button
class="actions-save"
mat-raised-button
color="primary"
(click)="save()"
>
Save
</button>
<button class="actions-cancel" mat-stroked-button (click)="cancel()">
Cancel
</button>
</ng-template>
<ng-template #inView>
<button #elseBlock mat-raised-button (click)="enableEdit()">Edit</button>
</ng-template>
</div>
</div>

View File

@@ -11,3 +11,49 @@
// 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.
.settings-whois {
margin-top: 1.5rem;
&__section {
display: flex;
flex-wrap: wrap;
margin-bottom: 10px;
min-width: 400px;
}
&__section-address {
display: flex;
flex-wrap: wrap;
margin-bottom: 5px;
min-width: 450px;
width: 50%;
max-width: 50%;
}
&__section-description {
display: inline-block;
margin-block-start: 1em;
min-width: 150px;
}
&__section-form {
display: inline-block;
width: 70%;
mat-form-field {
width: 90%;
min-width: 300px;
}
input:disabled {
border: 0;
}
}
&__loading {
margin: 2rem 0;
}
&__actions {
margin-top: 50px;
display: flex;
justify-content: flex-end;
margin-right: 50px;
button {
margin-left: 20px;
}
}
}

View File

@@ -12,13 +12,66 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { HttpErrorResponse } from '@angular/common/http';
import { Component } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import {
Registrar,
RegistrarService,
} from 'src/app/registrar/registrar.service';
import { WhoisService } from './whois.service';
@Component({
selector: 'app-whois',
templateUrl: './whois.component.html',
styleUrls: ['./whois.component.scss'],
providers: [WhoisService],
})
export default class WhoisComponent {
public static PATH = 'whois';
loading = false;
inEdit = false;
registrar: Registrar;
constructor(
public whoisService: WhoisService,
public registrarService: RegistrarService,
private _snackBar: MatSnackBar
) {
this.registrar = JSON.parse(
JSON.stringify(this.registrarService.registrar)
);
}
enableEdit() {
this.inEdit = true;
}
cancel() {
this.inEdit = false;
this.resetDataSource();
}
save() {
this.loading = true;
this.whoisService.saveChanges(this.registrar).subscribe({
complete: () => {
this.loading = false;
this.resetDataSource();
},
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error, undefined, {
duration: 1500,
});
},
});
this.cancel();
}
resetDataSource() {
this.registrar = JSON.parse(
JSON.stringify(this.registrarService.registrar)
);
}
}

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 { Injectable } from '@angular/core';
import { switchMap } from 'rxjs';
import { Address, RegistrarService } from 'src/app/registrar/registrar.service';
import { BackendService } from 'src/app/shared/services/backend.service';
export interface WhoisRegistrarFields {
ianaIdentifier?: number;
icannReferralEmail?: string;
localizedAddress?: Address;
registrarId?: string;
url?: string;
whoisServer?: string;
}
@Injectable()
export class WhoisService {
whoisRegistrarFields: WhoisRegistrarFields = {};
constructor(
private backend: BackendService,
private registrarService: RegistrarService
) {}
saveChanges(newWhoisRegistrarFields: WhoisRegistrarFields) {
return this.backend.postWhoisRegistrarFields(newWhoisRegistrarFields).pipe(
switchMap(() => {
return this.registrarService.loadRegistrars();
})
);
}
}

View File

@@ -20,6 +20,7 @@ import { SecuritySettingsBackendModel } from 'src/app/settings/security/security
import { Contact } from '../../settings/contact/contact.service';
import { Registrar } from '../../registrar/registrar.service';
import { UserData } from './userData.service';
import { WhoisRegistrarFields } from 'src/app/settings/whois/whois.service';
@Injectable()
export class BackendService {
@@ -94,7 +95,16 @@ export class BackendService {
getUserData(): Observable<UserData> {
return this.http
.get<UserData>(`/console-api/userdata`)
.get<UserData>('/console-api/userdata')
.pipe(catchError((err) => this.errorCatcher<UserData>(err)));
}
postWhoisRegistrarFields(
whoisRegistrarFields: WhoisRegistrarFields
): Observable<WhoisRegistrarFields> {
return this.http.post<WhoisRegistrarFields>(
'/console-api/settings/whois-fields',
whoisRegistrarFields
);
}
}

View File

@@ -44,13 +44,15 @@ body {
&-link {
padding: 0 !important;
text-align: left;
height: 20px !important;
min-width: auto !important;
height: min-content !important;
}
&-title {
color: var(--primary) !important;
text-align: center;
}
&-icon {
color: var(--text);
font-size: 5rem;
line-height: 5rem;
height: 5rem !important;

View File

@@ -17,20 +17,9 @@ $theme-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400);
// The warn palette is optional (defaults to red).
$theme-warn: mat.define-palette(mat.$red-palette);
// Create the theme object. A theme consists of configurations for individual
// theming systems such as "color" or "typography".
$theme: mat.define-light-theme(
(
color: (
primary: $theme-primary,
accent: $theme-accent,
warn: $theme-warn,
),
density: 0,
)
);
/** Application specific section **/
/**
** Application specific section - Global styles and mixins
**/
@mixin form-field-density($density) {
$field-typography: mat.define-typography-config(
@@ -46,19 +35,6 @@ $theme: mat.define-light-theme(
@include form-field-density(-5);
}
$foreground: map.merge($theme, mat.$light-theme-foreground-palette);
// Access and define a class with secondary color exposed
.secondary-text {
color: map.get($foreground, "secondary-text");
}
:root {
--primary: #{mat.get-color-from-palette($theme-primary, 500)};
--secondary: #{map.get($foreground, "secondary-text")};
}
@include mat.all-component-themes($theme);
@import "@angular/material/theming";
// Define application specific typography settings, font-family, etc
@@ -67,3 +43,61 @@ $typography-configuration: mat-typography-config(
);
@include angular-material-typography($typography-configuration);
/**
** Light theme
**/
$light-theme: mat.define-light-theme(
(
color: (
primary: $theme-primary,
accent: $theme-accent,
warn: $theme-warn,
),
density: 0,
)
);
// Access and define a class with secondary color exposed
.secondary-text {
color: map.get(mat.$light-theme-foreground-palette, "secondary-text");
}
:root {
--text: #{map.get(mat.$light-theme-foreground-palette, "base")};
--primary: #{mat.get-color-from-palette($theme-primary, 500)};
--secondary: #{map.get(mat.$light-theme-foreground-palette, "secondary-text")};
}
@include mat.all-component-themes($light-theme);
/**
** Dark theme
**/
$dark-theme: mat.define-dark-theme(
(
color: (
primary: mat.define-palette(mat.$pink-palette),
accent: mat.define-palette(mat.$blue-grey-palette),
),
density: 0,
)
);
@mixin _apply-dark-mode-colors() {
@include mat.all-component-colors($dark-theme);
.secondary-text {
color: map.get(mat.$dark-theme-foreground-palette, "secondary-text");
}
:root {
--text: #{map.get(mat.$dark-theme-foreground-palette, "base")};
--primary: #{mat.get-color-from-palette(mat.$pink-palette, 500)};
--secondary: #{map.get(mat.$dark-theme-background-palette, "secondary-text")};
}
}
@media (prefers-color-scheme: dark) {
@include _apply-dark-mode-colors();
}

View File

@@ -879,6 +879,17 @@ public final class RegistryConfig {
return Optional.ofNullable(config.misc.sheetExportId);
}
/**
* Returns the desired delay between outgoing emails when sending in bulk.
*
* <p>Gmail apparently has unpublished limits on peak throughput over short period.
*/
@Provides
@Config("emailThrottleDuration")
public static Duration provideEmailThrottleSeconds(RegistryConfigSettings config) {
return Duration.standardSeconds(config.misc.emailThrottleSeconds);
}
/**
* Returns the email address we send various alert e-mails to.
*

View File

@@ -208,6 +208,7 @@ public class RegistryConfigSettings {
public static class Misc {
public String sheetExportId;
public boolean isEmailSendingEnabled;
public int emailThrottleSeconds;
public String alertRecipientEmailAddress;
// TODO(b/279671974): remove below field after migration
public String newAlertRecipientEmailAddress;

View File

@@ -443,6 +443,9 @@ misc:
# Whether emails may be sent. For Prod and Sandbox this should be true.
isEmailSendingEnabled: false
# Delay between bulk messages to avoid triggering Gmail fraud checks
emailThrottleSeconds: 30
# Address we send alert summary emails to.
alertRecipientEmailAddress: email@example.com

View File

@@ -58,13 +58,13 @@ public class EntityYamlUtils {
SimpleModule module = new SimpleModule();
module.addSerializer(Money.class, new MoneySerializer());
module.addDeserializer(Money.class, new MoneyDeserializer());
module.addSerializer(Duration.class, new DurationSerializer());
ObjectMapper mapper =
JsonMapper.builder(new YAMLFactory().disable(Feature.WRITE_DOC_START_MARKER))
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY)
.build()
.registerModule(module);
mapper.findAndRegisterModules();
.build();
mapper.findAndRegisterModules().registerModule(module);
return mapper;
}
@@ -201,6 +201,24 @@ public class EntityYamlUtils {
}
}
/** A custom JSON serializer for a {@link Duration} object. */
public static class DurationSerializer extends StdSerializer<Duration> {
public DurationSerializer() {
this(null);
}
public DurationSerializer(Class<Duration> t) {
super(t);
}
@Override
public void serialize(Duration value, JsonGenerator gen, SerializerProvider provider)
throws IOException {
gen.writeString(value.toString());
}
}
/** A custom JSON serializer for an Optional of a {@link Duration} object. */
public static class OptionalDurationSerializer extends StdSerializer<Optional<Duration>> {
@@ -216,7 +234,7 @@ public class EntityYamlUtils {
public void serialize(Optional<Duration> value, JsonGenerator gen, SerializerProvider provider)
throws IOException {
if (value.isPresent()) {
gen.writeNumber(value.get().getMillis());
gen.writeString(value.get().toString());
} else {
gen.writeNull();
}

View File

@@ -35,21 +35,16 @@ import javax.persistence.Table;
/** A console user, either a registry employee or a registrar partner. */
@Entity
@Table(
indexes = {
@Index(columnList = "gaiaId", name = "user_gaia_id_idx"),
@Index(columnList = "emailAddress", name = "user_email_address_idx")
})
@Table(indexes = {@Index(columnList = "emailAddress", name = "user_email_address_idx")})
public class User extends UpdateAutoTimestampEntity implements Buildable {
private static final long serialVersionUID = 6936728603828566721L;
/** Autogenerated unique ID of this user. */
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** GAIA ID associated with the user in question. */
private String gaiaId;
/** Email address of the user in question. */
@Column(nullable = false)
private String emailAddress;
@@ -71,10 +66,6 @@ public class User extends UpdateAutoTimestampEntity implements Buildable {
return id;
}
public String getGaiaId() {
return gaiaId;
}
public String getEmailAddress() {
return emailAddress;
}
@@ -139,12 +130,6 @@ public class User extends UpdateAutoTimestampEntity implements Buildable {
return super.build();
}
public Builder setGaiaId(String gaiaId) {
checkArgument(!isNullOrEmpty(gaiaId), "Gaia ID cannot be null or empty");
getInstance().gaiaId = gaiaId;
return this;
}
public Builder setEmailAddress(String emailAddress) {
getInstance().emailAddress = checkValidEmail(emailAddress);
return this;

View File

@@ -130,7 +130,7 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
public static final Money DEFAULT_REGISTRY_LOCK_OR_UNLOCK_BILLING_COST = Money.of(USD, 0);
public boolean equalYaml(Tld tldToCompare) {
if (this == tldToCompare) {
if (this.equals(tldToCompare)) {
return true;
}
ObjectMapper mapper = createObjectMapper();

View File

@@ -53,7 +53,7 @@ public class BillingEmailUtils {
GmailClient gmailClient,
YearMonth yearMonth,
@Config("gSuiteOutgoingEmailAddress") InternetAddress outgoingEmailAddress,
@Config("alertRecipientEmailAddress") InternetAddress alertRecipientAddress,
@Config("newAlertRecipientEmailAddress") InternetAddress alertRecipientAddress,
@Config("invoiceEmailRecipients") ImmutableList<InternetAddress> invoiceEmailRecipients,
@Config("invoiceReplyToEmailAddress") Optional<InternetAddress> replyToEmailAddress,
@Config("billingBucket") String billingBucket,

View File

@@ -37,11 +37,13 @@ import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.reporting.spec11.soy.Spec11EmailSoyInfo;
import google.registry.util.EmailMessage;
import google.registry.util.Sleeper;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
import org.joda.time.Duration;
import org.joda.time.LocalDate;
/** Provides e-mail functionality for Spec11 tasks, such as sending Spec11 reports to registrars. */
@@ -57,6 +59,8 @@ public class Spec11EmailUtils {
.build()
.compileToTofu();
private final GmailClient gmailClient;
private final Sleeper sleeper;
private final Duration emailThrottleDuration;
private final InternetAddress outgoingEmailAddress;
private final ImmutableList<InternetAddress> spec11BccEmailAddresses;
private final InternetAddress alertRecipientAddress;
@@ -66,12 +70,16 @@ public class Spec11EmailUtils {
@Inject
Spec11EmailUtils(
GmailClient gmailClient,
Sleeper sleeper,
@Config("emailThrottleDuration") Duration emailThrottleDuration,
@Config("newAlertRecipientEmailAddress") InternetAddress alertRecipientAddress,
@Config("spec11OutgoingEmailAddress") InternetAddress spec11OutgoingEmailAddress,
@Config("spec11BccEmailAddresses") ImmutableList<InternetAddress> spec11BccEmailAddresses,
@Config("spec11WebResources") ImmutableList<String> spec11WebResources,
@Config("registryName") String registryName) {
this.gmailClient = gmailClient;
this.sleeper = sleeper;
this.emailThrottleDuration = emailThrottleDuration;
this.outgoingEmailAddress = spec11OutgoingEmailAddress;
this.spec11BccEmailAddresses = spec11BccEmailAddresses;
this.alertRecipientAddress = alertRecipientAddress;
@@ -94,6 +102,13 @@ public class Spec11EmailUtils {
for (RegistrarThreatMatches registrarThreatMatches : registrarThreatMatchesSet) {
RegistrarThreatMatches filteredMatches = filterOutNonPublishedMatches(registrarThreatMatches);
if (!filteredMatches.threatMatches().isEmpty()) {
if (numRegistrarsEmailed > 0) {
try {
sleeper.sleep(emailThrottleDuration);
} catch (InterruptedException ie) {
throw new RuntimeException(ie);
}
}
try {
// Handle exceptions individually per registrar so that one failed email doesn't prevent
// the rest from being sent.
@@ -156,7 +171,7 @@ public class Spec11EmailUtils {
gmailClient.sendEmail(
EmailMessage.newBuilder()
.setSubject(subject)
.setBody(getContent(date, soyTemplateInfo, registrarThreatMatches))
.setBody(getEmailBody(date, soyTemplateInfo, registrarThreatMatches))
.setContentType(MediaType.HTML_UTF_8)
.setFrom(outgoingEmailAddress)
.addRecipient(getEmailAddressForRegistrar(registrarThreatMatches.clientId()))
@@ -164,7 +179,7 @@ public class Spec11EmailUtils {
.build());
}
private String getContent(
private String getEmailBody(
LocalDate date,
SoyTemplateInfo soyTemplateInfo,
RegistrarThreatMatches registrarThreatMatches) {
@@ -175,7 +190,7 @@ public class Spec11EmailUtils {
.map(
threatMatch ->
ImmutableMap.of(
"domainName", threatMatch.domainName(),
"domainName", toEmailSafeString(threatMatch.domainName()),
"threatType", threatMatch.threatType()))
.collect(toImmutableList());
@@ -190,6 +205,12 @@ public class Spec11EmailUtils {
return renderer.render();
}
// Mutates a known bad domain to pass spam checks by Email sender and clients, as suggested by
// the Gmail abuse-detection team.
private String toEmailSafeString(String knownUnsafeDomain) {
return knownUnsafeDomain.replace(".", "[.]");
}
/** Sends an e-mail indicating the state of the spec11 pipeline, with a given subject and body. */
void sendAlertEmail(String subject, String body) {
try {

View File

@@ -43,34 +43,41 @@ final class RegistryCli implements CommandRunner {
// The environment parameter is parsed twice: once here, and once with {@link
// RegistryToolEnvironment#parseFromArgs} in the {@link RegistryTool#main} function.
//
// The flag names must be in sync between the two, and also - this is ugly and we should feel bad.
// The flag names must be in sync between the two, and also - this is ugly, and we should feel
// bad.
@Parameter(
names = {"-e", "--environment"},
description = "Sets the default environment to run the command.")
private RegistryToolEnvironment environment = RegistryToolEnvironment.PRODUCTION;
@Parameter(
names = "--oauth",
description =
"Turn on OAuth-based authentication, the usage of which is to be deprecated. Use"
+ " `create_user` to create an Admin user that allows for OIDC-based authentication.")
private boolean oAuth = false;
@Parameter(
names = {"-c", "--commands"},
description = "Returns all command names.")
private boolean showAllCommands;
@Parameter(
names = {"--credential"},
names = "--credential",
description =
"Name of a JSON file containing credential information used by the tool. "
+ "If not set, credentials saved by running `nomulus login' will be used.")
private String credentialJson = null;
@Parameter(
names = {"--sql_access_info"},
names = "--sql_access_info",
description =
"Name of a file containing space-separated SQL access info used when deploying "
+ "Beam pipelines")
private String sqlAccessInfoFile = null;
// Do not make this final - compile-time constant inlining may interfere with JCommander.
@ParametersDelegate
private LoggingParameters loggingParams = new LoggingParameters();
@ParametersDelegate private LoggingParameters loggingParams = new LoggingParameters();
RegistryToolComponent component;
@@ -105,8 +112,8 @@ final class RegistryCli implements CommandRunner {
jcommander.setProgramName(programName);
// Create all command instances. It would be preferrable to do this in the constructor, but
// JCommander mutates the command instances and doesn't reset them so we have to do it for every
// run.
// JCommander mutates the command instances and doesn't reset them, so we have to do it for
// every run.
try {
for (Map.Entry<String, ? extends Class<? extends Command>> entry : commands.entrySet()) {
Command command = entry.getValue().getDeclaredConstructor().newInstance();
@@ -161,6 +168,7 @@ final class RegistryCli implements CommandRunner {
DaggerRegistryToolComponent.builder()
.credentialFilePath(credentialJson)
.sqlAccessInfoFile(sqlAccessInfoFile)
.addOAuthHeader(oAuth)
.build();
// JCommander stores sub-commands as nested JCommander objects containing a list of user objects
@@ -169,7 +177,7 @@ final class RegistryCli implements CommandRunner {
Command command =
(Command)
Iterables.getOnlyElement(jcommander.getCommands().get(parsedCommand).getObjects());
loggingParams.configureLogging(); // Must be called after parameters are parsed.
loggingParams.configureLogging(); // Must be called after parameters are parsed.
try {
runCommand(command);

View File

@@ -123,6 +123,7 @@ interface RegistryToolComponent {
void inject(GetDomainCommand command);
void inject(GetHostCommand command);
void inject(GetKeyringSecretCommand command);
void inject(GetSqlCredentialCommand command);
@@ -190,6 +191,9 @@ interface RegistryToolComponent {
@BindsInstance
Builder sqlAccessInfoFile(@Nullable @Config("sqlAccessInfoFile") String sqlAccessInfoFile);
@BindsInstance
Builder addOAuthHeader(@Config("addOauthHeader") boolean addOauthHeader);
RegistryToolComponent build();
}
}

View File

@@ -42,7 +42,8 @@ final class RequestFactoryModule {
@Provides
static HttpRequestFactory provideHttpRequestFactory(
@ApplicationDefaultCredential GoogleCredentialsBundle credentialsBundle,
@Config("oauthClientId") String oauthClientId) {
@Config("oauthClientId") String oauthClientId,
@Config("addOauthHeader") boolean addOauthHeader) {
if (RegistryConfig.areServersLocal()) {
return new NetHttpTransport()
.createRequestFactory(
@@ -54,8 +55,10 @@ final class RequestFactoryModule {
return new NetHttpTransport()
.createRequestFactory(
request -> {
// Use the standard credential initializer to set the Authorization header
credentialsBundle.getHttpRequestInitializer().initialize(request);
if (addOauthHeader) {
// Use the standard credential initializer to set the Authorization header
credentialsBundle.getHttpRequestInitializer().initialize(request);
}
// Set OIDC token as the alternative bearer token.
request
.getHeaders()

View File

@@ -34,7 +34,7 @@ import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.LowLevelHttpRequest;
import com.google.api.client.http.LowLevelHttpResponse;
import com.google.api.client.json.Json;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.client.testing.http.HttpTesting;
import com.google.api.client.testing.http.MockHttpTransport;
import com.google.api.client.testing.http.MockLowLevelHttpRequest;
@@ -300,6 +300,6 @@ class DirectoryGroupsConnectionTest {
HttpRequest request = transport.createRequestFactory()
.buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL)
.setThrowExceptionOnExecuteError(false);
return GoogleJsonResponseException.from(new JacksonFactory(), request.execute());
return GoogleJsonResponseException.from(new GsonFactory(), request.execute());
}
}

View File

@@ -30,13 +30,11 @@ public class UserDaoTest extends EntityTestCase {
User user1 =
new User.Builder()
.setEmailAddress("email@email.com")
.setGaiaId("gaiaId")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.SUPPORT_AGENT).build())
.build();
User user2 =
new User.Builder()
.setEmailAddress("foo@bar.com")
.setGaiaId("otherId")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.SUPPORT_AGENT).build())
.build();
UserDao.saveUser(user1);
@@ -54,7 +52,6 @@ public class UserDaoTest extends EntityTestCase {
User user =
new User.Builder()
.setEmailAddress("email@email.com")
.setGaiaId("gaiaId")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.SUPPORT_AGENT).build())
.build();
UserDao.saveUser(user);
@@ -71,13 +68,11 @@ public class UserDaoTest extends EntityTestCase {
User user1 =
new User.Builder()
.setEmailAddress("email@email.com")
.setGaiaId("gaiaId")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.SUPPORT_AGENT).build())
.build();
User user2 =
new User.Builder()
.setEmailAddress("email@email.com")
.setGaiaId("otherId")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.SUPPORT_AGENT).build())
.build();
UserDao.saveUser(user1);

View File

@@ -30,10 +30,9 @@ public class UserTest extends EntityTestCase {
}
@Test
void testPersistence_lookupByGaiaId() {
void testPersistence_lookupByEmail() {
User user =
new User.Builder()
.setGaiaId("gaiaId")
.setEmailAddress("email@email.com")
.setUserRoles(
new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).setIsAdmin(true).build())
@@ -43,10 +42,11 @@ public class UserTest extends EntityTestCase {
() -> {
assertAboutImmutableObjects()
.that(
tm().query("FROM User WHERE gaiaId = 'gaiaId'", User.class).getSingleResult())
tm().query("FROM User WHERE emailAddress = 'email@email.com'", User.class)
.getSingleResult())
.isEqualExceptFields(user, "id", "updateTimestamp");
assertThat(
tm().query("FROM User WHERE gaiaId = 'badGaiaId'", User.class)
tm().query("FROM User WHERE emailAddress = 'nobody@email.com'", User.class)
.getResultList())
.isEmpty();
});
@@ -55,9 +55,6 @@ public class UserTest extends EntityTestCase {
@Test
void testFailure_badInputs() {
User.Builder builder = new User.Builder();
assertThat(assertThrows(IllegalArgumentException.class, () -> builder.setGaiaId(null)))
.hasMessageThat()
.isEqualTo("Gaia ID cannot be null or empty");
assertThat(assertThrows(IllegalArgumentException.class, () -> builder.setEmailAddress("")))
.hasMessageThat()
.isEqualTo("Provided email is not a valid email address");
@@ -72,7 +69,7 @@ public class UserTest extends EntityTestCase {
assertThat(assertThrows(IllegalArgumentException.class, () -> builder.setUserRoles(null)))
.hasMessageThat()
.isEqualTo("User roles cannot be null");
assertThat(assertThrows(IllegalArgumentException.class, builder::build))
.hasMessageThat()
.isEqualTo("Email address cannot be null");
@@ -99,7 +96,6 @@ public class UserTest extends EntityTestCase {
User user =
new User.Builder()
.setGaiaId("gaiaId")
.setEmailAddress("email@email.com")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build())
.build();

View File

@@ -26,6 +26,8 @@ import static google.registry.testing.DatabaseHelper.loadByEntity;
import static google.registry.testing.DatabaseHelper.persistActiveHost;
import static google.registry.testing.DatabaseHelper.persistResource;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -41,11 +43,13 @@ import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationT
import google.registry.reporting.spec11.soy.Spec11EmailSoyInfo;
import google.registry.testing.DatabaseHelper;
import google.registry.util.EmailMessage;
import google.registry.util.Sleeper;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
import org.joda.time.Duration;
import org.joda.time.LocalDate;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -101,6 +105,8 @@ class Spec11EmailUtilsTest {
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
@Mock private GmailClient gmailClient;
@Mock private Sleeper sleeper;
private Duration emailThrottleDuration = Duration.millis(1);
private Spec11EmailUtils emailUtils;
private ArgumentCaptor<EmailMessage> contentCaptor;
private final LocalDate date = new LocalDate(2018, 7, 15);
@@ -114,6 +120,8 @@ class Spec11EmailUtilsTest {
emailUtils =
new Spec11EmailUtils(
gmailClient,
sleeper,
emailThrottleDuration,
new InternetAddress("my-receiver@test.com"),
new InternetAddress("abuse@test.com"),
ImmutableList.of(
@@ -128,6 +136,19 @@ class Spec11EmailUtilsTest {
persistDomainWithHost("c.com", host);
}
@Test
void testSuccess_sleepsBetweenSending() throws Exception {
emailUtils.emailSpec11Reports(
date,
Spec11EmailSoyInfo.MONTHLY_SPEC_11_EMAIL,
"Super Cool Registry Monthly Threat Detector [2018-07-15]",
sampleThreatMatches());
// We inspect individual parameters because Message doesn't implement equals().
verify(gmailClient, times(3)).sendEmail(any(EmailMessage.class));
// Sleep once between two reports sent in a tight loop. No sleep before the final alert message.
verify(sleeper, times(1)).sleep(same(emailThrottleDuration));
}
@Test
void testSuccess_emailMonthlySpec11Reports() throws Exception {
emailUtils.emailSpec11Reports(
@@ -144,7 +165,7 @@ class Spec11EmailUtilsTest {
"the.registrar@example.com",
ImmutableList.of("abuse@test.com", "bcc@test.com"),
"Super Cool Registry Monthly Threat Detector [2018-07-15]",
String.format(MONTHLY_EMAIL_FORMAT, "<tr><td>a.com</td><td>MALWARE</td></tr>"),
String.format(MONTHLY_EMAIL_FORMAT, "<tr><td>a[.]com</td><td>MALWARE</td></tr>"),
Optional.of(MediaType.HTML_UTF_8));
validateMessage(
capturedContents.get(1),
@@ -154,7 +175,7 @@ class Spec11EmailUtilsTest {
"Super Cool Registry Monthly Threat Detector [2018-07-15]",
String.format(
MONTHLY_EMAIL_FORMAT,
"<tr><td>b.com</td><td>MALWARE</td></tr><tr><td>c.com</td><td>MALWARE</td></tr>"),
"<tr><td>b[.]com</td><td>MALWARE</td></tr><tr><td>c[.]com</td><td>MALWARE</td></tr>"),
Optional.of(MediaType.HTML_UTF_8));
validateMessage(
capturedContents.get(2),
@@ -182,7 +203,7 @@ class Spec11EmailUtilsTest {
"the.registrar@example.com",
ImmutableList.of("abuse@test.com", "bcc@test.com"),
"Super Cool Registry Daily Threat Detector [2018-07-15]",
String.format(DAILY_EMAIL_FORMAT, "<tr><td>a.com</td><td>MALWARE</td></tr>"),
String.format(DAILY_EMAIL_FORMAT, "<tr><td>a[.]com</td><td>MALWARE</td></tr>"),
Optional.of(MediaType.HTML_UTF_8));
validateMessage(
capturedMessages.get(1),
@@ -192,7 +213,7 @@ class Spec11EmailUtilsTest {
"Super Cool Registry Daily Threat Detector [2018-07-15]",
String.format(
DAILY_EMAIL_FORMAT,
"<tr><td>b.com</td><td>MALWARE</td></tr><tr><td>c.com</td><td>MALWARE</td></tr>"),
"<tr><td>b[.]com</td><td>MALWARE</td></tr><tr><td>c[.]com</td><td>MALWARE</td></tr>"),
Optional.of(MediaType.HTML_UTF_8));
validateMessage(
capturedMessages.get(2),
@@ -223,7 +244,7 @@ class Spec11EmailUtilsTest {
"new.registrar@example.com",
ImmutableList.of("abuse@test.com", "bcc@test.com"),
"Super Cool Registry Monthly Threat Detector [2018-07-15]",
String.format(MONTHLY_EMAIL_FORMAT, "<tr><td>c.com</td><td>MALWARE</td></tr>"),
String.format(MONTHLY_EMAIL_FORMAT, "<tr><td>c[.]com</td><td>MALWARE</td></tr>"),
Optional.of(MediaType.HTML_UTF_8));
validateMessage(
capturedContents.get(1),
@@ -256,7 +277,7 @@ class Spec11EmailUtilsTest {
"the.registrar@example.com",
ImmutableList.of("abuse@test.com", "bcc@test.com"),
"Super Cool Registry Monthly Threat Detector [2018-07-15]",
String.format(MONTHLY_EMAIL_FORMAT, "<tr><td>a.com</td><td>MALWARE</td></tr>"),
String.format(MONTHLY_EMAIL_FORMAT, "<tr><td>a[.]com</td><td>MALWARE</td></tr>"),
Optional.of(MediaType.HTML_UTF_8));
validateMessage(
capturedContents.get(1),
@@ -266,7 +287,7 @@ class Spec11EmailUtilsTest {
"Super Cool Registry Monthly Threat Detector [2018-07-15]",
String.format(
MONTHLY_EMAIL_FORMAT,
"<tr><td>b.com</td><td>MALWARE</td></tr><tr><td>c.com</td><td>MALWARE</td></tr>"),
"<tr><td>b[.]com</td><td>MALWARE</td></tr><tr><td>c[.]com</td><td>MALWARE</td></tr>"),
Optional.of(MediaType.HTML_UTF_8));
validateMessage(
capturedContents.get(2),
@@ -311,7 +332,7 @@ class Spec11EmailUtilsTest {
"the.registrar@example.com",
ImmutableList.of("abuse@test.com", "bcc@test.com"),
"Super Cool Registry Monthly Threat Detector [2018-07-15]",
String.format(MONTHLY_EMAIL_FORMAT, "<tr><td>a.com</td><td>MALWARE</td></tr>"),
String.format(MONTHLY_EMAIL_FORMAT, "<tr><td>a[.]com</td><td>MALWARE</td></tr>"),
Optional.of(MediaType.HTML_UTF_8));
validateMessage(
capturedMessages.get(1),
@@ -321,7 +342,7 @@ class Spec11EmailUtilsTest {
"Super Cool Registry Monthly Threat Detector [2018-07-15]",
String.format(
MONTHLY_EMAIL_FORMAT,
"<tr><td>b.com</td><td>MALWARE</td></tr><tr><td>c.com</td><td>MALWARE</td></tr>"),
"<tr><td>b[.]com</td><td>MALWARE</td></tr><tr><td>c[.]com</td><td>MALWARE</td></tr>"),
Optional.of(MediaType.HTML_UTF_8));
validateMessage(
capturedMessages.get(2),

View File

@@ -417,7 +417,6 @@ class AuthenticatedRegistrarAccessorTest {
void testConsoleUser_admin() {
google.registry.model.console.User consoleUser =
new google.registry.model.console.User.Builder()
.setGaiaId("gaiaId")
.setEmailAddress("email@email.com")
.setUserRoles(
new UserRoles.Builder().setIsAdmin(true).setGlobalRole(GlobalRole.FTE).build())
@@ -444,7 +443,6 @@ class AuthenticatedRegistrarAccessorTest {
// not admins
google.registry.model.console.User consoleUser =
new google.registry.model.console.User.Builder()
.setGaiaId("gaiaId")
.setEmailAddress("email@email.com")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.SUPPORT_AGENT).build())
.build();
@@ -462,7 +460,6 @@ class AuthenticatedRegistrarAccessorTest {
// Registrar employees should have OWNER access to their registrars
google.registry.model.console.User consoleUser =
new google.registry.model.console.User.Builder()
.setGaiaId("gaiaId")
.setEmailAddress("email@email.com")
.setUserRoles(
new UserRoles.Builder()

View File

@@ -61,7 +61,6 @@ public class OidcTokenAuthenticationMechanismTest {
private final User user =
new User.Builder()
.setEmailAddress(email)
.setGaiaId(gaiaId)
.setUserRoles(
new UserRoles.Builder().setIsAdmin(true).setGlobalRole(GlobalRole.FTE).build())
.build();
@@ -141,7 +140,6 @@ public class OidcTokenAuthenticationMechanismTest {
User serviceUser =
new User.Builder()
.setEmailAddress("service@email.test")
.setGaiaId("service-gaia-id")
.setUserRoles(
new UserRoles.Builder().setIsAdmin(true).setGlobalRole(GlobalRole.FTE).build())
.build();

View File

@@ -61,7 +61,7 @@ import org.junit.runner.RunWith;
* and have at least one test method that persists a JPA entity declared in persistence.xml.
*
* <p>Note that with {@link JpaIntegrationWithCoverageExtension}, each method starts with an empty
* database. Therefore this is not the right place for verifying backwards data compatibility in
* database. Therefore, this is not the right place for verifying backwards data compatibility in
* end-to-end functional tests.
*
* <p>As of April 2020, none of the before/after annotations ({@code BeforeClass} and {@code
@@ -107,7 +107,9 @@ import org.junit.runner.RunWith;
// AfterSuiteTest must be the last entry. See class javadoc for details.
AfterSuiteTest.class
})
public class SqlIntegrationTestSuite {
public final class SqlIntegrationTestSuite {
private SqlIntegrationTestSuite() {}
@BeforeAll // Not yet supported in JUnit 5. Called through BeforeSuiteTest.
public static void initJpaEntityCoverage() {

View File

@@ -144,7 +144,6 @@ public final class RegistryTestServerMain {
User user =
new User.Builder()
.setEmailAddress(loginEmail)
.setGaiaId("123457890")
.setUserRoles(userRoles)
.setRegistryLockPassword("registryLockPassword")
.build();

View File

@@ -26,7 +26,7 @@ import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.client.util.store.AbstractDataStoreFactory;
import com.google.api.client.util.store.DataStore;
import com.google.common.collect.ImmutableList;
@@ -74,7 +74,7 @@ class AuthModuleTest {
// We need to set the following fields because they are checked when
// Credential#setRefreshToken is called. However they are not actually persisted in the
// DataStore and not actually used in tests.
.setJsonFactory(new JacksonFactory())
.setJsonFactory(new GsonFactory())
.setTransport(new NetHttpTransport())
.setTokenServerUrl(new GenericUrl("https://accounts.google.com/o/oauth2/token"))
.setClientAuthentication(new ClientParametersAuthentication(CLIENT_ID, CLIENT_SECRET))
@@ -146,7 +146,7 @@ class AuthModuleTest {
private Credential getCredential() {
// Reconstruct the entire dependency graph, injecting FakeDataStoreFactory and credential
// parameters.
JacksonFactory jsonFactory = new JacksonFactory();
GsonFactory jsonFactory = new GsonFactory();
GoogleClientSecrets clientSecrets = getSecrets();
ImmutableList<String> scopes = ImmutableList.of("scope1");
return AuthModule.provideCredential(

View File

@@ -45,7 +45,7 @@ class GetTldCommandTest extends CommandTestCase<GetTldCommand> {
PremiumList premiumList = persistPremiumList("test", USD, "silver,USD 50", "gold,USD 80");
persistResource(
tld.asBuilder()
.setDnsAPlusAaaaTtl(Duration.millis(900))
.setDnsAPlusAaaaTtl(Duration.standardMinutes(15))
.setDriveFolderId("driveFolder")
.setCreateBillingCost(Money.of(USD, 25))
.setPremiumList(premiumList)

View File

@@ -64,7 +64,7 @@ public class RequestFactoryModuleTest {
RegistryConfig.CONFIG_SETTINGS.get().gcpProject.isLocal = true;
try {
HttpRequestFactory factory =
RequestFactoryModule.provideHttpRequestFactory(credentialsBundle, "client-id");
RequestFactoryModule.provideHttpRequestFactory(credentialsBundle, "client-id", false);
HttpRequestInitializer initializer = factory.getInitializer();
assertThat(initializer).isNotNull();
HttpRequest request = factory.buildGetRequest(new GenericUrl("http://localhost"));
@@ -97,14 +97,23 @@ public class RequestFactoryModuleTest {
boolean origIsLocal = RegistryConfig.CONFIG_SETTINGS.get().gcpProject.isLocal;
RegistryConfig.CONFIG_SETTINGS.get().gcpProject.isLocal = false;
try {
// With OAuth header.
HttpRequestFactory factory =
RequestFactoryModule.provideHttpRequestFactory(credentialsBundle, "clientId");
RequestFactoryModule.provideHttpRequestFactory(credentialsBundle, "clientId", true);
HttpRequest request = factory.buildGetRequest(new GenericUrl("http://localhost"));
assertThat(request.getHeaders().get("Proxy-Authorization")).isEqualTo("Bearer oidc.token");
assertThat(request.getConnectTimeout()).isEqualTo(REQUEST_TIMEOUT_MS);
assertThat(request.getReadTimeout()).isEqualTo(REQUEST_TIMEOUT_MS);
verify(httpRequestInitializer).initialize(request);
verifyNoMoreInteractions(httpRequestInitializer);
// No OAuth header.
factory =
RequestFactoryModule.provideHttpRequestFactory(credentialsBundle, "clientId", false);
request = factory.buildGetRequest(new GenericUrl("http://localhost"));
assertThat(request.getHeaders().get("Proxy-Authorization")).isEqualTo("Bearer oidc.token");
assertThat(request.getConnectTimeout()).isEqualTo(REQUEST_TIMEOUT_MS);
assertThat(request.getReadTimeout()).isEqualTo(REQUEST_TIMEOUT_MS);
verifyNoMoreInteractions(httpRequestInitializer);
} finally {
RegistryConfig.CONFIG_SETTINGS.get().gcpProject.isLocal = origIsLocal;
}

View File

@@ -128,7 +128,6 @@ public class ConsoleDomainGetActionTest {
private User createUser(UserRoles userRoles) {
return new User.Builder()
.setEmailAddress("email@email.com")
.setGaiaId("gaiaId")
.setUserRoles(userRoles)
.build();
}

View File

@@ -15,14 +15,12 @@
package google.registry.ui.server.console;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import com.google.api.client.http.HttpStatusCodes;
import com.google.gson.Gson;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.request.RequestModule;
import google.registry.request.auth.AuthResult;
@@ -31,15 +29,12 @@ import google.registry.request.auth.UserAuthInfo;
import google.registry.testing.FakeResponse;
import java.io.IOException;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Tests for {@link google.registry.ui.server.console.ConsoleUserDataAction}. */
class ConsoleUserDataActionTest {
private final HttpServletRequest request = mock(HttpServletRequest.class);
private RegistrarPoc testRegistrarPoc;
private static final Gson GSON = RequestModule.provideGson();
private FakeResponse response = new FakeResponse();
@@ -52,7 +47,6 @@ class ConsoleUserDataActionTest {
User user =
new User.Builder()
.setEmailAddress("email@email.com")
.setGaiaId("gaiaId")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build())
.build();

View File

@@ -232,7 +232,6 @@ class RegistrarsActionTest {
private User createUser(UserRoles userRoles) {
return new User.Builder()
.setEmailAddress("email@email.com")
.setGaiaId("gaiaId")
.setUserRoles(userRoles)
.build();
}

View File

@@ -226,7 +226,6 @@ class ContactActionTest {
private User createUser(UserRoles userRoles) {
return new User.Builder()
.setEmailAddress("email@email.com")
.setGaiaId("gaiaId")
.setUserRoles(userRoles)
.build();
}

View File

@@ -109,7 +109,6 @@ class SecurityActionTest {
private User createUser(UserRoles userRoles) {
return new User.Builder()
.setEmailAddress("email@email.com")
.setGaiaId("TestUserId")
.setUserRoles(userRoles)
.build();
}

View File

@@ -101,7 +101,6 @@ final class RegistryLockGetActionTest {
google.registry.model.console.User consoleUser =
new google.registry.model.console.User.Builder()
.setEmailAddress("johndoe@theregistrar.com")
.setGaiaId("gaiaId")
.setUserRoles(
new UserRoles.Builder()
.setRegistrarRoles(

View File

@@ -231,7 +231,6 @@ final class RegistryLockPostActionTest {
google.registry.model.console.User consoleUser =
new google.registry.model.console.User.Builder()
.setEmailAddress("johndoe@theregistrar.com")
.setGaiaId("gaiaId")
.setUserRoles(
new UserRoles.Builder()
.setRegistrarRoles(
@@ -252,7 +251,6 @@ final class RegistryLockPostActionTest {
google.registry.model.console.User consoleUser =
new google.registry.model.console.User.Builder()
.setEmailAddress("johndoe@theregistrar.com")
.setGaiaId("gaiaId")
.setUserRoles(new UserRoles.Builder().setIsAdmin(true).build())
.build();
AuthResult consoleAuthResult =
@@ -447,7 +445,6 @@ final class RegistryLockPostActionTest {
google.registry.model.console.User consoleUser =
new google.registry.model.console.User.Builder()
.setEmailAddress("johndoe@theregistrar.com")
.setGaiaId("gaiaId")
.setUserRoles(
new UserRoles.Builder()
.setRegistrarRoles(

View File

@@ -1,10 +1,10 @@
addGracePeriodLength: 432000000
addGracePeriodLength: "PT432000S"
allowedFullyQualifiedHostNames:
- "foo"
allowedRegistrantContactIds: []
anchorTenantAddGracePeriodLength: 2592000000
autoRenewGracePeriodLength: 3888000000
automaticTransferLength: 432000000
anchorTenantAddGracePeriodLength: "PT2592000S"
autoRenewGracePeriodLength: "PT3888000S"
automaticTransferLength: "PT432000S"
claimsPeriodEnd: "294247-01-10T04:00:54.775Z"
createBillingCost:
currency: "USD"
@@ -13,7 +13,7 @@ creationTime: "1970-01-01T00:00:00.000Z"
currency: "USD"
defaultPromoTokens:
- "bbbbb"
dnsAPlusAaaaTtl: 3600000
dnsAPlusAaaaTtl: "PT3600S"
dnsDsTtl: null
dnsNsTtl: null
dnsPaused: false
@@ -38,10 +38,10 @@ idnTables:
invoicingEnabled: false
lordnUsername: null
numDnsPublishLocks: 1
pendingDeleteLength: 432000000
pendingDeleteLength: "PT432000S"
premiumListName: "tld"
pricingEngineClassName: "google.registry.model.pricing.StaticPremiumListPricingEngine"
redemptionGracePeriodLength: 2592000000
redemptionGracePeriodLength: "PT2592000S"
registryLockOrUnlockBillingCost:
currency: "USD"
amount: 0.00
@@ -49,7 +49,7 @@ renewBillingCostTransitions:
"1970-01-01T00:00:00.000Z":
currency: "USD"
amount: 11.00
renewGracePeriodLength: 432000000
renewGracePeriodLength: "PT432000S"
reservedListNames: []
restoreBillingCost:
currency: "USD"
@@ -63,4 +63,4 @@ tldStateTransitions:
tldStr: "tld"
tldType: "REAL"
tldUnicode: "tld"
transferGracePeriodLength: 432000000
transferGracePeriodLength: "PT432000S"

View File

@@ -1,9 +1,9 @@
addGracePeriodLength: 432000000
addGracePeriodLength: "PT2592000S"
allowedFullyQualifiedHostNames: []
allowedRegistrantContactIds: []
anchorTenantAddGracePeriodLength: 2592000000
autoRenewGracePeriodLength: 3888000000
automaticTransferLength: 432000000
anchorTenantAddGracePeriodLength: "PT2592000S"
autoRenewGracePeriodLength: "PT2592000S"
automaticTransferLength: "PT2592000S"
claimsPeriodEnd: "294247-01-10T04:00:54.775Z"
createBillingCost:
currency: "USD"
@@ -11,7 +11,7 @@ createBillingCost:
creationTime: "2022-09-01T00:00:00.000Z"
currency: "USD"
defaultPromoTokens: []
dnsAPlusAaaaTtl: 900
dnsAPlusAaaaTtl: "PT900S"
dnsDsTtl: null
dnsNsTtl: null
dnsPaused: false
@@ -27,10 +27,10 @@ idnTables: []
invoicingEnabled: false
lordnUsername: null
numDnsPublishLocks: 1
pendingDeleteLength: 432000000
pendingDeleteLength: "PT2592000S"
premiumListName: "test"
pricingEngineClassName: "google.registry.model.pricing.StaticPremiumListPricingEngine"
redemptionGracePeriodLength: 2592000000
redemptionGracePeriodLength: "PT2592000S"
registryLockOrUnlockBillingCost:
currency: "USD"
amount: 0.00
@@ -38,7 +38,7 @@ renewBillingCostTransitions:
"1970-01-01T00:00:00.000Z":
currency: "USD"
amount: 11.00
renewGracePeriodLength: 432000000
renewGracePeriodLength: "PT2592000S"
reservedListNames: []
restoreBillingCost:
currency: "USD"
@@ -52,4 +52,4 @@ tldStateTransitions:
tldStr: "1tld"
tldType: "REAL"
tldUnicode: "1tld"
transferGracePeriodLength: 432000000
transferGracePeriodLength: "PT2592000S"

View File

@@ -1,9 +1,9 @@
addGracePeriodLength: 432000000
addGracePeriodLength: "PT2592000S"
allowedFullyQualifiedHostNames: []
allowedRegistrantContactIds: []
anchorTenantAddGracePeriodLength: 2592000000
autoRenewGracePeriodLength: 3888000000
automaticTransferLength: 432000000
anchorTenantAddGracePeriodLength: "PT2592000S"
autoRenewGracePeriodLength: "PT2592000S"
automaticTransferLength: "PT2592000S"
claimsPeriodEnd: "294247-01-10T04:00:54.775Z"
createBillingCost:
currency: "USD"
@@ -11,7 +11,7 @@ createBillingCost:
creationTime: "2022-09-01T00:00:00.000Z"
currency: "USD"
defaultPromoTokens: []
dnsAPlusAaaaTtl: 900
dnsAPlusAaaaTtl: "PT900S"
dnsDsTtl: null
dnsNsTtl: null
dnsPaused: false
@@ -28,10 +28,10 @@ idnTables:
invoicingEnabled: false
lordnUsername: null
numDnsPublishLocks: 1
pendingDeleteLength: 432000000
pendingDeleteLength: "PT2592000S"
premiumListName: "test"
pricingEngineClassName: "google.registry.model.pricing.StaticPremiumListPricingEngine"
redemptionGracePeriodLength: 2592000000
redemptionGracePeriodLength: "PT2592000S"
registryLockOrUnlockBillingCost:
currency: "USD"
amount: 0.00
@@ -39,7 +39,7 @@ renewBillingCostTransitions:
"1970-01-01T00:00:00.000Z":
currency: "USD"
amount: 11.00
renewGracePeriodLength: 432000000
renewGracePeriodLength: "PT2592000S"
reservedListNames: []
restoreBillingCost:
currency: "USD"
@@ -53,4 +53,4 @@ tldStateTransitions:
tldStr: "badidn"
tldType: "REAL"
tldUnicode: "badidn"
transferGracePeriodLength: 432000000
transferGracePeriodLength: "PT2592000S"

View File

@@ -1,9 +1,9 @@
addGracePeriodLength: 432000000
addGracePeriodLength: "PT2592000S"
allowedFullyQualifiedHostNames: []
allowedRegistrantContactIds: []
anchorTenantAddGracePeriodLength: 2592000000
autoRenewGracePeriodLength: 3888000000
automaticTransferLength: 432000000
anchorTenantAddGracePeriodLength: "PT2592000S"
autoRenewGracePeriodLength: "PT2592000S"
automaticTransferLength: "PT2592000S"
claimsPeriodEnd: "294247-01-10T04:00:54.775Z"
createBillingCost:
currency: "USD"
@@ -11,7 +11,7 @@ createBillingCost:
creationTime: "2022-09-01T00:00:00.000Z"
currency: "USD"
defaultPromoTokens: []
dnsAPlusAaaaTtl: 900
dnsAPlusAaaaTtl: "PT900S"
dnsDsTtl: null
dnsNsTtl: null
dnsPaused: false
@@ -27,10 +27,10 @@ idnTables: []
invoicingEnabled: false
lordnUsername: null
numDnsPublishLocks: 1
pendingDeleteLength: 432000000
pendingDeleteLength: "PT2592000S"
premiumListName: "test"
pricingEngineClassName: "google.registry.model.pricing.StaticPremiumListPricingEngine"
redemptionGracePeriodLength: 2592000000
redemptionGracePeriodLength: "PT2592000S"
registryLockOrUnlockBillingCost:
currency: "USD"
amount: 0.00
@@ -38,7 +38,7 @@ renewBillingCostTransitions:
"1970-01-01T00:00:00.000Z":
currency: "USD"
amount: 11.00
renewGracePeriodLength: 432000000
renewGracePeriodLength: "PT2592000S"
reservedListNames: []
restoreBillingCost:
currency: "USD"
@@ -52,4 +52,4 @@ tldStateTransitions:
tldStr: "badunicode"
tldType: "REAL"
tldUnicode: "tld"
transferGracePeriodLength: 432000000
transferGracePeriodLength: "PT2592000S"

View File

@@ -1,9 +1,9 @@
addGracePeriodLength: 432000000
addGracePeriodLength: "PT2592000S"
allowedFullyQualifiedHostNames: []
allowedRegistrantContactIds: []
anchorTenantAddGracePeriodLength: 2592000000
autoRenewGracePeriodLength: 3888000000
automaticTransferLength: 432000000
anchorTenantAddGracePeriodLength: "PT2592000S"
autoRenewGracePeriodLength: "PT2592000S"
automaticTransferLength: "PT2592000S"
claimsPeriodEnd: "294247-01-10T04:00:54.775Z"
createBillingCost:
currency: "USD"
@@ -11,7 +11,7 @@ createBillingCost:
creationTime: "2022-09-01T00:00:00.000Z"
currency: "USD"
defaultPromoTokens: []
dnsAPlusAaaaTtl: 900
dnsAPlusAaaaTtl: "PT900S"
dnsDsTtl: null
dnsNsTtl: null
dnsPaused: false
@@ -27,10 +27,10 @@ idnTables: []
invoicingEnabled: false
lordnUsername: null
numDnsPublishLocks: 1
pendingDeleteLength: 432000000
pendingDeleteLength: "PT2592000S"
premiumListName: "test"
pricingEngineClassName: "google.registry.model.pricing.StaticPremiumListPricingEngine"
redemptionGracePeriodLength: 2592000000
redemptionGracePeriodLength: "PT2592000S"
registryLockOrUnlockBillingCost:
currency: "USD"
amount: 0.00
@@ -38,7 +38,7 @@ renewBillingCostTransitions:
"1970-01-01T00:00:00.000Z":
currency: "USD"
amount: 11.00
renewGracePeriodLength: 432000000
renewGracePeriodLength: "PT2592000S"
reservedListNames: []
restoreBillingCost:
currency: "USD"
@@ -52,5 +52,5 @@ tldStateTransitions:
tldStr: "extrafield"
tldType: "REAL"
tldUnicode: "extrafield"
transferGracePeriodLength: 432000000
transferGracePeriodLength: "PT2592000S"
extraField: "hello"

View File

@@ -1,13 +1,13 @@
addGracePeriodLength: 432000000
addGracePeriodLength: "PT432000S"
allowedFullyQualifiedHostNames:
- "beta"
- "zeta"
- "alpha"
- "gamma"
allowedRegistrantContactIds: []
anchorTenantAddGracePeriodLength: 2592000000
autoRenewGracePeriodLength: 3888000000
automaticTransferLength: 432000000
anchorTenantAddGracePeriodLength: "PT2592000S"
autoRenewGracePeriodLength: "PT3888000S"
automaticTransferLength: "PT432000S"
claimsPeriodEnd: "294247-01-10T04:00:54.775Z"
createBillingCost:
currency: "USD"
@@ -34,10 +34,10 @@ idnTables:
invoicingEnabled: false
lordnUsername: null
numDnsPublishLocks: 1
pendingDeleteLength: 432000000
pendingDeleteLength: "PT432000S"
premiumListName: "idns"
pricingEngineClassName: "google.registry.model.pricing.StaticPremiumListPricingEngine"
redemptionGracePeriodLength: 2592000000
redemptionGracePeriodLength: "PT2592000S"
registryLockOrUnlockBillingCost:
currency: "USD"
amount: 0.00
@@ -45,7 +45,7 @@ renewBillingCostTransitions:
"1970-01-01T00:00:00.000Z":
currency: "USD"
amount: 11.00
renewGracePeriodLength: 432000000
renewGracePeriodLength: "PT432000S"
reservedListNames: []
restoreBillingCost:
currency: "USD"
@@ -59,4 +59,4 @@ tldStateTransitions:
tldStr: "idns"
tldType: "REAL"
tldUnicode: "idns"
transferGracePeriodLength: 432000000
transferGracePeriodLength: "PT432000S"

View File

@@ -1,9 +1,9 @@
addGracePeriodLength: 432000000
addGracePeriodLength: "PT2592000S"
allowedFullyQualifiedHostNames: []
allowedRegistrantContactIds: []
anchorTenantAddGracePeriodLength: 2592000000
automaticTransferLength: 432000000
autoRenewGracePeriodLength: 3888000000
anchorTenantAddGracePeriodLength: "PT2592000S"
automaticTransferLength: "PT2592000S"
autoRenewGracePeriodLength: "PT2592000S"
claimsPeriodEnd: "294247-01-10T04:00:54.775Z"
createBillingCost:
currency: "USD"
@@ -25,9 +25,9 @@ escrowEnabled: false
idnTables: []
invoicingEnabled: false
lordnUsername: null
pendingDeleteLength: 432000000
pendingDeleteLength: "PT2592000S"
pricingEngineClassName: "google.registry.model.pricing.StaticPremiumListPricingEngine"
redemptionGracePeriodLength: 2592000000
redemptionGracePeriodLength: "PT2592000S"
registryLockOrUnlockBillingCost:
currency: "USD"
amount: 0.00
@@ -35,7 +35,7 @@ renewBillingCostTransitions:
"1970-01-01T00:00:00.000Z":
currency: "USD"
amount: 11.00
renewGracePeriodLength: 432000000
renewGracePeriodLength: "PT2592000S"
reservedListNames: []
restoreBillingCost:
currency: "USD"
@@ -47,4 +47,4 @@ serverStatusChangeBillingCost:
tldStr: "missingnullablefields"
tldType: "REAL"
tldUnicode: "missingnullablefields"
transferGracePeriodLength: 432000000
transferGracePeriodLength: "PT2592000S"

View File

@@ -18,14 +18,14 @@ reservedListNames: null
premiumListName: null
escrowEnabled: false
dnsPaused: false
addGracePeriodLength: 432000000
anchorTenantAddGracePeriodLength: 2592000000
autoRenewGracePeriodLength: 3888000000
redemptionGracePeriodLength: 2592000000
renewGracePeriodLength: 432000000
transferGracePeriodLength: 432000000
automaticTransferLength: 432000000
pendingDeleteLength: 432000000
addGracePeriodLength: "PT2592000S"
anchorTenantAddGracePeriodLength: "PT2592000S"
autoRenewGracePeriodLength: "PT2592000S"
redemptionGracePeriodLength: "PT2592000S"
renewGracePeriodLength: "PT2592000S"
transferGracePeriodLength: "PT2592000S"
automaticTransferLength: "PT2592000S"
pendingDeleteLength: "PT2592000S"
currency: "USD"
createBillingCost:
currency: "USD"

View File

@@ -5,13 +5,13 @@ reservedListNames: []
dnsPaused: false
tldType: "REAL"
escrowEnabled: false
anchorTenantAddGracePeriodLength: 2592000000
anchorTenantAddGracePeriodLength: "PT2592000S"
dnsNsTtl: null
tldStr: "outoforderfields"
roidSuffix: "TLD"
dnsWriters:
- "VoidDnsWriter"
dnsAPlusAaaaTtl: 900
dnsAPlusAaaaTtl: "PT900S"
dnsDsTtl: null
tldUnicode: "outoforderfields"
driveFolderId: "driveFolder"
@@ -19,13 +19,13 @@ invoicingEnabled: false
tldStateTransitions:
"1970-01-01T00:00:00.000Z": "GENERAL_AVAILABILITY"
premiumListName: "test"
addGracePeriodLength: 432000000
autoRenewGracePeriodLength: 3888000000
redemptionGracePeriodLength: 2592000000
renewGracePeriodLength: 432000000
transferGracePeriodLength: 432000000
automaticTransferLength: 432000000
pendingDeleteLength: 432000000
addGracePeriodLength: "PT2592000S"
autoRenewGracePeriodLength: "PT2592000S"
redemptionGracePeriodLength: "PT2592000S"
renewGracePeriodLength: "PT2592000S"
transferGracePeriodLength: "PT2592000S"
automaticTransferLength: "PT2592000S"
pendingDeleteLength: "PT2592000S"
currency: "USD"
createBillingCost:
currency: "USD"

View File

@@ -1,9 +1,9 @@
addGracePeriodLength: 432000000
addGracePeriodLength: "PT432000S"
allowedFullyQualifiedHostNames: []
allowedRegistrantContactIds: []
anchorTenantAddGracePeriodLength: 2592000000
autoRenewGracePeriodLength: 3888000000
automaticTransferLength: 432000000
anchorTenantAddGracePeriodLength: "PT2592000S"
autoRenewGracePeriodLength: "PT3888000S"
automaticTransferLength: "PT432000S"
claimsPeriodEnd: "294247-01-10T04:00:54.775Z"
createBillingCost:
currency: "USD"
@@ -11,7 +11,7 @@ createBillingCost:
creationTime: "2022-09-01T00:00:00.000Z"
currency: "USD"
defaultPromoTokens: []
dnsAPlusAaaaTtl: 900
dnsAPlusAaaaTtl: "PT900S"
dnsDsTtl: null
dnsNsTtl: null
dnsPaused: false
@@ -27,10 +27,10 @@ idnTables: []
invoicingEnabled: false
lordnUsername: null
numDnsPublishLocks: 1
pendingDeleteLength: 432000000
pendingDeleteLength: "PT432000S"
premiumListName: "test"
pricingEngineClassName: "google.registry.model.pricing.StaticPremiumListPricingEngine"
redemptionGracePeriodLength: 2592000000
redemptionGracePeriodLength: "PT2592000S"
registryLockOrUnlockBillingCost:
currency: "USD"
amount: 0.00
@@ -38,7 +38,7 @@ renewBillingCostTransitions:
"1970-01-01T00:00:00.000Z":
currency: "USD"
amount: 11.00
renewGracePeriodLength: 432000000
renewGracePeriodLength: "PT432000S"
reservedListNames: []
restoreBillingCost:
currency: "USD"
@@ -52,4 +52,4 @@ tldStateTransitions:
tldStr: "tld"
tldType: "REAL"
tldUnicode: "tld"
transferGracePeriodLength: 432000000
transferGracePeriodLength: "PT432000S"

View File

@@ -1,9 +1,9 @@
addGracePeriodLength: 432000000
addGracePeriodLength: "PT432000S"
allowedFullyQualifiedHostNames: []
allowedRegistrantContactIds: []
anchorTenantAddGracePeriodLength: 2592000000
autoRenewGracePeriodLength: 3888000000
automaticTransferLength: 432000000
anchorTenantAddGracePeriodLength: "PT2592000S"
autoRenewGracePeriodLength: "PT3888000S"
automaticTransferLength: "PT432000S"
claimsPeriodEnd: "294247-01-10T04:00:54.775Z"
createBillingCost:
currency: "USD"
@@ -11,7 +11,7 @@ createBillingCost:
creationTime: "2022-09-01T00:00:00.000Z"
currency: "USD"
defaultPromoTokens: []
dnsAPlusAaaaTtl: 900
dnsAPlusAaaaTtl: "PT900S"
dnsDsTtl: null
dnsNsTtl: null
dnsPaused: false
@@ -27,10 +27,10 @@ idnTables: []
invoicingEnabled: false
lordnUsername: null
numDnsPublishLocks: 1
pendingDeleteLength: 432000000
pendingDeleteLength: "PT432000S"
premiumListName: "test"
pricingEngineClassName: "google.registry.model.pricing.StaticPremiumListPricingEngine"
redemptionGracePeriodLength: 2592000000
redemptionGracePeriodLength: "PT2592000S"
registryLockOrUnlockBillingCost:
currency: "USD"
amount: 0.00
@@ -38,7 +38,7 @@ renewBillingCostTransitions:
"1970-01-01T00:00:00.000Z":
currency: "USD"
amount: 11.00
renewGracePeriodLength: 432000000
renewGracePeriodLength: "PT432000S"
reservedListNames: []
restoreBillingCost:
currency: "USD"
@@ -52,4 +52,4 @@ tldStateTransitions:
tldStr: %TLDSTR%
tldType: "REAL"
tldUnicode: %TLDUNICODE%
transferGracePeriodLength: 432000000
transferGracePeriodLength: "PT432000S"

View File

@@ -756,7 +756,6 @@
id bigserial not null,
update_timestamp timestamptz,
email_address text not null,
gaia_id text,
registry_lock_password_hash text,
registry_lock_password_salt text,
global_role text not null,
@@ -852,7 +851,6 @@ create index reservedlist_name_idx on "ReservedList" (name);
create index spec11threatmatch_registrar_id_idx on "Spec11ThreatMatch" (registrar_id);
create index spec11threatmatch_tld_idx on "Spec11ThreatMatch" (tld);
create index spec11threatmatch_check_date_idx on "Spec11ThreatMatch" (check_date);
create index user_gaia_id_idx on "User" (gaia_id);
create index user_email_address_idx on "User" (email_address);
alter table if exists "DelegationSignerData"

View File

@@ -0,0 +1,42 @@
## About
We have deployed a Cloud Build notifier on Cloud Run that sends build failure
notifications to a Google Chat space. This folder contains the configuration and
helper scripts.
## Details
The instructions for notifier setup can be found on the
[GCP documentation site](https://cloud.google.com/build/docs/configuring-notifications/configure-googlechat).
For the **initial** setup, Cloud Build provides a
[script](https://cloud.google.com/build/docs/configuring-notifications/automate)
that automates a lot of the work.
With the automated procedure, the notifier is deployed as a Cloud Run service
named `googlechat-notifier` in a single region of user's choice. The notifier
subscribes to the `cloud-builds` Pub/sub topic for build statuses, applies our
custom filter, and sends matching notifications to our Google Chat space.
Our custom configuration for the notifier is in the `googlechat.yaml` file. It
defines:
* The build status filter, currently matching for all triggered builds that
have failed or timed out. The filter semantics are explained
[here](https://cloud.google.com/build/docs/configuring-notifications/configure-googlechat#using_cel_to_filter_build_events).
* The secret name of the Chat Webhook token stored in the Secret Manager. This
token allows the notifier to send notifications to our Chat space. The
webhook token can be managed in the Google Chat client.
The `googlechat.yaml` configuration file should be uploaded to the GCS bucket
`{PROJECT_ID}-notifiers-config`. The `upload_config.sh` script can be used for
uploading. The new configuration will take effect eventually after all currently
running notifier instances shutdown due to inactivity, which is bound to happen
with our workload (Note: this depends on the scale-to-zero policy, which is the
default).
To make sure the new configurations take effect immediately, the
`update_notifier.sh`. It deploys a new revision of the notifier and shuts down
old instances.
Note that if the notifier is moved to another region, the full setup must be
repeated.

View File

@@ -0,0 +1,14 @@
apiVersion: cloud-build-notifiers/v1
kind: GoogleChatNotifier
metadata:
name: nomulus-cloudbuild-googlechat-notifier
spec:
notification:
filter: has(build.build_trigger_id) && build.status in [Build.Status.FAILURE, Build.Status.TIMEOUT]
delivery:
webhookUrl:
secretRef: webhook-url
secrets:
- name: webhook-url
value: projects/_project_id_/secrets/Chat-Webhook-CloudBuildNotifications/versions/latest

View File

@@ -0,0 +1,44 @@
#!/bin/bash
# Copyright 2019 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.
#
# This script redeploys the Cloud Build notifier with the latest container
# image. It can be used to force immediate configuration change after invoking
# `upload_config.sh`.
set -e
if [ $# -ne 1 ];
then
echo "Usage: $0 <project_id>"
exit 1
fi
project_id="$1"
region=$(gcloud run services list \
--filter="SERVICE:googlechat-notifier" \
--format="csv[no-heading](REGION)" \
--project="${project_id}")
SERVICE_NAME=googlechat-notifier
IMAGE_PATH=us-east1-docker.pkg.dev/gcb-release/cloud-build-notifiers/googlechat:latest
DESTINATION_CONFIG_PATH="gs://${project_id}-notifiers-config/googlechat.yaml"
gcloud run deploy "${SERVICE_NAME}" --image="${IMAGE_PATH}" \
--no-allow-unauthenticated \
--update-env-vars="CONFIG_PATH=${DESTINATION_CONFIG_PATH},PROJECT_ID=${project_id}" \
--region="${region}" \
--project="${project_id}" \
|| fail "failed to deploy notifier service -- check service logs for configuration error"

View File

@@ -0,0 +1,36 @@
#!/bin/bash
# Copyright 2019 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.
#
# This script uploads the `googlechat.yaml` configuration to the GCS bucket used
# by the Cloud Build notifier. The new config takes effect only AFTER all
# currently running notifier instances are shut down due to inactivity. To force
# immediate change, use `update_notifier.sh` in this directory to redeploy the
# service.
set -e
if [ $# -ne 1 ];
then
echo "Usage: $0 <project_id>"
exit 1
fi
project_id="$1"
SCRIPT_DIR="$(realpath $(dirname $0))"
cat "${SCRIPT_DIR}"/googlechat.yaml \
| sed "s/_project_id_/${project_id}/g" \
| gcloud storage cp - "gs://${project_id}-notifiers-config/googlechat.yaml"