1
0
mirror of https://github.com/google/nomulus synced 2026-07-08 00:56:53 +00:00

Compare commits

...

39 Commits

Author SHA1 Message Date
Pavlo Tkach 69ea87be31 Add handler for Console API requests and XSRF token creation and verification (#2211) 2023-11-09 17:51:53 -05:00
Lai Jiang 779d0c9d37 Add a fallback token verifier (#2216)
This allows us to switch the proxy to a different client ID without
disrupting the service. This is a temporary measure and will be removed
once the switch is complete.
2023-11-09 16:05:14 -05:00
Weimin Yu 2855944214 Add TLD BSA enroll start date to schema (#2215)
Also adds a placeholder getter in the Tld class, so that it can be
mocked/spied in tests. This way more BSA related code can be submitted
before the schema is deployed to prod.
2023-11-09 13:52:45 -05:00
Ben McIlwain 992d1c1349 Reduce the QPS of the refresh DNS for all domains action (#2212)
This also adds a targeted QPS as a parameter in case we need to manually bump it
up (or down) for some reason without having to make code changes and re-deploy.
2023-11-08 13:47:37 -05:00
Pavlo Tkach f50290ce1d Add static IP connector to crash and alpha configs (#2213) 2023-11-08 13:26:32 -05:00
Pavlo Tkach e647d4e215 Add retry to cloud build node installation (#2210) 2023-11-06 09:15:36 -05:00
Lai Jiang 08471242df Refactor transact() related methods. (#2195)
This PR makes a few changes to make it possible to turn on
per-transaction isolation level with minimal disruption:

1) Changed the signatures of transact() and reTransact() methods to allow
passing in lambdas that throw checked exceptions. Previously one has
always to wrap such lambdas in try-and-retrow blocks, which wasn't a
big issue when one can liberally open nested transactions around small
lambdas and keeps the "throwing" part outside the lambda. This becomes a
much bigger hassle when the goal is to eliminate nested transactions and
put as much code as possible within the top-level lambda. As a result,
the transactNoRetry() method now handles checked exceptions by re-throwing
them as runtime exceptions.

2) Changed the name and meaning of the config file field that used to
indicate if per-transaction isolation level is enabled or not. Now it
decides if transact() is called within a transaction, whether to
throw or to log, regardless whether the transaction could have
succeeded based on the isolation override level (if provided). The
flag will initially be set to false and would help us identify all
instances of nested calls and either refactor them or use reTransact()
instead. Once we are fairly certain that no nested calls to transact()
exists, we flip the flag to true and start enforcing this logic.
Eventually the flag will go away and nested calls to transact() will
always throw.

3) Per-transaction isolation level will now always be applied, if an
override is provided. Because currently there should be no actual
use of such feature (except for places where we explicitly use an
override and have ensured no nested transactions exist, like in
RefreshDnsForAllDomainsAction), we do not expect any issues with
conflicting isolation levels, which would resulted in failure.

3) transactNoRetry() is made package private and removed from the
exposed API of JpaTransactionManager. This saves a lot of redundant
methods that do not have a practical use. The only instances where this
method was called outside the package was in the reader of
RegistryJpaIO, which should have no problem with retrying.
2023-11-03 17:43:27 -04:00
Lai Jiang cd23fea698 Switch to a stronger algorithm for password hashing (#2191)
We have been using SHA256 to hash passwords (for both EPP and registry lock),
which is now considered too weak.

This PR switches to using Scrypt, a memory-hard slow hash function, with
recommended parameters per go/crypto-password-hash.

To ease the transition, when a password is being verified, both Scrypt
and SHA256 are tried. If SHA256 verification is successful, we re-hash
the verified password with Scrypt and replace the stored SHA256 hash
with the new one. This way, as long as a user uses the password once
before the transition period ends (when Scrypt becomes the only valid
algorithm), there would be no need for manual intervention from them.

We will send out notifications to users to remind them of the transition
and urge them to use the password (which should not be a problem with
EPP, but less so with the registry lock). After the transition,
out-of-band reset for EPP password, or remove-and-add on the console for
registry lock password, would be required for the hashes that have not
been re-saved.

Note that the re-save logic is not present for console user's registry
lock password, as there is no production data for console users yet.
Only legacy GAE user's password requires re-save.
2023-11-03 17:29:01 -04:00
Ben McIlwain ba54208dad Also load domains for domain checks of type renew/transfer (#2207)
The domains (and their associated billing recurrences) were already being loaded
to check restores, but they also now need to be loaded for renews and transfers
as well, as the billing renewal behavior on the recurrence could be modifying
the relevant renew price that should be shown. (The renew price is used for
transfers as well.)

See https://buganizer.corp.google.com/issues/306212810
2023-11-03 14:33:34 -04:00
Weimin Yu b5e131ecba Add BSA schema (#2204)
* Add BSA schema

Also lock down flyway due to java8 compatiblity
2023-11-02 15:38:23 -04:00
Pavlo Tkach 87e99f59bc Replace node.js installation method in build.sh (#2206) 2023-11-02 14:17:18 -04:00
Weimin Yu 30accea383 Add keyring support for BSA API key (#2208)
* Add keyring support for BSA API key

Also removing JSON_CREDENTIAL. It is an exported service account key,
which we no longer use.
2023-11-02 14:08:50 -04:00
Lai Jiang 72e0101746 Delete unused actions (#2197)
Both actions have not been used for a while (the wipe out action
actually caused problems when it ran unintentionally and wiped out QA).
Keeping them around is a burden when refactoring efforts have to take
them into consideration.

It is always possible to resurrect them form git history should the need
arises.
2023-11-02 11:41:03 -04:00
Lai Jiang 3090df9a78 Upgrade to Java 17 runtime (#2201)
We finally fixed Spinnaker (I hope) to deploy bundled services with Java
17 runtime. Note that the bytecodes are still targeting Java 8. The only
change this PR introduces is to switch the runtime environment to Java
17.

TESTED=deployed to crash.
2023-11-02 10:08:14 -04:00
gbrodman 7332b1fa38 Add TypeAdapters for VKey objects (#2194)
GSON doesn't allow for clean (de)serialization of Class or Serializable
objects which we'll need for converting VKeys to/from JSON.
2023-10-31 15:14:41 -04:00
Lai Jiang 9330e3a50d Move truely public endpoints to a separate Auth (#2200)
This allows us to more easily refactor public endpoints that still use
the legacy auth mechanism to identify logged-in users (for the legacy
console).
2023-10-31 13:58:45 -04:00
gbrodman 1d6b119340 Add a console action to retrieve a paged list of domains (#2193)
In the future we'll want to add searching capability but for now we can
go with straightforward pagination.
2023-10-30 17:01:31 -04:00
Weimin Yu 8158f761c8 Add BSA configurations (#2202) 2023-10-30 16:44:28 -04:00
Pavlo Tkach 08838e091f Enable BACKEND service to route external traffic through VPC on Sandbox (#2199) 2023-10-30 13:36:04 -04:00
sarahcaseybot 59720a207d Change the default config for perTransactionIsolation to true (#2196)
This was already set to true in all environments except prod last week. Now that the release has gone out and we have not seen any issues, we should feel safe turning this on in production as well.
2023-10-26 17:16:02 -04:00
Pavlo Tkach 26bae65e1e Add registrar details view (#2186) 2023-10-26 09:14:09 -04:00
Pavlo Tkach 23a2861b37 Remove node.js download instruction (#2192) 2023-10-25 14:48:35 -04:00
Pavlo Tkach 341238305d Update console versions (#2190) 2023-10-24 09:34:02 -04:00
Lai Jiang d210bed744 Add connection.disconnect() in finally blocks (#2189) 2023-10-23 16:38:16 -04:00
dependabot[bot] fe710e5510 Bump postcss from 8.4.21 to 8.4.31 in /console-webapp (#2187)
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.21 to 8.4.31.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.21...8.4.31)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-23 10:29:49 -04:00
sarahcaseybot 8f8ffe7020 Add a dryrun flag to configure_tld command (#2188)
This will be used for presubmit testing.
2023-10-20 16:16:05 -04:00
Lai Jiang 16e5018489 Update postcss version (#2185)
Per https://github.com/google/nomulus/security/dependabot/17
2023-10-20 13:30:40 -04:00
Lai Jiang af303bd26f Remove URLFetch (#2181)
We previously needed to use URLFetch in some instances where TLS 1.3 is
required (mostly when connecting to ICANN servers),and the JDK-bundled SSL
engine that came with App Engine runtime did not support TLS 1.3.

It appears now that the Java 8 runtime on App Engine supports TLS 1.3
out of the box, which allows us to get rid of URLFetch, which depends on
App Engine APIs.

Also removed some redundant retry and logging logic, now that we know
the HTTP client behaves correctly.

TESTED=modified the CannedScriptExecutionAction and deployed to alpha, used the
new HTTP client to connect to the three URL endpoints that were
problematic before and confirmed that TLS connections can be established. HTTP
sessions were rejected in some cases when authentication failed, but
that was expected.
2023-10-19 14:51:51 -04:00
sarahcaseybot bf3bb5d804 Add a Cloud Build job for syncing Tld configuration files from the internal repo with the database (#2174)
* Add a cloudbuild-tld-sync job

This job checks the Tld config files in the internal repo and syncs them with the actual Tld objects in the database using the configure_tld numulus command.

* Add the dockerfile and shell script

* Force the command

* Add comments

* add newline

* Create a separate copy of the job for each environment

* fix file name

* Fix indentation
2023-10-19 14:01:40 -04:00
dependabot[bot] dcb16e05bd Bump @babel/traverse from 7.22.10 to 7.23.2 in /console-webapp (#2184)
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.22.10 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-19 11:46:02 -04:00
sarahcaseybot 2facedd60f Lower the isolation level for RefreshDnsForAllDomainsAction (#2182)
* Lower the isolation level for RefreshDnsForAllDomainsAction

This lowers the isolation level to TRANSACTION_REPEATABLE_READ which will hopefully allow the action to run the entire action without timing out on our larger TLDs.

* Unchange default config
2023-10-17 16:58:37 -04:00
Lai Jiang b1ec81f054 Remove the wipeout job on QA (#2183) 2023-10-17 13:05:31 -04:00
gbrodman 779da518df Pass name/email/phone info to the new console front end (#2180) 2023-10-16 16:51:35 -04:00
sarahcaseybot 4f53ae0e89 Use reTransact when loading the cache for database objects (#2179)
Cache loads will likely always be inner transactions, if they have a transaction at all. Cache loads do not always call a transaction since they are only necessary if the cache is not fresh at the time it is called. Since the cache itself needs to decide whether or not a DB transaction is necessary, it should use the reTransact method to safely indicate that the isolation level of the outer transaction is what should be used.
2023-10-16 15:22:22 -04:00
gbrodman da04caeea2 Don't check cert validation if we're not changing the certs in the console (#2178)
If the cert(s) are invalid or expired that's a problem, but that
shouldn't necessarily prevent us from changing other things. If we're
not changing the certs, leave them alone.
2023-10-16 13:37:57 -04:00
gbrodman a63916b08e Refine error handling in RequestHandler and the console slightly (#2177)
If we don't explicitly handle random unexpected exceptions, the error
that the front end receives includes a big ole stacktrace, which is
unhelpful for regular users and possibly bad to expose. Instead, we
should provide a vague "something went wrong" message.

Separately, we can create a default SnackBar options and use that (we
want it longer than 1.5 seconds because that's pretty short).
2023-10-12 14:03:12 -04:00
Lai Jiang 36bd508bf9 Remove OAuthAuthenticationMechanism (#2171)
Also made some refactoring to various Auth related classes to clean up things a bit and make the logic less convoluted:

1. In Auth, remove AUTH_API_PUBLIC as it is only used by the WHOIS and EPP endpoints accessed by the proxy. Previously, the proxy relies on OAuth and its service account is not given admin role (in OAuth parlance), so we made them accessible by a public user, deferring authorization to the actions themselves. In practice, OAuth checks for allowlisted client IDs and only the proxy client ID was allowlisted, which effectively limited access to only the proxy anyway.

2. In AuthResult, expose the service account email if it is at APP level. RequestAuthenticator will print out the auth result and therefore log the email, making it easy to identify which account was used. This field is mutually exclusive to the user auth info field. As a result, the factory methods are refactored to explicitly create either APP or USER level auth result.

3. Completely re-wrote RequestAuthenticatorTest. Previously, the test mingled testing functionalities of the target class with testing how various authentication mechanisms work. Now they are cleanly decoupled, and each method in RequestAuthenticator is tested individually.

4. Removed nomulus-config-production-sample.yaml as it is vastly out of date.
2023-10-11 19:12:26 -04:00
Lai Jiang bbdbfe85ed Remove the GAIA ID column from the User table (#2172)
The field has already been removed from the Java code base in #2170.
2023-10-11 12:47:48 -04:00
gbrodman 2a7e9a266a Fix minor alignment issue on console WHOIS page (#2166) 2023-10-11 09:25:05 -04:00
208 changed files with 8593 additions and 7031 deletions
+2 -3
View File
@@ -60,9 +60,8 @@ dependencyLocking {
}
node {
download = true
version = "16.14.0"
npmVersion = "6.14.11"
download = false
version = "16.19.0"
}
wrapper {
+1141 -988
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, ViewChild } from '@angular/core';
import { AfterViewInit, 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';
@@ -24,7 +24,7 @@ import { MatSidenav } from '@angular/material/sidenav';
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
export class AppComponent implements AfterViewInit {
renderRouter: boolean = true;
@ViewChild('sidenav')
+8
View File
@@ -48,6 +48,11 @@ 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';
import { SnackBarModule } from './snackbar.module';
import {
RegistrarDetailsComponent,
RegistrarDetailsWrapperComponent,
} from './registrar/registrarDetails.component';
@NgModule({
declarations: [
@@ -62,6 +67,8 @@ import WhoisComponent from './settings/whois/whois.component';
HomeComponent,
PromotionsWidgetComponent,
RegistrarComponent,
RegistrarDetailsComponent,
RegistrarDetailsWrapperComponent,
RegistrarSelectorComponent,
ResourcesWidgetComponent,
SecurityComponent,
@@ -79,6 +86,7 @@ import WhoisComponent from './settings/whois/whois.component';
FormsModule,
HttpClientModule,
MaterialModule,
SnackBarModule,
],
providers: [
BackendService,
@@ -5,21 +5,23 @@
<mat-icon class="console-app__widget-icon">call</mat-icon>
<h1 class="console-app__widget-title">Contact Support</h1>
<h4 class="secondary-text text-center">
View Google Registry support email and phone information
Let us know if you have any questions
</h4>
</div>
<div class="console-app__widget_right">
<button mat-button color="primary" class="console-app__widget-link">
Give us a Call
</button>
<div class="console-app__widget-section-header">Give us a Call</div>
<p class="secondary-text">
Call Google Registry support at <b>+1 (404) 978 8419</b>
Call {{ userDataService.userData?.productName }} support at
<a href="tel:{{ userDataService.userData?.supportPhoneNumber }}">{{
userDataService.userData?.supportPhoneNumber
}}</a>
</p>
<button mat-button color="primary" class="console-app__widget-link">
Send us an Email
</button>
<div class="console-app__widget-section-header">Send us an Email</div>
<p class="secondary-text">
Email Google Registry at <b>support@google.com</b>
Email {{ userDataService.userData?.productName }} at
<a href="mailto:{{ userDataService.userData?.supportEmail }}">{{
userDataService.userData?.supportEmail
}}</a>
</p>
</div>
</div>
@@ -13,11 +13,12 @@
// limitations under the License.
import { Component } from '@angular/core';
import { UserDataService } from 'src/app/shared/services/userData.service';
@Component({
selector: '[app-contact-widget]',
templateUrl: './contact-widget.component.html',
})
export class ContactWidgetComponent {
constructor() {}
constructor(public userDataService: UserDataService) {}
}
@@ -45,6 +45,7 @@ import { DialogModule } from '@angular/cdk/dialog';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatChipsModule } from '@angular/material/chips';
@NgModule({
exports: [
@@ -81,6 +82,7 @@ import { MatPaginatorModule } from '@angular/material/paginator';
DialogModule,
MatSnackBarModule,
MatPaginatorModule,
MatChipsModule,
],
})
export class MaterialModule {}
@@ -13,7 +13,9 @@
<mat-label>Registrar</mat-label>
<mat-select
[ngModel]="registrarService.activeRegistrarId"
(selectionChange)="registrarService.updateRegistrar($event.value)"
(selectionChange)="
registrarService.updateSelectedRegistrar($event.value)
"
>
<mat-option
*ngFor="let registrar of registrarService.registrars"
@@ -73,7 +73,7 @@ export class RegistrarService implements GlobalLoader {
)[0];
}
public updateRegistrar(registrarId: string) {
public updateSelectedRegistrar(registrarId: string) {
this.activeRegistrarId = registrarId;
this.activeRegistrarIdChange.next(registrarId);
}
@@ -89,8 +89,6 @@ export class RegistrarService implements GlobalLoader {
}
loadingTimeout() {
this._snackBar.open('Timeout loading registrars', undefined, {
duration: 1500,
});
this._snackBar.open('Timeout loading registrars');
}
}
@@ -0,0 +1,40 @@
<div class="registrarDetails">
<h3 mat-dialog-title>Edit Registrar: {{ registrarInEdit.registrarId }}</h3>
<div mat-dialog-content>
<form (ngSubmit)="saveAndClose($event)">
<mat-form-field class="registrarDetails__input">
<mat-label>Registry Lock:</mat-label>
<mat-select
[(ngModel)]="registrarInEdit.registryLockAllowed"
name="registryLockAllowed"
>
<mat-option [value]="true">True</mat-option>
<mat-option [value]="false">False</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="registrarDetails__input">
<mat-label>Onboarded TLDs: </mat-label>
<mat-chip-grid #chipGrid aria-label="Enter TLD">
<mat-chip-row
*ngFor="let tld of registrarInEdit.allowedTlds"
(removed)="removeTLD(tld)"
>
{{ tld }}
<button matChipRemove aria-label="'remove ' + tld">
<mat-icon>cancel</mat-icon>
</button>
</mat-chip-row>
</mat-chip-grid>
<input
placeholder="New tld..."
[matChipInputFor]="chipGrid"
(matChipInputTokenEnd)="addTLD($event)"
/>
</mat-form-field>
<mat-dialog-actions>
<button mat-button (click)="onCancel($event)">Cancel</button>
<button type="submit" mat-button color="primary">Save</button>
</mat-dialog-actions>
</form>
</div>
</div>
@@ -0,0 +1,8 @@
.registrarDetails {
min-width: 30vw;
&__input {
display: block;
margin-top: 0.5rem;
}
}
@@ -0,0 +1,105 @@
// 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, Injector } from '@angular/core';
import { Registrar, RegistrarService } from './registrar.service';
import { BreakpointObserver } from '@angular/cdk/layout';
import {
MAT_BOTTOM_SHEET_DATA,
MatBottomSheet,
MatBottomSheetRef,
} from '@angular/material/bottom-sheet';
import {
MAT_DIALOG_DATA,
MatDialog,
MatDialogRef,
} from '@angular/material/dialog';
import { MatChipInputEvent } from '@angular/material/chips';
const MOBILE_LAYOUT_BREAKPOINT = '(max-width: 599px)';
@Component({
selector: 'app-registrar-details',
templateUrl: './registrarDetails.component.html',
styleUrls: ['./registrarDetails.component.scss'],
})
export class RegistrarDetailsComponent {
registrarInEdit!: Registrar;
private elementRef:
| MatBottomSheetRef<RegistrarDetailsComponent>
| MatDialogRef<RegistrarDetailsComponent>;
constructor(
protected registrarService: RegistrarService,
private injector: Injector
) {
// We only inject one, either Dialog or Bottom Sheet data
// so one of the injectors is expected to fail
try {
var params = this.injector.get(MAT_DIALOG_DATA);
this.elementRef = this.injector.get(MatDialogRef);
} catch (e) {
var params = this.injector.get(MAT_BOTTOM_SHEET_DATA);
this.elementRef = this.injector.get(MatBottomSheetRef);
}
this.registrarInEdit = JSON.parse(JSON.stringify(params.registrar));
}
onCancel(e: MouseEvent) {
if (this.elementRef instanceof MatBottomSheetRef) {
this.elementRef.dismiss();
} else if (this.elementRef instanceof MatDialogRef) {
this.elementRef.close();
}
}
saveAndClose(e: MouseEvent) {
// TODO: Implement save call to API
this.onCancel(e);
}
addTLD(e: MatChipInputEvent) {
this.removeTLD(e.value); // Prevent dups
this.registrarInEdit.allowedTlds = this.registrarInEdit.allowedTlds?.concat(
[e.value.toLowerCase()]
);
}
removeTLD(tld: string) {
this.registrarInEdit.allowedTlds = this.registrarInEdit.allowedTlds?.filter(
(v) => v != tld
);
}
}
@Component({
selector: 'app-registrar-details-wrapper',
template: '',
})
export class RegistrarDetailsWrapperComponent {
constructor(
private dialog: MatDialog,
private bottomSheet: MatBottomSheet,
protected breakpointObserver: BreakpointObserver
) {}
open(registrar: Registrar) {
const config = { data: { registrar } };
if (this.breakpointObserver.isMatched(MOBILE_LAYOUT_BREAKPOINT)) {
this.bottomSheet.open(RegistrarDetailsComponent, config);
} else {
this.dialog.open(RegistrarDetailsComponent, config);
}
}
}
@@ -1,10 +1,34 @@
<div class="console-app__registrars">
<mat-form-field class="console-app__registrars-filter">
<mat-label>Search</mat-label>
<input
matInput
(keyup)="applyFilter($event)"
placeholder="..."
type="search"
/>
<mat-icon matPrefix>search</mat-icon>
</mat-form-field>
<mat-table
[dataSource]="dataSource"
class="mat-elevation-z8"
class="console-app__registrars-table"
matSort
>
<ng-container matColumnDef="edit">
<mat-header-cell *matHeaderCellDef></mat-header-cell>
<mat-cell *matCellDef="let row">
<button
mat-icon-button
color="primary"
aria-label="Edit registrar"
(click)="openDetails($event, row)"
>
<mat-icon>edit</mat-icon>
</button>
</mat-cell>
</ng-container>
<ng-container
*ngFor="let column of columns"
[matColumnDef]="column.columnDef"
@@ -13,7 +37,10 @@
<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-row
*matRowDef="let row; columns: displayedColumns"
(click)="registrarService.updateSelectedRegistrar(row.registrarId)"
></mat-row>
</mat-table>
<mat-paginator
@@ -21,4 +48,7 @@
[pageSizeOptions]="[5, 10, 20]"
showFirstLastButtons
></mat-paginator>
<app-registrar-details-wrapper
#registrarDetailsView
></app-registrar-details-wrapper>
</div>
@@ -7,6 +7,11 @@
overflow: auto;
}
&__registrars-filter {
min-width: $min-width !important;
width: 100%;
}
&__registrars-table {
min-width: $min-width !important;
}
@@ -16,6 +21,10 @@
}
.mat-column {
&-edit {
max-width: 55px;
padding-left: 5px;
}
&-driveId {
min-width: 200px;
word-break: break-all;
@@ -17,6 +17,7 @@ import { Registrar, RegistrarService } from './registrar.service';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { RegistrarDetailsWrapperComponent } from './registrarDetails.component';
@Component({
selector: 'app-registrar',
@@ -76,10 +77,12 @@ export class RegistrarComponent {
cell: (record: Registrar) => `${record.driveFolderId || ''}`,
},
];
displayedColumns = this.columns.map((c) => c.columnDef);
displayedColumns = ['edit'].concat(this.columns.map((c) => c.columnDef));
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
@ViewChild('registrarDetailsView')
detailsComponentWrapper!: RegistrarDetailsWrapperComponent;
constructor(protected registrarService: RegistrarService) {
this.dataSource = new MatTableDataSource<Registrar>(
@@ -91,4 +94,14 @@ export class RegistrarComponent {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
openDetails(event: MouseEvent, registrar: Registrar) {
event.stopPropagation();
this.detailsComponentWrapper.open(registrar);
}
applyFilter(event: Event) {
const filterValue = (event.target as HTMLInputElement).value;
this.dataSource.filter = filterValue.trim().toLowerCase();
}
}
@@ -129,9 +129,7 @@ export class ContactDetailsDialogComponent {
operationObservable.subscribe({
complete: this.onCloseCallback.bind(this),
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error, undefined, {
duration: 1500,
});
this._snackBar.open(err.error);
},
});
}
@@ -175,9 +173,7 @@ export default class ContactComponent {
if (confirm(`Please confirm contact ${contact.name} delete`)) {
this.contactService.deleteContact(contact).subscribe({
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error, undefined, {
duration: 1500,
});
this._snackBar.open(err.error);
},
});
}
@@ -64,9 +64,7 @@ export default class SecurityComponent {
this.resetDataSource();
},
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error, undefined, {
duration: 1500,
});
this._snackBar.open(err.error);
},
});
this.cancel();
@@ -24,14 +24,14 @@
display: flex;
flex-wrap: wrap;
margin-bottom: 5px;
min-width: 450px;
min-width: 400px;
width: 50%;
max-width: 50%;
}
&__section-description {
display: inline-block;
margin-block-start: 1em;
min-width: 150px;
width: 160px;
}
&__section-form {
display: inline-block;
@@ -61,9 +61,8 @@ export default class WhoisComponent {
this.resetDataSource();
},
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error, undefined, {
duration: 1500,
});
this._snackBar.open(err.error);
this.loading = false;
},
});
this.cancel();
@@ -19,8 +19,11 @@ import { MatSnackBar } from '@angular/material/snack-bar';
import { GlobalLoader, GlobalLoaderService } from './globalLoader.service';
export interface UserData {
isAdmin: boolean;
globalRole: string;
isAdmin: boolean;
productName: string;
supportEmail: string;
supportPhoneNumber: string;
technicalDocsUrl: string;
}
@@ -49,8 +52,6 @@ export class UserDataService implements GlobalLoader {
}
loadingTimeout() {
this._snackBar.open('Timeout loading user data', undefined, {
duration: 1500,
});
this._snackBar.open('Timeout loading user data');
}
}
+24
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.
import { NgModule } from '@angular/core';
import { MAT_SNACK_BAR_DEFAULT_OPTIONS } from '@angular/material/snack-bar';
/** Provides a default set of options for the snack bar. */
@NgModule({
providers: [
{ provide: MAT_SNACK_BAR_DEFAULT_OPTIONS, useValue: { duration: 5000 } },
],
})
export class SnackBarModule {}
+4
View File
@@ -47,6 +47,10 @@ body {
min-width: auto !important;
height: min-content !important;
}
&-section-header {
font-weight: 500;
color: var(--primary) !important;
}
&-title {
color: var(--primary) !important;
text-align: center;
@@ -43,6 +43,12 @@ public class BatchModule {
public static final String PARAM_DRY_RUN = "dryRun";
public static final String PARAM_FAST = "fast";
@Provides
@Parameter("url")
static String provideUrl(HttpServletRequest req) {
return extractRequiredParameter(req, "url");
}
@Provides
@Parameter("jobName")
static Optional<String> provideJobName(HttpServletRequest req) {
@@ -14,26 +14,20 @@
package google.registry.batch;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.POST;
import static google.registry.util.RegistrarUtils.normalizeRegistrarId;
import static java.nio.charset.StandardCharsets.UTF_8;
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.Parameter;
import google.registry.request.Response;
import google.registry.request.UrlConnectionService;
import google.registry.request.UrlConnectionUtils;
import google.registry.request.auth.Auth;
import google.registry.util.EmailMessage;
import java.io.IOException;
import java.util.Set;
import java.net.URL;
import javax.inject.Inject;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.net.ssl.HttpsURLConnection;
/**
* Action that executes a canned script specified by the caller.
@@ -50,88 +44,45 @@ import javax.mail.internet.InternetAddress;
@Action(
service = Action.Service.BACKEND,
path = "/_dr/task/executeCannedScript",
method = POST,
method = {POST, GET},
automaticallyPrintOk = true,
auth = Auth.AUTH_API_ADMIN)
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 UrlConnectionService urlConnectionService;
@Inject Response response;
@Inject
CannedScriptExecutionAction(
GroupsConnection groupsConnection,
GmailClient gmailClient,
@Config("projectId") String projectId,
@Config("gSuiteDomainName") String gSuiteDomainName,
@Config("newAlertRecipientEmailAddress") 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);
}
@Parameter("url")
String url;
@Inject
CannedScriptExecutionAction() {}
@Override
public void run() {
Integer responseCode = null;
String responseContent = null;
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());
// One success is enough for validation.
return;
} catch (IOException e) {
logger.atWarning().withCause(e).log("Failed to check %s", groupKey);
}
logger.atInfo().log("Connecting to: %s", url);
HttpsURLConnection connection =
(HttpsURLConnection) urlConnectionService.createConnection(new URL(url));
responseCode = connection.getResponseCode();
logger.atInfo().log("Code: %d", responseCode);
logger.atInfo().log("Headers: %s", connection.getHeaderFields());
responseContent = new String(UrlConnectionUtils.getResponseBytes(connection), UTF_8);
logger.atInfo().log("Response: %s", responseContent);
} catch (Exception e) {
logger.atWarning().withCause(e).log("Connection to %s failed", url);
throw new RuntimeException(e);
} finally {
if (responseCode != null) {
response.setStatus(responseCode);
}
if (responseContent != null) {
response.setPayload(responseContent);
}
}
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();
}
}
@@ -1,161 +0,0 @@
// Copyright 2021 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.batch;
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryEnvironment;
import google.registry.persistence.PersistenceModule.SchemaManagerConnection;
import google.registry.request.Action;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.util.Retrier;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.function.Supplier;
import javax.inject.Inject;
/**
* Wipes out all Cloud SQL data in a Nomulus GCP environment.
*
* <p>This class is created for the QA environment, where migration testing with production data
* will happen. A regularly scheduled wipeout is a prerequisite to using production data there.
*/
@Action(
service = Action.Service.BACKEND,
path = "/_dr/task/wipeOutCloudSql",
auth = Auth.AUTH_API_ADMIN)
public class WipeOutCloudSqlAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final ImmutableSet<RegistryEnvironment> FORBIDDEN_ENVIRONMENTS =
ImmutableSet.of(RegistryEnvironment.PRODUCTION, RegistryEnvironment.SANDBOX);
private final Supplier<Connection> connectionSupplier;
private final Response response;
private final Retrier retrier;
@Inject
WipeOutCloudSqlAction(
@SchemaManagerConnection Supplier<Connection> connectionSupplier,
Response response,
Retrier retrier) {
this.connectionSupplier = connectionSupplier;
this.response = response;
this.retrier = retrier;
}
@Override
public void run() {
response.setContentType(PLAIN_TEXT_UTF_8);
if (FORBIDDEN_ENVIRONMENTS.contains(RegistryEnvironment.get())) {
response.setStatus(SC_FORBIDDEN);
response.setPayload("Wipeout is not allowed in " + RegistryEnvironment.get());
return;
}
try {
retrier.callWithRetry(
() -> {
try (Connection conn = connectionSupplier.get()) {
dropAllTables(conn, listTables(conn));
dropAllSequences(conn, listSequences(conn));
}
return null;
},
e -> !(e instanceof SQLException));
response.setStatus(SC_OK);
response.setPayload("Wiped out Cloud SQL in " + RegistryEnvironment.get());
} catch (RuntimeException e) {
logger.atSevere().withCause(e).log("Failed to wipe out Cloud SQL data.");
response.setStatus(SC_INTERNAL_SERVER_ERROR);
response.setPayload("Failed to wipe out Cloud SQL in " + RegistryEnvironment.get());
}
}
/** Returns a list of all tables in the public schema of a Postgresql database. */
static ImmutableList<String> listTables(Connection connection) throws SQLException {
try (ResultSet resultSet =
connection.getMetaData().getTables(null, null, null, new String[] {"TABLE"})) {
ImmutableList.Builder<String> tables = new ImmutableList.Builder<>();
while (resultSet.next()) {
String schema = resultSet.getString("TABLE_SCHEM");
if (schema == null || !schema.equalsIgnoreCase("public")) {
continue;
}
String tableName = resultSet.getString("TABLE_NAME");
tables.add("public.\"" + tableName + "\"");
}
return tables.build();
}
}
static void dropAllTables(Connection conn, ImmutableList<String> tables) throws SQLException {
if (tables.isEmpty()) {
return;
}
try (Statement statement = conn.createStatement()) {
for (String table : tables) {
statement.addBatch(String.format("DROP TABLE IF EXISTS %s CASCADE;", table));
}
for (int code : statement.executeBatch()) {
if (code == Statement.EXECUTE_FAILED) {
throw new RuntimeException("Failed to drop some tables. Please check.");
}
}
}
}
/** Returns a list of all sequences in a Postgresql database. */
static ImmutableList<String> listSequences(Connection conn) throws SQLException {
try (Statement statement = conn.createStatement();
ResultSet resultSet =
statement.executeQuery("SELECT c.relname FROM pg_class c WHERE c.relkind = 'S';")) {
ImmutableList.Builder<String> sequences = new ImmutableList.Builder<>();
while (resultSet.next()) {
sequences.add('\"' + resultSet.getString(1) + '\"');
}
return sequences.build();
}
}
static void dropAllSequences(Connection conn, ImmutableList<String> sequences)
throws SQLException {
if (sequences.isEmpty()) {
return;
}
try (Statement statement = conn.createStatement()) {
for (String sequence : sequences) {
statement.addBatch(String.format("DROP SEQUENCE IF EXISTS %s CASCADE;", sequence));
}
for (int code : statement.executeBatch()) {
if (code == Statement.EXECUTE_FAILED) {
throw new RuntimeException("Failed to drop some sequences. Please check.");
}
}
}
}
}
@@ -209,7 +209,7 @@ public final class RegistryJpaIO {
@ProcessElement
public void processElement(OutputReceiver<T> outputReceiver) {
tm().transactNoRetry(
tm().transact(
() -> {
query.stream().map(resultMapper::apply).forEach(outputReceiver::output);
});
@@ -28,6 +28,7 @@ import com.google.common.base.Ascii;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import dagger.Module;
@@ -1174,44 +1175,6 @@ public final class RegistryConfig {
return CONFIG_SETTINGS.get();
}
/**
* Provides the OAuth scopes that authentication logic should detect on access tokens.
*
* <p>This list should be a superset of the required OAuth scope set provided below. Note that
* ideally, this setting would not be required and all scopes on an access token would be
* detected automatically, but that is not the case due to the way {@code OAuthService} works.
*
* <p>This is an independent setting from the required OAuth scopes (below) to support use cases
* where certain actions require some additional scope (e.g. access to a user's Google Drive)
* but that scope shouldn't be required for authentication alone; in that case the Drive scope
* would be specified only for this setting, allowing that action to check for its presence.
*/
@Provides
@Config("availableOauthScopes")
public static ImmutableSet<String> provideAvailableOauthScopes(RegistryConfigSettings config) {
return ImmutableSet.copyOf(config.auth.availableOauthScopes);
}
/**
* Provides the OAuth scopes that are required for authenticating successfully.
*
* <p>This set contains the scopes which must be present to authenticate a user. It should be a
* subset of the scopes we request from the OAuth interface, provided above.
*
* <p>If we feel the need, we could define additional fixed scopes, similar to the Java remote
* API, which requires at least one of:
*
* <ul>
* <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.auth.requiredOauthScopes);
}
/**
* Provides service account email addresses allowed to authenticate with the app at {@link
* google.registry.request.auth.AuthSettings.AuthLevel#APP} level.
@@ -1223,19 +1186,18 @@ public final class RegistryConfig {
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.auth.allowedOauthClientIds);
}
@Provides
@Config("oauthClientId")
public static String provideOauthClientId(RegistryConfigSettings config) {
return config.auth.oauthClientId;
}
@Provides
@Config("fallbackOauthClientId")
public static String provideFallbackOauthClientId(RegistryConfigSettings config) {
return config.auth.fallbackOauthClientId;
}
/**
* Provides the OAuth scopes required for accessing Google APIs using the default credential.
*/
@@ -1435,6 +1397,24 @@ public final class RegistryConfig {
return config.bulkPricingPackageMonitoring.bulkPricingPackageDomainLimitUpgradeEmailBody;
}
@Provides
@Config("bsaAuthUrl")
public static String provideBsaAuthUrl(RegistryConfigSettings config) {
return config.bsa.authUrl;
}
@Provides
@Config("bsaAuthTokenExpiry")
public static Duration provideBsaAuthTokenExpiry(RegistryConfigSettings config) {
return Duration.standardSeconds(config.bsa.authTokenExpirySeconds);
}
@Provides
@Config("bsaDataUrls")
public static ImmutableMap<String, String> provideBsaDataUrls(RegistryConfigSettings config) {
return ImmutableMap.copyOf(config.bsa.dataUrls);
}
private static String formatComments(String text) {
return Splitter.on('\n').omitEmptyStrings().trimResults().splitToList(text).stream()
.map(s -> "# " + s)
@@ -1574,9 +1554,9 @@ public final class RegistryConfig {
return CONFIG_SETTINGS.get().hibernate.connectionIsolation;
}
/** Returns true if per-transaction isolation level is enabled. */
public static boolean getHibernatePerTransactionIsolationEnabled() {
return CONFIG_SETTINGS.get().hibernate.perTransactionIsolation;
/** Returns true if nested calls to {@code tm().transact()} are allowed. */
public static boolean getHibernateAllowNestedTransactions() {
return CONFIG_SETTINGS.get().hibernate.allowNestedTransactions;
}
/** Returns true if hibernate.show_sql is enabled. */
@@ -43,6 +43,7 @@ public class RegistryConfigSettings {
public ContactHistory contactHistory;
public DnsUpdate dnsUpdate;
public BulkPricingPackageMonitoring bulkPricingPackageMonitoring;
public Bsa bsa;
/** Configuration options that apply to the entire GCP project. */
public static class GcpProject {
@@ -58,11 +59,9 @@ public class RegistryConfigSettings {
/** Configuration options for authenticating users. */
public static class Auth {
public List<String> availableOauthScopes;
public List<String> requiredOauthScopes;
public List<String> allowedOauthClientIds;
public List<String> allowedServiceAccountEmails;
public String oauthClientId;
public String fallbackOauthClientId;
}
/** Configuration options for accessing Google APIs. */
@@ -115,7 +114,7 @@ public class RegistryConfigSettings {
/** Configuration for Hibernate. */
public static class Hibernate {
public boolean perTransactionIsolation;
public boolean allowNestedTransactions;
public String connectionIsolation;
public String logSqlQueries;
public String hikariConnectionTimeout;
@@ -264,4 +263,11 @@ public class RegistryConfigSettings {
public String bulkPricingPackageDomainLimitUpgradeEmailSubject;
public String bulkPricingPackageDomainLimitUpgradeEmailBody;
}
/** Configurations for integration with Brand Safety Alliance (BSA) API. */
public static class Bsa {
public String authUrl;
public int authTokenExpirySeconds;
public Map<String, String> dataUrls;
}
}
@@ -189,11 +189,13 @@ registryPolicy:
sunriseDomainCreateDiscount: 0.15
hibernate:
# Make it possible to specify the isolation level for each transaction. If set
# to true, nested transactions will throw an exception. If set to false, a
# transaction with the isolation override specified will still execute at the
# default level (specified below).
perTransactionIsolation: false
# If set to false, calls to tm().transact() cannot be nested. If set to true,
# nested calls to tm().transact() are allowed, as long as they do not specify
# a transaction isolation level override. These nested transactions should
# either be refactored to non-nested transactions, or changed to
# tm().reTransact(), which explicitly allows nested transactions, but does not
# allow setting an isolation level override.
allowNestedTransactions: true
# Make 'SERIALIZABLE' the default isolation level to ensure correctness.
#
@@ -304,24 +306,6 @@ caching:
# 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 value in
# registryTool.clientId field should be included in this list. Client IDs are
# typically of the format
# numbers-alphanumerics.apps.googleusercontent.com
allowedOauthClientIds: []
# Service accounts (e.g. default service account, account used by Cloud
# Scheduler) allowed to send authenticated requests.
allowedServiceAccountEmails:
@@ -337,6 +321,10 @@ auth:
# the same as this one.
oauthClientId: iap-oauth-clientid
# Same as above, but serve as a fallback, so we can switch the client ID of
# the proxy without downtime.
fallbackOauthClientId: fallback-oauth-clientid
credentialOAuth:
# OAuth scopes required for accessing Google APIs using the default
# credential.
@@ -616,3 +604,14 @@ bulkPricingPackageMonitoring:
Registrar: %3$s
Active Domain Limit: %4$s
Current Active Domains: %5$s
# Configurations for integration with Brand Safety Alliance (BSA) API
bsa:
# Http endpoint for acquiring Auth tokens.
authUrl: "https://"
# Auth token expiry.
authTokenExpirySeconds: 1800
# Http endpoints for downloading data
dataUrls:
"BLOCK": "https://"
"BLOCK_PLUS": "https://"
@@ -1,76 +0,0 @@
# This is a sample production config (to be deployed in the WEB-INF directory).
# This is the same as what Google Registry runs in production, except with
# placeholders for Google-specific settings.
gcpProject:
projectId: placeholder
# Set to true if running against local servers (localhost)
isLocal: false
# The "<service>-dot-" prefix is used on the project ID in this URL in order
# to get around an issue with double-wildcard SSL certs.
defaultServiceUrl: https://domain-registry-placeholder.appspot.com
backendServiceUrl: https://backend-dot-domain-registry-placeholder.appspot.com
toolsServiceUrl: https://tools-dot-domain-registry-placeholder.appspot.com
pubapiServiceUrl: https://pubapi-dot-domain-registry-placeholder.appspot.com
gSuite:
domainName: placeholder
outgoingEmailDisplayName: placeholder
outgoingEmailAddress: placeholder
adminAccountEmailAddress: placeholder
supportGroupEmailAddress: placeholder
registryPolicy:
contactAndHostRoidSuffix: placeholder
productName: placeholder
greetingServerId: placeholder
registrarChangesNotificationEmailAddresses:
- placeholder
- placeholder
defaultRegistrarWhoisServer: placeholder
tmchCaMode: PRODUCTION
tmchCrlUrl: http://crl.icann.org/tmch.crl
tmchMarksDbUrl: https://ry.marksdb.org
checkApiServletClientId: placeholder
registryAdminClientId: placeholder
whoisDisclaimer: |
multi-line
placeholder
icannReporting:
icannTransactionsReportingUploadUrl: https://ry-api.icann.org/report/registrar-transactions
icannActivityReportingUploadUrl: https://ry-api.icann.org/report/registry-functions-activity
oAuth:
allowedOauthClientIds:
- placeholder.apps.googleusercontent.com
- placeholder-for-proxy
rde:
reportUrlPrefix: https://ry-api.icann.org/report/registry-escrow-report
uploadUrl: sftp://placeholder@sftpipm2.ironmountain.com/Outbox
sshIdentityEmailAddress: placeholder
registrarConsole:
logoFilename: placeholder
supportPhoneNumber: placeholder
supportEmailAddress: placeholder
announcementsEmailAddress: placeholder
integrationEmailAddress: placeholder
technicalDocsUrl: https://drive.google.com/drive/folders/placeholder
misc:
sheetExportId: placeholder
cloudDns:
rootUrl: null
servicePath: null
keyring:
activeKeyring: KMS
kms:
projectId: placeholder
registryTool:
clientId: placeholder.apps.googleusercontent.com
clientSecret: placeholder
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>backend</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>
@@ -18,6 +18,13 @@
value="alpha"/>
</system-properties>
<!-- Enable external traffic to go through VPC, required for static ip -->
<vpc-access-connector>
<name>projects/domain-registry-alpha/locations/us-central1/connectors/appengine-connector</name>
<egress-setting>all-traffic</egress-setting>
</vpc-access-connector>
<static-files>
<include path="/*.html" expiration="1m"/>
</static-files>
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>default</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>pubapi</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>tools</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>backend</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>
@@ -18,6 +18,12 @@
value="crash"/>
</system-properties>
<!-- Enable external traffic to go through VPC, required for static ip -->
<vpc-access-connector>
<name>projects/domain-registry-crash/locations/us-central1/connectors/appengine-connector</name>
<egress-setting>all-traffic</egress-setting>
</vpc-access-connector>
<static-files>
<include path="/*.html" expiration="1m"/>
</static-files>
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>default</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4_1G</instance-class>
<basic-scaling>
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>pubapi</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>tools</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>backend</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>default</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4_1G</instance-class>
<basic-scaling>
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>pubapi</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>tools</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>backend</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4_1G</instance-class>
<basic-scaling>
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>default</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4_1G</instance-class>
<manual-scaling>
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>pubapi</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4_1G</instance-class>
<manual-scaling>
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>tools</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4_1G</instance-class>
<basic-scaling>
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>backend</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>
@@ -22,6 +22,12 @@
<include path="/*.html" expiration="1h"/>
</static-files>
<!-- Enable external traffic to go through VPC, required for static ip -->
<vpc-access-connector>
<name>projects/domain-registry-qa/locations/us-central1/connectors/appengine-connector</name>
<egress-setting>all-traffic</egress-setting>
</vpc-access-connector>
<!-- Prevent uncaught servlet errors from leaking a stack trace. -->
<static-error-handlers>
<handler file="error.html"/>
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>default</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>F4_1G</instance-class>
<automatic-scaling>
@@ -59,13 +59,4 @@
</description>
<schedule>7 3 * * *</schedule>
</task>
<task>
<url><![CDATA[/_dr/task/wipeOutCloudSql]]></url>
<name>wipeOutCloudSql</name>
<description>
This job runs an action that deletes all data in Cloud SQL.
</description>
<schedule>7 3 * * 6</schedule>
</task>
</entries>
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>pubapi</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>tools</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>backend</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>
@@ -22,6 +22,12 @@
<include path="/*.html" expiration="1d"/>
</static-files>
<!-- Enable external traffic to go through VPC, required for static ip -->
<vpc-access-connector>
<name>projects/domain-registry-sandbox/locations/us-central1/connectors/appengine-connector</name>
<egress-setting>all-traffic</egress-setting>
</vpc-access-connector>
<!-- Prevent uncaught servlet errors from leaking a stack trace. -->
<static-error-handlers>
<handler file="error.html"/>
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>default</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4_1G</instance-class>
<manual-scaling>
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>pubapi</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4_1G</instance-class>
<manual-scaling>
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java8</runtime>
<runtime>java17</runtime>
<service>tools</service>
<threadsafe>true</threadsafe>
<app-engine-apis>true</app-engine-apis>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>
@@ -29,7 +29,7 @@ import javax.servlet.http.HttpSession;
service = Action.Service.DEFAULT,
path = "/_dr/epp",
method = Method.POST,
auth = Auth.AUTH_API_PUBLIC)
auth = Auth.AUTH_API_ADMIN)
public class EppTlsAction implements Runnable {
@Inject @Payload byte[] inputXmlBytes;
@@ -66,7 +66,6 @@ import google.registry.model.domain.DomainCommand.Check;
import google.registry.model.domain.fee.FeeCheckCommandExtension;
import google.registry.model.domain.fee.FeeCheckCommandExtensionItem;
import google.registry.model.domain.fee.FeeCheckResponseExtensionItem;
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName;
import google.registry.model.domain.fee06.FeeCheckCommandExtensionV06;
import google.registry.model.domain.launch.LaunchCheckExtension;
import google.registry.model.domain.token.AllocationToken;
@@ -272,7 +271,7 @@ public final class DomainCheckFlow implements TransactionalFlow {
ImmutableList.Builder<FeeCheckResponseExtensionItem> responseItems =
new ImmutableList.Builder<>();
ImmutableMap<String, Domain> domainObjs =
loadDomainsForRestoreChecks(feeCheck, domainNames, existingDomains);
loadDomainsForChecks(feeCheck, domainNames, existingDomains);
ImmutableMap<String, BillingRecurrence> recurrences = loadRecurrencesForDomains(domainObjs);
for (FeeCheckCommandExtensionItem feeCheckItem : feeCheck.getItems()) {
@@ -335,17 +334,20 @@ public final class DomainCheckFlow implements TransactionalFlow {
}
/**
* Loads and returns all existing domains that are having restore fees checked.
* Loads and returns all existing domains that are having restore/renew/transfer fees checked.
*
* <p>This is necessary so that we can check their expiration dates to determine if a one-year
* renewal is part of the cost of a restore.
* <p>These need to be loaded for renews and transfers because there could be a relevant {@link
* google.registry.model.billing.BillingBase.RenewalPriceBehavior} on the {@link
* BillingRecurrence} affecting the price. They also need to be loaded for restores so that we can
* check their expiration dates to determine if a one-year renewal is part of the cost of a
* restore.
*
* <p>This may be resource-intensive for large checks of many restore fees, but those are
* comparatively rare, and we are at least using an in-memory cache. Also, this will get a lot
* nicer in Cloud SQL when we can SELECT just the fields we want rather than having to load the
* entire entity.
*/
private ImmutableMap<String, Domain> loadDomainsForRestoreChecks(
private ImmutableMap<String, Domain> loadDomainsForChecks(
FeeCheckCommandExtension<?, ?> feeCheck,
ImmutableMap<String, InternetDomainName> domainNames,
ImmutableMap<String, VKey<Domain>> existingDomains) {
@@ -354,18 +356,18 @@ public final class DomainCheckFlow implements TransactionalFlow {
// The V06 fee extension supports specifying the command fees to check on a per-domain basis.
restoreCheckDomains =
feeCheck.getItems().stream()
.filter(fc -> fc.getCommandName() == CommandName.RESTORE)
.filter(fc -> fc.getCommandName().shouldLoadDomainForCheck())
.map(FeeCheckCommandExtensionItem::getDomainName)
.distinct()
.collect(toImmutableList());
} else if (feeCheck.getItems().stream()
.anyMatch(fc -> fc.getCommandName() == CommandName.RESTORE)) {
.anyMatch(fc -> fc.getCommandName().shouldLoadDomainForCheck())) {
// The more recent fee extension versions support specifying the command fees to check only on
// the overall domain check, not per-domain.
restoreCheckDomains = ImmutableList.copyOf(domainNames.keySet());
} else {
// Fall-through case for more recent fee extension versions when the restore fee isn't being
// checked.
// Fall-through case for more recent fee extension versions when the restore/renew/transfer
// fees aren't being checked.
restoreCheckDomains = ImmutableList.of();
}
@@ -28,11 +28,17 @@ import google.registry.flows.EppException.CommandUseErrorException;
import google.registry.flows.EppException.ParameterValuePolicyErrorException;
import google.registry.flows.EppException.UnimplementedExtensionException;
import google.registry.flows.EppException.UnimplementedObjectServiceException;
import google.registry.flows.EppException.UnimplementedProtocolVersionException;
import google.registry.flows.ExtensionManager;
import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.FlowUtils.GenericXmlSyntaxErrorException;
import google.registry.flows.MutatingFlow;
import google.registry.flows.SessionMetadata;
import google.registry.flows.TlsCredentials.BadRegistrarCertificateException;
import google.registry.flows.TlsCredentials.BadRegistrarIpAddressException;
import google.registry.flows.TlsCredentials.MissingRegistrarCertificateException;
import google.registry.flows.TransportCredentials;
import google.registry.flows.TransportCredentials.BadRegistrarPasswordException;
import google.registry.model.eppcommon.ProtocolDefinition;
import google.registry.model.eppcommon.ProtocolDefinition.ServiceExtension;
import google.registry.model.eppinput.EppInput;
@@ -41,6 +47,7 @@ import google.registry.model.eppinput.EppInput.Options;
import google.registry.model.eppinput.EppInput.Services;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.registrar.Registrar;
import google.registry.util.PasswordUtils.HashAlgorithm;
import java.util.Optional;
import java.util.Set;
import javax.inject.Inject;
@@ -48,14 +55,14 @@ import javax.inject.Inject;
/**
* An EPP flow for login.
*
* @error {@link google.registry.flows.EppException.UnimplementedExtensionException}
* @error {@link google.registry.flows.EppException.UnimplementedObjectServiceException}
* @error {@link google.registry.flows.EppException.UnimplementedProtocolVersionException}
* @error {@link google.registry.flows.FlowUtils.GenericXmlSyntaxErrorException}
* @error {@link google.registry.flows.TlsCredentials.BadRegistrarCertificateException}
* @error {@link google.registry.flows.TlsCredentials.BadRegistrarIpAddressException}
* @error {@link google.registry.flows.TlsCredentials.MissingRegistrarCertificateException}
* @error {@link google.registry.flows.TransportCredentials.BadRegistrarPasswordException}
* @error {@link UnimplementedExtensionException}
* @error {@link UnimplementedObjectServiceException}
* @error {@link UnimplementedProtocolVersionException}
* @error {@link GenericXmlSyntaxErrorException}
* @error {@link BadRegistrarCertificateException}
* @error {@link BadRegistrarIpAddressException}
* @error {@link MissingRegistrarCertificateException}
* @error {@link BadRegistrarPasswordException}
* @error {@link LoginFlow.AlreadyLoggedInException}
* @error {@link BadRegistrarIdException}
* @error {@link LoginFlow.TooManyFailedLoginsException}
@@ -134,13 +141,24 @@ public class LoginFlow implements MutatingFlow {
if (!registrar.get().isLive()) {
throw new RegistrarAccountNotActiveException();
}
if (login.getNewPassword().isPresent()) {
if (login.getNewPassword().isPresent()
|| registrar.get().getCurrentHashAlgorithm(login.getPassword()).orElse(null)
!= HashAlgorithm.SCRYPT) {
String newPassword =
login
.getNewPassword()
.orElseGet(
() -> {
logger.atInfo().log("Rehashing existing registrar password with Scrypt");
return login.getPassword();
});
// Load fresh from database (bypassing the cache) to ensure we don't save stale data.
Optional<Registrar> freshRegistrar = Registrar.loadByRegistrarId(login.getClientId());
if (!freshRegistrar.isPresent()) {
throw new BadRegistrarIdException(login.getClientId());
}
tm().put(freshRegistrar.get().asBuilder().setPassword(login.getNewPassword().get()).build());
tm().put(freshRegistrar.get().asBuilder().setPassword(newPassword).build());
}
// We are in!
@@ -152,35 +170,35 @@ public class LoginFlow implements MutatingFlow {
/** Registrar with this ID could not be found. */
static class BadRegistrarIdException extends AuthenticationErrorException {
public BadRegistrarIdException(String registrarId) {
BadRegistrarIdException(String registrarId) {
super("Registrar with this ID could not be found: " + registrarId);
}
}
/** Registrar login failed too many times. */
static class TooManyFailedLoginsException extends AuthenticationErrorClosingConnectionException {
public TooManyFailedLoginsException() {
TooManyFailedLoginsException() {
super("Registrar login failed too many times");
}
}
/** Registrar account is not active. */
static class RegistrarAccountNotActiveException extends AuthorizationErrorException {
public RegistrarAccountNotActiveException() {
RegistrarAccountNotActiveException() {
super("Registrar account is not active");
}
}
/** Registrar is already logged in. */
static class AlreadyLoggedInException extends CommandUseErrorException {
public AlreadyLoggedInException() {
AlreadyLoggedInException() {
super("Registrar is already logged in");
}
}
/** Specified language is not supported. */
static class UnsupportedLanguageException extends ParameterValuePolicyErrorException {
public UnsupportedLanguageException() {
UnsupportedLanguageException() {
super("Specified language is not supported");
}
}
@@ -38,7 +38,7 @@ public final class InMemoryKeyring implements Keyring {
private final String marksdbDnlLoginAndPassword;
private final String marksdbLordnPassword;
private final String marksdbSmdrlLoginAndPassword;
private final String jsonCredential;
private final String bsaApiKey;
public InMemoryKeyring(
PGPKeyPair rdeStagingKey,
@@ -53,9 +53,9 @@ public final class InMemoryKeyring implements Keyring {
String marksdbDnlLoginAndPassword,
String marksdbLordnPassword,
String marksdbSmdrlLoginAndPassword,
String jsonCredential,
String cloudSqlPassword,
String toolsCloudSqlPassword) {
String toolsCloudSqlPassword,
String bsaApiKey) {
checkArgument(PgpHelper.isSigningKey(rdeSigningKey.getPublicKey()),
"RDE signing key must support signing: %s", rdeSigningKey.getKeyID());
checkArgument(rdeStagingKey.getPublicKey().isEncryptionKey(),
@@ -80,7 +80,7 @@ public final class InMemoryKeyring implements Keyring {
this.marksdbLordnPassword = checkNotNull(marksdbLordnPassword, "marksdbLordnPassword");
this.marksdbSmdrlLoginAndPassword =
checkNotNull(marksdbSmdrlLoginAndPassword, "marksdbSmdrlLoginAndPassword");
this.jsonCredential = checkNotNull(jsonCredential, "jsonCredential");
this.bsaApiKey = checkNotNull(bsaApiKey, "bsaApiKey");
}
@Override
@@ -149,8 +149,8 @@ public final class InMemoryKeyring implements Keyring {
}
@Override
public String getJsonCredential() {
return jsonCredential;
public String getBsaApiKey() {
return bsaApiKey;
}
/** Does nothing. */
@@ -145,11 +145,8 @@ public interface Keyring extends AutoCloseable {
*/
String getMarksdbSmdrlLoginAndPassword();
/**
* Returns the credentials for a service account on the Google AppEngine project downloaded from
* the Cloud Console dashboard in JSON format.
*/
String getJsonCredential();
/** Returns the API_KEY for authentication with the BSA portal. */
String getBsaApiKey();
// Don't throw so try-with-resources works better.
@Override
@@ -58,8 +58,8 @@ public class SecretManagerKeyring implements Keyring {
/** Key labels for string secrets. */
enum StringKeyLabel {
SAFE_BROWSING_API_KEY,
BSA_API_KEY_STRING,
ICANN_REPORTING_PASSWORD_STRING,
JSON_CREDENTIAL_STRING,
MARKSDB_DNL_LOGIN_STRING,
MARKSDB_LORDN_PASSWORD_STRING,
MARKSDB_SMDRL_LOGIN_STRING,
@@ -143,10 +143,9 @@ public class SecretManagerKeyring implements Keyring {
return getString(StringKeyLabel.MARKSDB_SMDRL_LOGIN_STRING);
}
// TODO(b/237305940): remove this method and all supports, including entry in secretmanager
@Override
public String getJsonCredential() {
return getString(StringKeyLabel.JSON_CREDENTIAL_STRING);
public String getBsaApiKey() {
return getString(StringKeyLabel.BSA_API_KEY_STRING);
}
/** No persistent resources are maintained for this Keyring implementation. */
@@ -24,8 +24,8 @@ import static google.registry.keyring.secretmanager.SecretManagerKeyring.PublicK
import static google.registry.keyring.secretmanager.SecretManagerKeyring.PublicKeyLabel.RDE_RECEIVER_PUBLIC;
import static google.registry.keyring.secretmanager.SecretManagerKeyring.PublicKeyLabel.RDE_SIGNING_PUBLIC;
import static google.registry.keyring.secretmanager.SecretManagerKeyring.PublicKeyLabel.RDE_STAGING_PUBLIC;
import static google.registry.keyring.secretmanager.SecretManagerKeyring.StringKeyLabel.BSA_API_KEY_STRING;
import static google.registry.keyring.secretmanager.SecretManagerKeyring.StringKeyLabel.ICANN_REPORTING_PASSWORD_STRING;
import static google.registry.keyring.secretmanager.SecretManagerKeyring.StringKeyLabel.JSON_CREDENTIAL_STRING;
import static google.registry.keyring.secretmanager.SecretManagerKeyring.StringKeyLabel.MARKSDB_DNL_LOGIN_STRING;
import static google.registry.keyring.secretmanager.SecretManagerKeyring.StringKeyLabel.MARKSDB_LORDN_PASSWORD_STRING;
import static google.registry.keyring.secretmanager.SecretManagerKeyring.StringKeyLabel.MARKSDB_SMDRL_LOGIN_STRING;
@@ -120,8 +120,8 @@ public final class SecretManagerKeyringUpdater {
return setString(login, MARKSDB_SMDRL_LOGIN_STRING);
}
public SecretManagerKeyringUpdater setJsonCredential(String credential) {
return setString(credential, JSON_CREDENTIAL_STRING);
public SecretManagerKeyringUpdater setBsaApiKey(String credential) {
return setString(credential, BSA_API_KEY_STRING);
}
/**
@@ -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.
package google.registry.model.adapters;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
/**
* Adapter factory that allows for (de)serialization of Class objects in GSON.
*
* <p>GSON's built-in adapter for Class objects throws an exception, but there are situations where
* we want to (de)serialize these, such as in VKeys. This instructs GSON to look for our custom
* {@link ClassTypeAdapter} rather than the default.
*/
public class ClassProcessingTypeAdapterFactory implements TypeAdapterFactory {
@Override
@SuppressWarnings("unchecked")
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
if (Class.class.isAssignableFrom(typeToken.getRawType())) {
// in this case, T is a class object
return (TypeAdapter<T>) new ClassTypeAdapter();
}
return null;
}
}
@@ -0,0 +1,48 @@
// 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.model.adapters;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
/**
* TypeAdapter for {@link Class} objects.
*
* <p>GSON's default adapter doesn't allow this, but we want to allow for (de)serialization of Class
* objects for containers like VKeys using the full name of the class.
*/
public class ClassTypeAdapter extends TypeAdapter<Class<?>> {
@Override
public void write(JsonWriter out, Class value) throws IOException {
out.value(value.getName());
}
@Override
public Class<?> read(JsonReader reader) throws IOException {
String stringValue = reader.nextString();
if (stringValue.equals("null")) {
return null;
}
try {
return Class.forName(stringValue);
} catch (ClassNotFoundException e) {
// this should not happen...
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,38 @@
// 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.model.adapters;
import google.registry.util.StringBaseTypeAdapter;
import java.io.IOException;
import java.io.Serializable;
/**
* TypeAdapter for {@link Serializable} objects.
*
* <p>VKey keys (primary keys in SQL) are usually represented by either a long or a String. There
* are a couple situations (CursorId, HistoryEntryId) where the Serializable in question is a
* complex object, but we do not need to worry about (de)serializing those objects to/from JSON.
*/
public class SerializableJsonTypeAdapter extends StringBaseTypeAdapter<Serializable> {
@Override
protected Serializable fromString(String stringValue) throws IOException {
try {
return Long.parseLong(stringValue);
} catch (NumberFormatException e) {
return stringValue;
}
}
}
@@ -25,6 +25,7 @@ import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import google.registry.model.Buildable;
import google.registry.model.UpdateAutoTimestampEntity;
import google.registry.persistence.VKey;
import google.registry.util.PasswordUtils;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
@@ -84,8 +85,9 @@ public class User extends UpdateAutoTimestampEntity implements Buildable {
|| isNullOrEmpty(registryLockPasswordHash)) {
return false;
}
return hashPassword(registryLockPassword, registryLockPasswordSalt)
.equals(registryLockPasswordHash);
return PasswordUtils.verifyPassword(
registryLockPassword, registryLockPasswordHash, registryLockPasswordSalt)
.isPresent();
}
/**
@@ -154,9 +156,9 @@ public class User extends UpdateAutoTimestampEntity implements Buildable {
!getInstance().hasRegistryLockPassword(), "User already has a password, remove it first");
checkArgument(
!isNullOrEmpty(registryLockPassword), "Registry lock password was null or empty");
getInstance().registryLockPasswordSalt = base64().encode(SALT_SUPPLIER.get());
getInstance().registryLockPasswordHash =
hashPassword(registryLockPassword, getInstance().registryLockPasswordSalt);
byte[] salt = SALT_SUPPLIER.get();
getInstance().registryLockPasswordSalt = base64().encode(salt);
getInstance().registryLockPasswordHash = hashPassword(registryLockPassword, salt);
return this;
}
}
@@ -34,12 +34,14 @@ public abstract class FeeQueryCommandExtensionItem extends ImmutableObject {
/** The name of a command that might have an associated fee. */
public enum CommandName {
UNKNOWN,
CREATE,
RENEW,
TRANSFER,
RESTORE,
UPDATE;
UNKNOWN(false),
CREATE(false),
RENEW(true),
TRANSFER(true),
RESTORE(true),
UPDATE(false);
private final boolean loadDomainForCheck;
public static CommandName parseKnownCommand(String string) {
try {
@@ -52,6 +54,14 @@ public abstract class FeeQueryCommandExtensionItem extends ImmutableObject {
+ " UPDATE");
}
}
CommandName(boolean loadDomainForCheck) {
this.loadDomainForCheck = loadDomainForCheck;
}
public boolean shouldLoadDomainForCheck() {
return this.loadDomainForCheck;
}
}
/** The default validity period (if not specified) is 1 year for all operations. */
@@ -305,14 +305,14 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda
new CacheLoader<VKey<AllocationToken>, Optional<AllocationToken>>() {
@Override
public Optional<AllocationToken> load(VKey<AllocationToken> key) {
return tm().transact(() -> tm().loadByKeyIfPresent(key));
return tm().reTransact(() -> tm().loadByKeyIfPresent(key));
}
@Override
public Map<VKey<AllocationToken>, Optional<AllocationToken>> loadAll(
Iterable<? extends VKey<AllocationToken>> keys) {
ImmutableSet<VKey<AllocationToken>> keySet = ImmutableSet.copyOf(keys);
return tm().transact(
return tm().reTransact(
() ->
keySet.stream()
.collect(
@@ -60,6 +60,8 @@ import google.registry.model.tld.Tld;
import google.registry.model.tld.Tld.TldType;
import google.registry.persistence.VKey;
import google.registry.util.CidrAddressBlock;
import google.registry.util.PasswordUtils;
import google.registry.util.PasswordUtils.HashAlgorithm;
import java.security.cert.CertificateParsingException;
import java.util.Comparator;
import java.util.List;
@@ -97,7 +99,7 @@ import org.joda.time.DateTime;
column = @Column(nullable = false, name = "lastUpdateTime"))
public class Registrar extends UpdateAutoTimestampEntity implements Buildable, Jsonifiable {
/** Represents the type of a registrar entity. */
/** Represents the type of registrar entity. */
public enum Type {
/** A real-world, third-party registrar. Should have non-null IANA and billing account IDs. */
REAL(Objects::nonNull),
@@ -200,7 +202,11 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
/** A caching {@link Supplier} of a registrarId to {@link Registrar} map. */
private static final Supplier<ImmutableMap<String, Registrar>> CACHE_BY_REGISTRAR_ID =
memoizeWithShortExpiration(() -> Maps.uniqueIndex(loadAll(), Registrar::getRegistrarId));
memoizeWithShortExpiration(
() ->
Maps.uniqueIndex(
tm().reTransact(() -> tm().loadAllOf(Registrar.class)),
Registrar::getRegistrarId));
/**
* Unique registrar client id. Must conform to "clIDType" as defined in RFC5730.
@@ -372,7 +378,7 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
*/
@Expose String icannReferralEmail;
/** Id of the folder in drive used to publish information for this registrar. */
/** ID of the folder in drive used to publish information for this registrar. */
@Expose String driveFolderId;
// Metadata.
@@ -635,7 +641,11 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
}
public boolean verifyPassword(String password) {
return hashPassword(password, salt).equals(passwordHash);
return getCurrentHashAlgorithm(password).isPresent();
}
public Optional<HashAlgorithm> getCurrentHashAlgorithm(String password) {
return PasswordUtils.verifyPassword(password, passwordHash, salt);
}
public String getPhonePasscode() {
@@ -857,8 +867,9 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
checkArgument(
Range.closed(6, 16).contains(nullToEmpty(password).length()),
"Password must be 6-16 characters long.");
getInstance().salt = base64().encode(SALT_SUPPLIER.get());
getInstance().passwordHash = hashPassword(password, getInstance().salt);
byte[] salt = SALT_SUPPLIER.get();
getInstance().salt = base64().encode(salt);
getInstance().passwordHash = hashPassword(password, salt);
return this;
}
@@ -37,6 +37,8 @@ import google.registry.model.Jsonifiable;
import google.registry.model.UnsafeSerializable;
import google.registry.model.registrar.RegistrarPoc.RegistrarPocId;
import google.registry.persistence.VKey;
import google.registry.util.PasswordUtils;
import google.registry.util.PasswordUtils.HashAlgorithm;
import java.io.Serializable;
import java.util.Map;
import java.util.Optional;
@@ -240,8 +242,12 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
|| isNullOrEmpty(registryLockPasswordHash)) {
return false;
}
return hashPassword(registryLockPassword, registryLockPasswordSalt)
.equals(registryLockPasswordHash);
return getCurrentHashAlgorithm(registryLockPassword).isPresent();
}
public Optional<HashAlgorithm> getCurrentHashAlgorithm(String registryLockPassword) {
return PasswordUtils.verifyPassword(
registryLockPassword, registryLockPasswordHash, registryLockPasswordSalt);
}
/**
@@ -436,9 +442,9 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
"Not allowed to set registry lock password for this contact");
checkArgument(
!isNullOrEmpty(registryLockPassword), "Registry lock password was null or empty");
getInstance().registryLockPasswordSalt = base64().encode(SALT_SUPPLIER.get());
getInstance().registryLockPasswordHash =
hashPassword(registryLockPassword, getInstance().registryLockPasswordSalt);
byte[] salt = SALT_SUPPLIER.get();
getInstance().registryLockPasswordSalt = base64().encode(salt);
getInstance().registryLockPasswordHash = hashPassword(registryLockPassword, salt);
getInstance().allowedToSetRegistryLockPassword = false;
return this;
}
@@ -25,9 +25,10 @@ import com.google.common.base.Strings;
import com.google.common.flogger.FluentLogger;
import google.registry.model.ImmutableObject;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.TransactionManager.ThrowingRunnable;
import java.io.Serializable;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.concurrent.Callable;
import javax.annotation.Nullable;
import javax.persistence.Column;
import javax.persistence.Entity;
@@ -184,7 +185,7 @@ public class Lock extends ImmutableObject implements Serializable {
public static Optional<Lock> acquire(
String resourceName, @Nullable String tld, Duration leaseLength) {
String scope = tld != null ? tld : GLOBAL;
Supplier<AcquireResult> lockAcquirer =
Callable<AcquireResult> lockAcquirer =
() -> {
DateTime now = tm().getTransactionTime();
@@ -221,7 +222,7 @@ public class Lock extends ImmutableObject implements Serializable {
/** Release the lock. */
public void release() {
// Just use the default clock because we aren't actually doing anything that will use the clock.
Supplier<Void> lockReleaser =
ThrowingRunnable lockReleaser =
() -> {
// To release a lock, check that no one else has already obtained it and if not
// delete it. If the lock in the database was different, then this lock is gone already;
@@ -246,7 +247,6 @@ public class Lock extends ImmutableObject implements Serializable {
logger.atInfo().log(
"Not deleting lock: %s - someone else has it: %s", lockId, loadedLock);
}
return null;
};
tm().transact(lockReleaser);
}
@@ -28,7 +28,7 @@ public class SignedMarkRevocationListDao {
/** Loads the {@link SignedMarkRevocationList}. */
static SignedMarkRevocationList load() {
Optional<SignedMarkRevocationList> smdrl =
tm().transact(
tm().reTransact(
() -> {
Long revisionId =
tm().query("SELECT MAX(revisionId) FROM SignedMarkRevocationList", Long.class)
@@ -233,7 +233,8 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
new CacheLoader<String, Tld>() {
@Override
public Tld load(final String tld) {
return tm().transact(() -> tm().loadByKeyIfPresent(createVKey(tld))).orElse(null);
return tm().reTransact(() -> tm().loadByKeyIfPresent(createVKey(tld)))
.orElse(null);
}
@Override
@@ -241,7 +242,7 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
ImmutableMap<String, VKey<Tld>> keysMap =
toMap(ImmutableSet.copyOf(tlds), Tld::createVKey);
Map<VKey<? extends Tld>, Tld> entities =
tm().transact(() -> tm().loadByKeysIfPresent(keysMap.values()));
tm().reTransact(() -> tm().loadByKeysIfPresent(keysMap.values()));
return Maps.transformEntries(keysMap, (k, v) -> entities.getOrDefault(v, null));
}
});
@@ -549,6 +550,10 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
@JsonSerialize(using = SortedEnumSetSerializer.class)
Set<IdnTableEnum> idnTables;
// TODO(11/30/2023): uncomment below two lines
// /** The start time of this TLD's enrollment in the BSA program, if applicable. */
// @JsonIgnore @Nullable DateTime bsaEnrollStartTime;
public String getTldStr() {
return tldStr;
}
@@ -568,6 +573,15 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
return tldType;
}
/** Returns the time when this TLD was enrolled in the Brand Safety Alliance (BSA) program. */
@JsonIgnore // Annotation can be removed once we add the field and annotate it.
@Nullable
public DateTime getBsaEnrollStartTime() {
// TODO(11/30/2023): uncomment below.
// return this.bsaEnrollStartTime;
return null;
}
/** Retrieve whether invoicing is enabled. */
public boolean isInvoicingEnabled() {
return invoicingEnabled;
@@ -938,6 +952,7 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
}
public Builder setReservedListsByName(Set<String> reservedListNames) {
// TODO(b/309175133): forbid if enrolled with BSA
checkArgument(reservedListNames != null, "reservedListNames must not be null");
ImmutableSet.Builder<ReservedList> builder = new ImmutableSet.Builder<>();
for (String reservedListName : reservedListNames) {
@@ -957,6 +972,7 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
}
public Builder setReservedLists(Set<ReservedList> reservedLists) {
// TODO(b/309175133): forbid if enrolled with BSA
checkArgumentNotNull(reservedLists, "reservedLists must not be null");
ImmutableSet.Builder<String> nameBuilder = new ImmutableSet.Builder<>();
for (ReservedList reservedList : reservedLists) {
@@ -1075,6 +1091,7 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
}
public Builder setIdnTables(ImmutableSet<IdnTableEnum> idnTables) {
// TODO(b/309175133): forbid if enrolled with BSA.
getInstance().idnTables = idnTables;
return this;
}
@@ -1084,6 +1101,13 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
return this;
}
public Builder setBsaEnrollStartTime(DateTime enrollTime) {
// TODO(b/309175133): forbid if enrolled with BSA
// TODO(11/30/2023): uncomment below line
// getInstance().bsaEnrollStartTime = enrollTime;
return this;
}
@Override
public Tld build() {
final Tld instance = getInstance();
@@ -56,7 +56,7 @@ public final class Tlds {
private static Supplier<ImmutableMap<String, TldType>> createFreshCache() {
return memoizeWithShortExpiration(
() ->
tm().transact(
tm().reTransact(
() -> {
EntityManager entityManager = tm().getEntityManager();
Stream<?> resultStream =
@@ -43,8 +43,6 @@ import google.registry.rde.JSchModule;
import google.registry.request.Modules.GsonModule;
import google.registry.request.Modules.NetHttpTransportModule;
import google.registry.request.Modules.UrlConnectionServiceModule;
import google.registry.request.Modules.UrlFetchServiceModule;
import google.registry.request.Modules.UrlFetchTransportModule;
import google.registry.request.Modules.UserServiceModule;
import google.registry.request.auth.AuthModule;
import google.registry.util.UtilsModule;
@@ -80,8 +78,6 @@ import javax.inject.Singleton;
SheetsServiceModule.class,
StackdriverModule.class,
UrlConnectionServiceModule.class,
UrlFetchServiceModule.class,
UrlFetchTransportModule.class,
UserServiceModule.class,
VoidDnsWriterModule.class,
UtilsModule.class
@@ -26,7 +26,6 @@ import google.registry.batch.RelockDomainAction;
import google.registry.batch.ResaveAllEppResourcesPipelineAction;
import google.registry.batch.ResaveEntityAction;
import google.registry.batch.SendExpiringCertificateNotificationEmailAction;
import google.registry.batch.WipeOutCloudSqlAction;
import google.registry.batch.WipeOutContactHistoryPiiAction;
import google.registry.cron.CronModule;
import google.registry.cron.TldFanoutAction;
@@ -178,8 +177,6 @@ interface BackendRequestComponent {
UpdateRegistrarRdapBaseUrlsAction updateRegistrarRdapBaseUrlsAction();
WipeOutCloudSqlAction wipeOutCloudSqlAction();
WipeOutContactHistoryPiiAction wipeOutContactHistoryPiiAction();
@Subcomponent.Builder
@@ -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.ConsoleDomainListAction;
import google.registry.ui.server.console.ConsoleUserDataAction;
import google.registry.ui.server.console.RegistrarsAction;
import google.registry.ui.server.console.settings.ContactAction;
@@ -55,6 +56,8 @@ import google.registry.ui.server.registrar.RegistryLockVerifyAction;
interface FrontendRequestComponent {
ConsoleDomainGetAction consoleDomainGetAction();
ConsoleDomainListAction consoleDomainListAction();
ConsoleOteSetupAction consoleOteSetupAction();
ConsoleRegistrarCreatorAction consoleRegistrarCreatorAction();
ConsoleUiAction consoleUiAction();
@@ -57,7 +57,7 @@ public class VKey<T> extends ImmutableObject implements Serializable {
// The primary key for the referenced entity.
@Expose Serializable key;
Class<? extends T> kind;
@Expose Class<? extends T> kind;
@SuppressWarnings("unused")
VKey() {}
@@ -62,7 +62,7 @@ class DatabaseException extends PersistenceException {
* <p>If the {@code original Throwable} has at least one {@link SQLException} in its chain of
* causes, a {@link DatabaseException} is thrown; otherwise this does nothing.
*/
static void tryWrapAndThrow(Throwable original) {
static void throwIfSqlException(Throwable original) {
Throwable t = original;
do {
if (t instanceof SQLException) {
@@ -16,7 +16,6 @@ package google.registry.persistence.transaction;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import google.registry.persistence.VKey;
import java.util.function.Supplier;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
@@ -62,24 +61,6 @@ public interface JpaTransactionManager extends TransactionManager {
*/
Query query(String sqlString);
/** Executes the work in a transaction with no retries and returns the result. */
<T> T transactNoRetry(Supplier<T> work);
/**
* Executes the work in a transaction at the given {@link TransactionIsolationLevel} with no
* retries and returns the result.
*/
<T> T transactNoRetry(Supplier<T> work, TransactionIsolationLevel isolationLevel);
/** Executes the work in a transaction with no retries. */
void transactNoRetry(Runnable work);
/**
* Executes the work in a transaction at the given {@link TransactionIsolationLevel} with no
* retries.
*/
void transactNoRetry(Runnable work, TransactionIsolationLevel isolationLevel);
/** Deletes the entity by its id, throws exception if the entity is not deleted. */
<T> void assertDelete(VKey<T> key);
@@ -103,7 +84,4 @@ public interface JpaTransactionManager extends TransactionManager {
/** Return the {@link TransactionIsolationLevel} used in the current transaction. */
TransactionIsolationLevel getCurrentTransactionIsolationLevel();
/** Asserts that the current transaction runs at the given level. */
void assertTransactionIsolationLevel(TransactionIsolationLevel expectedLevel);
}
@@ -15,11 +15,12 @@
package google.registry.persistence.transaction;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Throwables.throwIfUnchecked;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.config.RegistryConfig.getHibernatePerTransactionIsolationEnabled;
import static google.registry.persistence.transaction.DatabaseException.tryWrapAndThrow;
import static google.registry.config.RegistryConfig.getHibernateAllowNestedTransactions;
import static google.registry.persistence.transaction.DatabaseException.throwIfSqlException;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import static java.util.AbstractMap.SimpleEntry;
import static java.util.stream.Collectors.joining;
@@ -31,6 +32,7 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import com.google.common.flogger.StackSize;
import google.registry.model.ImmutableObject;
import google.registry.persistence.JpaRetries;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
@@ -52,7 +54,7 @@ import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import java.util.concurrent.Callable;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import javax.annotation.Nullable;
@@ -76,6 +78,9 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final Retrier retrier = new Retrier(new SystemSleeper(), 3);
private static final String NESTED_TRANSACTION_MESSAGE =
"Nested transaction detected. Try refactoring to avoid nested transactions. If unachievable,"
+ " use reTransact() in nested transactions";
// EntityManagerFactory is thread safe.
private final EntityManagerFactory emf;
@@ -138,21 +143,23 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
}
@Override
public void assertTransactionIsolationLevel(TransactionIsolationLevel expectedLevel) {
assertInTransaction();
TransactionIsolationLevel currentLevel = getCurrentTransactionIsolationLevel();
if (currentLevel != expectedLevel) {
throw new IllegalStateException(
String.format(
"Current transaction isolation level (%s) is not as expected (%s)",
currentLevel, expectedLevel));
public <T> T reTransact(Callable<T> work) {
// This prevents inner transaction from retrying, thus avoiding a cascade retry effect.
if (inTransaction()) {
return transactNoRetry(work, null);
}
return retrier.callWithRetry(
() -> transactNoRetry(work, null), JpaRetries::isFailedTxnRetriable);
}
@Override
public <T> T transact(Supplier<T> work, TransactionIsolationLevel isolationLevel) {
// This prevents inner transaction from retrying, thus avoiding a cascade retry effect.
public <T> T transact(Callable<T> work, TransactionIsolationLevel isolationLevel) {
if (inTransaction()) {
if (!getHibernateAllowNestedTransactions()) {
throw new IllegalStateException(NESTED_TRANSACTION_MESSAGE);
}
logger.atWarning().withStackTrace(StackSize.MEDIUM).log(NESTED_TRANSACTION_MESSAGE);
// This prevents inner transaction from retrying, thus avoiding a cascade retry effect.
return transactNoRetry(work, isolationLevel);
}
return retrier.callWithRetry(
@@ -160,30 +167,32 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
}
@Override
public <T> T reTransact(Supplier<T> work) {
return transact(work);
}
@Override
public <T> T transact(Supplier<T> work) {
public <T> T transact(Callable<T> work) {
return transact(work, null);
}
@Override
public <T> T transactNoRetry(
Supplier<T> work, @Nullable TransactionIsolationLevel isolationLevel) {
Callable<T> work, @Nullable TransactionIsolationLevel isolationLevel) {
if (inTransaction()) {
if (isolationLevel != null && getHibernatePerTransactionIsolationEnabled()) {
TransactionIsolationLevel enclosingLevel = getCurrentTransactionIsolationLevel();
if (isolationLevel != enclosingLevel) {
throw new IllegalStateException(
String.format(
"Isolation level conflict detected in nested transactions.\n"
+ "Enclosing transaction: %s\nCurrent transaction: %s",
enclosingLevel, isolationLevel));
}
// This check will no longer be necessary when the transact() method always throws
// inside a nested transaction, as the only way to pass a non-null isolation level
// is by calling the transact() method (and its variants), which would have already
// thrown before calling transactNoRetry() when inside a nested transaction.
//
// For now, we still need it, so we don't accidentally call a nested transact() with an
// isolation level override. This buys us time to detect nested transact() calls and either
// remove them or change the call site to reTransact().
if (isolationLevel != null) {
throw new IllegalStateException(
"Transaction isolation level cannot be specified for nested transactions");
}
try {
return work.call();
} catch (Exception e) {
throwIfSqlException(e);
throwIfUnchecked(e);
throw new RuntimeException(e);
}
return work.get();
}
TransactionInfo txnInfo = transactionInfo.get();
txnInfo.entityManager = emf.createEntityManager();
@@ -191,43 +200,36 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
try {
txn.begin();
txnInfo.start(clock);
if (isolationLevel != null) {
if (getHibernatePerTransactionIsolationEnabled()) {
getEntityManager()
.createNativeQuery(
String.format("SET TRANSACTION ISOLATION LEVEL %s", isolationLevel.getMode()))
.executeUpdate();
logger.atInfo().log("Running transaction at %s", isolationLevel);
} else {
logger.atWarning().log(
"Per-transaction isolation level disabled, but %s was requested", isolationLevel);
}
if (isolationLevel != null && isolationLevel != getDefaultTransactionIsolationLevel()) {
getEntityManager()
.createNativeQuery(
String.format("SET TRANSACTION ISOLATION LEVEL %s", isolationLevel.getMode()))
.executeUpdate();
logger.atInfo().log(
"Overriding transaction isolation level from %s to %s",
getDefaultTransactionIsolationLevel(), isolationLevel);
}
T result = work.get();
T result = work.call();
txn.commit();
return result;
} catch (RuntimeException | Error e) {
// Error is unchecked!
} catch (Throwable e) {
// Catch a Throwable here so even Errors would lead to a rollback.
try {
txn.rollback();
logger.atWarning().log("Error during transaction; transaction rolled back.");
} catch (Throwable rollbackException) {
} catch (Exception rollbackException) {
logger.atSevere().withCause(rollbackException).log("Rollback failed; suppressing error.");
}
tryWrapAndThrow(e);
throw e;
throwIfSqlException(e);
throwIfUnchecked(e);
throw new RuntimeException(e);
} finally {
txnInfo.clear();
}
}
@Override
public <T> T transactNoRetry(Supplier<T> work) {
return transactNoRetry(work, null);
}
@Override
public void transact(Runnable work, TransactionIsolationLevel isolationLevel) {
public void transact(ThrowingRunnable work, TransactionIsolationLevel isolationLevel) {
transact(
() -> {
work.run();
@@ -237,28 +239,17 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
}
@Override
public void reTransact(Runnable work) {
transact(work);
}
@Override
public void transact(Runnable work) {
public void transact(ThrowingRunnable work) {
transact(work, null);
}
@Override
public void transactNoRetry(Runnable work, TransactionIsolationLevel isolationLevel) {
transactNoRetry(
public void reTransact(ThrowingRunnable work) {
reTransact(
() -> {
work.run();
return null;
},
isolationLevel);
}
@Override
public void transactNoRetry(Runnable work) {
transactNoRetry(work, null);
});
}
@Override
@@ -22,7 +22,7 @@ import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import google.registry.persistence.VKey;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.concurrent.Callable;
import java.util.stream.Stream;
import org.joda.time.DateTime;
@@ -48,51 +48,51 @@ public interface TransactionManager {
void assertInTransaction();
/** Executes the work in a transaction and returns the result. */
<T> T transact(Supplier<T> work);
<T> T transact(Callable<T> work);
/**
* Executes the work in a transaction at the given {@link TransactionIsolationLevel} and returns
* the result.
*/
<T> T transact(Supplier<T> work, TransactionIsolationLevel isolationLevel);
<T> T transact(Callable<T> work, TransactionIsolationLevel isolationLevel);
/**
* Executes the work in a (potentially wrapped) transaction and returns the result.
*
* <p>Calls to this method are typically going to be in inner functions, that are called either as
* top-level transactions themselves or are nested inside of larger transactions (e.g. a
* top-level transactions themselves or are nested inside larger transactions (e.g. a
* transactional flow). Invocations of reTransact must be vetted to occur in both situations and
* with such complexity that it is not trivial to refactor out the nested transaction calls. New
* code should be written in such a way as to avoid requiring reTransact in the first place.
*
* <p>In the future we will be enforcing that {@link #transact(Supplier)} calls be top-level only,
* <p>In the future we will be enforcing that {@link #transact(Callable)} calls be top-level only,
* with reTransact calls being the only ones that can potentially be an inner nested transaction
* (which is a noop). Note that, as this can be a nested inner exception, there is no overload
* provided to specify a (potentially conflicting) transaction isolation level.
*/
<T> T reTransact(Supplier<T> work);
<T> T reTransact(Callable<T> work);
/** Executes the work in a transaction. */
void transact(Runnable work);
void transact(ThrowingRunnable work);
/** Executes the work in a transaction at the given {@link TransactionIsolationLevel}. */
void transact(Runnable work, TransactionIsolationLevel isolationLevel);
void transact(ThrowingRunnable work, TransactionIsolationLevel isolationLevel);
/**
* Executes the work in a (potentially wrapped) transaction and returns the result.
*
* <p>Calls to this method are typically going to be in inner functions, that are called either as
* top-level transactions themselves or are nested inside of larger transactions (e.g. a
* top-level transactions themselves or are nested inside larger transactions (e.g. a
* transactional flow). Invocations of reTransact must be vetted to occur in both situations and
* with such complexity that it is not trivial to refactor out the nested transaction calls. New
* code should be written in such a way as to avoid requiring reTransact in the first place.
*
* <p>In the future we will be enforcing that {@link #transact(Runnable)} calls be top-level only,
* with reTransact calls being the only ones that can potentially be an inner nested transaction
* (which is a noop). Note that, as this can be a nested inner exception, there is no overload *
* provided to specify a (potentially conflicting) transaction isolation level.
* <p>In the future we will be enforcing that {@link #transact(ThrowingRunnable)} calls be
* top-level only, with reTransact calls being the only ones that can potentially be an inner
* nested transaction (which is a noop). Note that, as this can be a nested inner exception, there
* is no overload provided to specify a (potentially conflicting) transaction isolation level.
*/
void reTransact(Runnable work);
void reTransact(ThrowingRunnable work);
/** Returns the time associated with the start of this particular transaction attempt. */
DateTime getTransactionTime();
@@ -216,4 +216,15 @@ public interface TransactionManager {
/** Returns a QueryComposer which can be used to perform queries against the current database. */
<T> QueryComposer<T> createQueryComposer(Class<T> entity);
/**
* A runnable that allows for checked exceptions to be thrown.
*
* <p>This makes it easier to write lambdas without having to worry about wrapping and re-throwing
* checked excpetions as unchecked ones.
*/
@FunctionalInterface
interface ThrowingRunnable {
void run() throws Exception;
}
}
@@ -14,22 +14,26 @@
package google.registry.rdap;
import static com.google.api.client.http.HttpStatusCodes.STATUS_CODE_OK;
import static com.google.common.net.HttpHeaders.ACCEPT_ENCODING;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpTransport;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.common.io.ByteStreams;
import google.registry.model.registrar.Registrar;
import google.registry.request.Action;
import google.registry.request.HttpException.InternalServerErrorException;
import google.registry.request.UrlConnectionService;
import google.registry.request.UrlConnectionUtils;
import google.registry.request.auth.Auth;
import google.registry.util.UrlConnectionException;
import java.io.IOException;
import java.io.StringReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.GeneralSecurityException;
import javax.inject.Inject;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
@@ -41,9 +45,9 @@ import org.apache.commons.csv.CSVRecord;
* <p>This will update ALL the REAL registrars. If a REAL registrar doesn't have an RDAP entry in
* MoSAPI, we'll delete any BaseUrls it has.
*
* <p>The ICANN base website that provides this information can be found at
* https://www.iana.org/assignments/registrar-ids/registrar-ids.xhtml. The provided CSV endpoint
* requires no authentication.
* <p>The ICANN base website that provides this information can be found at <a
* href=https://www.iana.org/assignments/registrar-ids/registrar-ids.xhtml>here</a>. The provided
* CSV endpoint requires no authentication.
*/
@Action(
service = Action.Service.BACKEND,
@@ -52,22 +56,26 @@ import org.apache.commons.csv.CSVRecord;
auth = Auth.AUTH_API_ADMIN)
public final class UpdateRegistrarRdapBaseUrlsAction implements Runnable {
private static final GenericUrl RDAP_IDS_URL =
new GenericUrl("https://www.iana.org/assignments/registrar-ids/registrar-ids-1.csv");
private static final String RDAP_IDS_URL =
"https://www.iana.org/assignments/registrar-ids/registrar-ids-1.csv";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@Inject HttpTransport httpTransport;
@Inject UrlConnectionService urlConnectionService;
@Inject
UpdateRegistrarRdapBaseUrlsAction() {}
@Override
public void run() {
ImmutableMap<String, String> ianaIdsToUrls = getIanaIdsToUrls();
tm().transact(() -> processAllRegistrars(ianaIdsToUrls));
try {
ImmutableMap<String, String> ianaIdsToUrls = getIanaIdsToUrls();
tm().transact(() -> processAllRegistrars(ianaIdsToUrls));
} catch (Exception e) {
throw new InternalServerErrorException("Error when retrieving RDAP base URL CSV file", e);
}
}
private void processAllRegistrars(ImmutableMap<String, String> ianaIdsToUrls) {
private static void processAllRegistrars(ImmutableMap<String, String> ianaIdsToUrls) {
int nonUpdatedRegistrars = 0;
for (Registrar registrar : Registrar.loadAll()) {
// Only update REAL registrars
@@ -95,23 +103,28 @@ public final class UpdateRegistrarRdapBaseUrlsAction implements Runnable {
logger.atInfo().log("No change in RDAP base URLs for %d registrars", nonUpdatedRegistrars);
}
private ImmutableMap<String, String> getIanaIdsToUrls() {
private ImmutableMap<String, String> getIanaIdsToUrls()
throws IOException, GeneralSecurityException {
CSVParser csv;
HttpURLConnection connection = urlConnectionService.createConnection(new URL(RDAP_IDS_URL));
// Explictly set the accepted encoding, as we know Brotli causes us problems when talking to
// ICANN.
connection.setRequestProperty(ACCEPT_ENCODING, "gzip");
String csvString;
try {
HttpRequest request = httpTransport.createRequestFactory().buildGetRequest(RDAP_IDS_URL);
// AppEngine might insert accept-encodings for us if we use the default gzip, so remove it
request.getHeaders().setAcceptEncoding(null);
HttpResponse response = request.execute();
String csvString = new String(ByteStreams.toByteArray(response.getContent()), UTF_8);
csv =
CSVFormat.Builder.create(CSVFormat.DEFAULT)
.setHeader()
.setSkipHeaderRecord(true)
.build()
.parse(new StringReader(csvString));
} catch (IOException e) {
throw new RuntimeException("Error when retrieving RDAP base URL CSV file", e);
if (connection.getResponseCode() != STATUS_CODE_OK) {
throw new UrlConnectionException("Failed to load RDAP base URLs from ICANN", connection);
}
csvString = new String(UrlConnectionUtils.getResponseBytes(connection), UTF_8);
} finally {
connection.disconnect();
}
csv =
CSVFormat.Builder.create(CSVFormat.DEFAULT)
.setHeader()
.setSkipHeaderRecord(true)
.build()
.parse(new StringReader(csvString));
ImmutableMap.Builder<String, String> result = new ImmutableMap.Builder<>();
for (CSVRecord record : csv) {
String ianaIdentifierString = record.get("ID");
@@ -14,25 +14,23 @@
package google.registry.rde;
import static com.google.appengine.api.urlfetch.FetchOptions.Builder.validateCertificate;
import static com.google.appengine.api.urlfetch.HTTPMethod.PUT;
import static com.google.common.io.BaseEncoding.base64;
import static com.google.common.net.HttpHeaders.AUTHORIZATION;
import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
import static com.google.api.client.http.HttpStatusCodes.STATUS_CODE_BAD_REQUEST;
import static com.google.api.client.http.HttpStatusCodes.STATUS_CODE_OK;
import static google.registry.request.UrlConnectionUtils.getResponseBytes;
import static google.registry.request.UrlConnectionUtils.setBasicAuth;
import static google.registry.request.UrlConnectionUtils.setPayload;
import static google.registry.util.DomainNameUtils.canonicalizeHostname;
import static java.nio.charset.StandardCharsets.UTF_8;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import com.google.appengine.api.urlfetch.HTTPHeader;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.api.client.http.HttpMethods;
import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import google.registry.config.RegistryConfig.Config;
import google.registry.keyring.api.KeyModule.Key;
import google.registry.request.HttpException.InternalServerErrorException;
import google.registry.util.Retrier;
import google.registry.request.UrlConnectionService;
import google.registry.util.UrlConnectionException;
import google.registry.xjc.XjcXmlTransformer;
import google.registry.xjc.iirdea.XjcIirdeaResponseElement;
import google.registry.xjc.iirdea.XjcIirdeaResult;
@@ -40,10 +38,11 @@ import google.registry.xjc.rdeheader.XjcRdeHeader;
import google.registry.xjc.rdereport.XjcRdeReportReport;
import google.registry.xml.XmlException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.util.Arrays;
import java.security.GeneralSecurityException;
import javax.inject.Inject;
/**
@@ -59,49 +58,47 @@ public class RdeReporter {
* @see <a href="http://tools.ietf.org/html/draft-lozano-icann-registry-interfaces-05#section-4">
* ICANN Registry Interfaces - Interface details</a>
*/
private static final String REPORT_MIME = "text/xml";
private static final MediaType MEDIA_TYPE = MediaType.XML_UTF_8;
@Inject Retrier retrier;
@Inject URLFetchService urlFetchService;
@Inject UrlConnectionService urlConnectionService;
@Inject @Config("rdeReportUrlPrefix") String reportUrlPrefix;
@Inject @Key("icannReportingPassword") String password;
@Inject RdeReporter() {}
/** Uploads {@code reportBytes} to ICANN. */
public void send(byte[] reportBytes) throws XmlException {
XjcRdeReportReport report = XjcXmlTransformer.unmarshal(
XjcRdeReportReport.class, new ByteArrayInputStream(reportBytes));
public void send(byte[] reportBytes) throws XmlException, GeneralSecurityException, IOException {
XjcRdeReportReport report =
XjcXmlTransformer.unmarshal(
XjcRdeReportReport.class, new ByteArrayInputStream(reportBytes));
XjcRdeHeader header = report.getHeader().getValue();
// Send a PUT request to ICANN's HTTPS server.
URL url = makeReportUrl(header.getTld(), report.getId());
String username = header.getTld() + "_ry";
String token = base64().encode(String.format("%s:%s", username, password).getBytes(UTF_8));
final HTTPRequest req = new HTTPRequest(url, PUT, validateCertificate().setDeadline(60d));
req.addHeader(new HTTPHeader(CONTENT_TYPE, REPORT_MIME));
req.addHeader(new HTTPHeader(AUTHORIZATION, "Basic " + token));
req.setPayload(reportBytes);
logger.atInfo().log("Sending report:\n%s", new String(reportBytes, UTF_8));
HTTPResponse rsp =
retrier.callWithRetry(
() -> {
HTTPResponse rsp1 = urlFetchService.fetch(req);
int responseCode = rsp1.getResponseCode();
if (responseCode != SC_OK && responseCode != SC_BAD_REQUEST) {
logger.atSevere().log(
"Failure when trying to PUT RDE report to ICANN server: %d\n%s",
responseCode, Arrays.toString(rsp1.getContent()));
throw new RuntimeException("Error uploading deposits to ICANN");
}
return rsp1;
},
SocketTimeoutException.class);
HttpURLConnection connection = urlConnectionService.createConnection(url);
connection.setRequestMethod(HttpMethods.PUT);
setBasicAuth(connection, username, password);
setPayload(connection, reportBytes, MEDIA_TYPE.toString());
int responseCode;
byte[] responseBytes;
// Ensure the XML response is valid. The EPP result code would not be 1000 if we get an
// SC_BAD_REQUEST as the HTTP response code.
XjcIirdeaResult result = parseResult(rsp.getContent());
if (result.getCode().getValue() != 1000) {
try {
responseCode = connection.getResponseCode();
if (responseCode != STATUS_CODE_OK && responseCode != STATUS_CODE_BAD_REQUEST) {
logger.atWarning().log("Connection to RDE report server failed: %d", responseCode);
throw new UrlConnectionException("PUT failed", connection);
}
responseBytes = getResponseBytes(connection);
} finally {
connection.disconnect();
}
// We know that an HTTP 200 response can only contain a result code of
// 1000 (i. e. success), there is no need to parse it.
if (responseCode != STATUS_CODE_OK) {
XjcIirdeaResult result = parseResult(responseBytes);
logger.atWarning().log(
"Rejected when trying to PUT RDE report to ICANN server: %d %s\n%s",
result.getCode().getValue(), result.getMsg(), result.getDescription());
@@ -116,10 +113,11 @@ public class RdeReporter {
* href="http://tools.ietf.org/html/draft-lozano-icann-registry-interfaces-05#section-4.1">
* ICANN Registry Interfaces - IIRDEA Result Object</a>
*/
private XjcIirdeaResult parseResult(byte[] responseBytes) throws XmlException {
private static XjcIirdeaResult parseResult(byte[] responseBytes) throws XmlException {
logger.atInfo().log("Received response:\n%s", new String(responseBytes, UTF_8));
XjcIirdeaResponseElement response = XjcXmlTransformer.unmarshal(
XjcIirdeaResponseElement.class, new ByteArrayInputStream(responseBytes));
XjcIirdeaResponseElement response =
XjcXmlTransformer.unmarshal(
XjcIirdeaResponseElement.class, new ByteArrayInputStream(responseBytes));
return response.getResult();
}
@@ -14,33 +14,31 @@
package google.registry.reporting.icann;
import static com.google.api.client.http.HttpStatusCodes.STATUS_CODE_BAD_REQUEST;
import static com.google.api.client.http.HttpStatusCodes.STATUS_CODE_OK;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.net.MediaType.CSV_UTF_8;
import static google.registry.model.tld.Tlds.assertTldExists;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.api.client.http.ByteArrayContent;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpHeaders;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpResponseException;
import com.google.api.client.http.HttpStatusCodes;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.HttpMethods;
import com.google.common.base.Ascii;
import com.google.common.base.Splitter;
import com.google.common.flogger.FluentLogger;
import com.google.common.io.BaseEncoding;
import com.google.common.io.ByteStreams;
import google.registry.config.RegistryConfig.Config;
import google.registry.keyring.api.KeyModule.Key;
import google.registry.reporting.icann.IcannReportingModule.ReportType;
import google.registry.request.UrlConnectionService;
import google.registry.request.UrlConnectionUtils;
import google.registry.xjc.XjcXmlTransformer;
import google.registry.xjc.iirdea.XjcIirdeaResponseElement;
import google.registry.xjc.iirdea.XjcIirdeaResult;
import google.registry.xml.XmlException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.util.List;
import javax.inject.Inject;
import org.joda.time.YearMonth;
@@ -62,78 +60,64 @@ public class IcannHttpReporter {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@Inject HttpTransport httpTransport;
@Inject @Key("icannReportingPassword") String password;
@Inject @Config("icannTransactionsReportingUploadUrl") String icannTransactionsUrl;
@Inject @Config("icannActivityReportingUploadUrl") String icannActivityUrl;
@Inject IcannHttpReporter() {}
@Inject UrlConnectionService urlConnectionService;
@Inject
@Key("icannReportingPassword")
String password;
@Inject
@Config("icannTransactionsReportingUploadUrl")
String icannTransactionsUrl;
@Inject
@Config("icannActivityReportingUploadUrl")
String icannActivityUrl;
@Inject
IcannHttpReporter() {}
/** Uploads {@code reportBytes} to ICANN, returning whether or not it succeeded. */
public boolean send(byte[] reportBytes, String reportFilename) throws XmlException, IOException {
public boolean send(byte[] reportBytes, String reportFilename)
throws GeneralSecurityException, XmlException, IOException {
validateReportFilename(reportFilename);
GenericUrl uploadUrl = new GenericUrl(makeUrl(reportFilename));
HttpRequest request =
httpTransport
.createRequestFactory()
.buildPutRequest(uploadUrl, new ByteArrayContent(CSV_UTF_8.toString(), reportBytes));
HttpHeaders headers = request.getHeaders();
headers.setBasicAuthentication(getTld(reportFilename) + "_ry", password);
headers.setContentType(CSV_UTF_8.toString());
request.setHeaders(headers);
request.setFollowRedirects(false);
request.setThrowExceptionOnExecuteError(false);
HttpResponse response = null;
URL uploadUrl = makeUrl(reportFilename);
logger.atInfo().log(
"Sending report to %s with content length %d.",
uploadUrl, request.getContent().getLength());
boolean success = true;
"Sending report to %s with content length %d.", uploadUrl, reportBytes.length);
HttpURLConnection connection = urlConnectionService.createConnection(uploadUrl);
connection.setRequestMethod(HttpMethods.PUT);
UrlConnectionUtils.setBasicAuth(connection, getTld(reportFilename) + "_ry", password);
UrlConnectionUtils.setPayload(connection, reportBytes, CSV_UTF_8.toString());
connection.setInstanceFollowRedirects(false);
int responseCode;
byte[] content;
try {
response = request.execute();
// Only responses with a 200 or 400 status have a body. For everything else, throw so that
// the caller catches it and prints the stack trace.
if (response.getStatusCode() != HttpStatusCodes.STATUS_CODE_OK
&& response.getStatusCode() != HttpStatusCodes.STATUS_CODE_BAD_REQUEST) {
throw new HttpResponseException(response);
}
byte[] content;
try {
content = ByteStreams.toByteArray(response.getContent());
} finally {
response.getContent().close();
}
logger.atInfo().log(
"Received response code %d\n\n"
+ "Response headers: %s\n\n"
+ "Response content in UTF-8: %s\n\n"
+ "Response content in HEX: %s",
response.getStatusCode(),
response.getHeaders(),
new String(content, UTF_8),
BaseEncoding.base16().encode(content));
// For reasons unclear at the moment, when we parse the response content using UTF-8 we get
// garbled texts. Since we know that an HTTP 200 response can only contain a result code of
// 1000 (i. e. success), there is no need to parse it.
if (response.getStatusCode() == HttpStatusCodes.STATUS_CODE_BAD_REQUEST) {
success = false;
XjcIirdeaResult result = parseResult(content);
logger.atWarning().log(
"PUT rejected, status code %s:\n%s\n%s",
result.getCode().getValue(), result.getMsg(), result.getDescription());
responseCode = connection.getResponseCode();
// Only responses with a 200 or 400 status have a body. For everything else, we can return
// false early.
if (responseCode != STATUS_CODE_OK && responseCode != STATUS_CODE_BAD_REQUEST) {
logger.atWarning().log("Connection to ICANN server failed", connection);
return false;
}
content = UrlConnectionUtils.getResponseBytes(connection);
} finally {
if (response != null) {
response.disconnect();
} else {
success = false;
logger.atWarning().log("Received null response from ICANN server at %s", uploadUrl);
}
connection.disconnect();
}
return success;
// We know that an HTTP 200 response can only contain a result code of
// 1000 (i. e. success), there is no need to parse it.
// See: https://tools.ietf.org/html/draft-lozano-icann-registry-interfaces-13#page-16
if (responseCode != STATUS_CODE_OK) {
XjcIirdeaResult result = parseResult(content);
logger.atWarning().log(
"PUT rejected, status code %s:\n%s\n%s",
result.getCode().getValue(), result.getMsg(), result.getDescription());
return false;
}
return true;
}
private XjcIirdeaResult parseResult(byte[] content) throws XmlException {
private static XjcIirdeaResult parseResult(byte[] content) throws XmlException {
XjcIirdeaResponseElement response =
XjcXmlTransformer.unmarshal(
XjcIirdeaResponseElement.class, new ByteArrayInputStream(content));
@@ -141,7 +125,7 @@ public class IcannHttpReporter {
}
/** Verifies a given report filename matches the pattern tld-reportType-yyyyMM.csv. */
private void validateReportFilename(String filename) {
private static void validateReportFilename(String filename) {
checkArgument(
filename.matches("[a-z0-9.\\-]+-((activity)|(transactions))-[0-9]{6}\\.csv"),
"Expected file format: tld-reportType-yyyyMM.csv, got %s instead",
@@ -149,12 +133,12 @@ public class IcannHttpReporter {
assertTldExists(getTld(filename));
}
private String getTld(String filename) {
private static String getTld(String filename) {
// Extract the TLD, up to second-to-last hyphen in the filename (works with international TLDs)
return filename.substring(0, filename.lastIndexOf('-', filename.lastIndexOf('-') - 1));
}
private String makeUrl(String filename) {
private URL makeUrl(String filename) throws MalformedURLException {
// Filename is in the format tld-reportType-yearMonth.csv
String tld = getTld(filename);
// Remove the tld- prefix and csv suffix
@@ -164,7 +148,7 @@ public class IcannHttpReporter {
// Re-add hyphen between year and month, because ICANN is inconsistent between filename and URL
String yearMonth =
YearMonth.parse(elements.get(1), DateTimeFormat.forPattern("yyyyMM")).toString("yyyy-MM");
return String.format("%s/%s/%s", getUrlPrefix(reportType), tld, yearMonth);
return new URL(String.format("%s/%s/%s", getUrlPrefix(reportType), tld, yearMonth));
}
private String getUrlPrefix(ReportType reportType) {
@@ -14,20 +14,18 @@
package google.registry.request;
import com.google.api.client.extensions.appengine.http.UrlFetchTransport;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.gson.GsonFactory;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.appengine.api.urlfetch.URLFetchServiceFactory;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import dagger.Module;
import dagger.Provides;
import java.net.HttpURLConnection;
import javax.inject.Singleton;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
/** Dagger modules for App Engine services and other vendor classes. */
public final class Modules {
@@ -37,18 +35,16 @@ public final class Modules {
public static final class UrlConnectionServiceModule {
@Provides
static UrlConnectionService provideUrlConnectionService() {
return url -> (HttpURLConnection) url.openConnection();
}
}
/** Dagger module for {@link URLFetchService}. */
@Module
public static final class UrlFetchServiceModule {
private static final URLFetchService fetchService = URLFetchServiceFactory.getURLFetchService();
@Provides
static URLFetchService provideUrlFetchService() {
return fetchService;
return url -> {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
if (connection instanceof HttpsURLConnection) {
HttpsURLConnection httpsConnection = (HttpsURLConnection) connection;
SSLContext tls13Context = SSLContext.getInstance("TLSv1.3");
tls13Context.init(null, null, null);
httpsConnection.setSSLSocketFactory(tls13Context.getSocketFactory());
}
return connection;
};
}
}
@@ -72,17 +68,6 @@ public final class Modules {
}
}
/** Dagger module that causes the App Engine's URL fetcher to be used for Google APIs requests. */
@Module
public static final class UrlFetchTransportModule {
private static final UrlFetchTransport HTTP_TRANSPORT = new UrlFetchTransport();
@Provides
static HttpTransport provideHttpTransport() {
return HTTP_TRANSPORT;
}
}
/**
* Dagger module that provides standard {@link NetHttpTransport}. Used in non App Engine
* environment.
@@ -17,6 +17,7 @@ package google.registry.request;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
@@ -162,6 +163,10 @@ public class RequestHandler<C> {
} catch (HttpException e) {
e.send(rsp);
success = false;
} catch (Exception e) {
rsp.setStatus(SC_INTERNAL_SERVER_ERROR);
rsp.getWriter().write("Internal server error, please try again later");
logger.atSevere().withCause(e).log("Encountered internal server error");
} finally {
requestMetrics.record(
new Duration(startTime, clock.nowUtc()),
@@ -15,6 +15,7 @@
package google.registry.request;
import com.google.common.net.MediaType;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import org.joda.time.DateTime;
@@ -51,4 +52,11 @@ public interface Response {
* @see HttpServletResponse#setDateHeader(String, long)
*/
void setDateHeader(String header, DateTime timestamp);
/**
* Adds a cookie to the response
*
* @see HttpServletResponse#addCookie(Cookie)
*/
void addCookie(Cookie cookie);
}
@@ -17,6 +17,7 @@ package google.registry.request;
import com.google.common.net.MediaType;
import java.io.IOException;
import javax.inject.Inject;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import org.joda.time.DateTime;
@@ -58,4 +59,9 @@ public final class ResponseImpl implements Response {
public void setDateHeader(String header, DateTime timestamp) {
rsp.setDateHeader(header, timestamp.getMillis());
}
@Override
public void addCookie(Cookie cookie) {
rsp.addCookie(cookie);
}
}
@@ -27,15 +27,20 @@ import com.google.common.io.ByteStreams;
import com.google.common.net.MediaType;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLConnection;
import java.util.Random;
/** Utilities for common functionality relating to {@link java.net.URLConnection}s. */
public class UrlConnectionUtils {
/** Utilities for common functionality relating to {@link URLConnection}s. */
public final class UrlConnectionUtils {
private UrlConnectionUtils() {}
/** Retrieves the response from the given connection as a byte array. */
public static byte[] getResponseBytes(URLConnection connection) throws IOException {
return ByteStreams.toByteArray(connection.getInputStream());
try (InputStream is = connection.getInputStream()) {
return ByteStreams.toByteArray(is);
}
}
/** Sets auth on the given connection with the given username/password. */
@@ -15,8 +15,6 @@
package google.registry.request.auth;
import com.google.common.collect.ImmutableList;
import google.registry.flows.EppTlsAction;
import google.registry.flows.TlsCredentials;
import google.registry.request.auth.AuthSettings.AuthLevel;
import google.registry.request.auth.AuthSettings.AuthMethod;
import google.registry.request.auth.AuthSettings.UserPolicy;
@@ -32,46 +30,39 @@ public enum Auth {
* <p>If a user is logged in, will authenticate (and return) them. Otherwise, access is still
* granted, but NOT_AUTHENTICATED is returned.
*
* <p>This is used for public HTML endpoints like RDAP, the check API, and web WHOIS.
*
* <p>User-facing legacy console endpoints (those that extend {@link HtmlAction}) also use it.
* They need to allow requests from signed-out users so that they can redirect users to the login
* page. After a user is logged in, they check if the user actually has access to the specific
* console using {@link AuthenticatedRegistrarAccessor}.
* <p>User-facing legacy console endpoints (those that extend {@link HtmlAction}) use it. They
* need to allow requests from signed-out users so that they can redirect users to the login page.
* After a user is logged in, they check if the user actually has access to the specific console
* using {@link AuthenticatedRegistrarAccessor}.
*
* @see HtmlAction
*/
AUTH_PUBLIC(
AUTH_PUBLIC_LEGACY(
ImmutableList.of(AuthMethod.API, AuthMethod.LEGACY), AuthLevel.NONE, UserPolicy.PUBLIC),
/**
* Allows anyone to access, as long as they are logged in.
*
* <p>This is used by legacy registrar console programmatic endpoints (those that extend {@link
* JsonGetAction}, which are accessed via XHR requests sent from a logged-in user when performing
* JsonGetAction}), which are accessed via XHR requests sent from a logged-in user when performing
* actions on the console.
*/
AUTH_PUBLIC_LOGGED_IN(
ImmutableList.of(AuthMethod.API, AuthMethod.LEGACY), AuthLevel.USER, UserPolicy.PUBLIC),
/**
* Allows any client to access, as long as they are logged in via API-based authentication
* mechanisms.
* Allows anyone to access.
*
* <p>This is used by the proxy to access Nomulus endpoints. The proxy service account does NOT
* have admin privileges. For EPP, we handle client authentication within {@link EppTlsAction},
* using {@link TlsCredentials}. For WHOIS, anyone connecting to the proxy can access.
*
* <p>Note that the proxy service account DOES need to be allow-listed in the {@code
* auth.allowedServiceAccountEmails} field in the config YAML file in order for OIDC-based
* authentication to pass.
* <p>This is used for public HTML endpoints like RDAP, the check API, and web WHOIS.
*/
AUTH_API_PUBLIC(ImmutableList.of(AuthMethod.API), AuthLevel.APP, UserPolicy.PUBLIC),
AUTH_PUBLIC(ImmutableList.of(AuthMethod.API), AuthLevel.NONE, UserPolicy.PUBLIC),
/**
* Allows only admins to access.
* Allows only the app itself (via service accounts) or admins to access.
*
* <p>This applies to the majority of the endpoints.
* <p>This applies to the majority of the endpoints. For APP level authentication to work, the
* associated service account needs to be allowlisted in the {@code
* auth.allowedServiceAccountEmails} field in the config YAML file.
*/
AUTH_API_ADMIN(ImmutableList.of(AuthMethod.API), AuthLevel.APP, UserPolicy.ADMIN);
@@ -16,8 +16,6 @@ package google.registry.request.auth;
import static com.google.common.net.HttpHeaders.AUTHORIZATION;
import com.google.appengine.api.oauth.OAuthService;
import com.google.appengine.api.oauth.OAuthServiceFactory;
import com.google.auth.oauth2.TokenVerifier;
import com.google.common.collect.ImmutableList;
import dagger.Module;
@@ -36,9 +34,6 @@ public class AuthModule {
// IAP-signed JWT will be in this header.
// See https://cloud.google.com/iap/docs/signed-headers-howto#securing_iap_headers.
public static final String IAP_HEADER_NAME = "X-Goog-IAP-JWT-Assertion";
// GAE will put the content in header "proxy-authorization" in this header when it routes the
// request to the app.
public static final String PROXY_HEADER_NAME = "X-Google-Proxy-Authorization";
public static final String BEARER_PREFIX = "Bearer ";
// TODO: Change the IAP audience format once we are on GKE.
// See: https://cloud.google.com/iap/docs/signed-headers-howto#verifying_the_jwt_payload
@@ -46,16 +41,12 @@ public class AuthModule {
private static final String IAP_ISSUER_URL = "https://cloud.google.com/iap";
private static final String REGULAR_ISSUER_URL = "https://accounts.google.com";
/** Provides the custom authentication mechanisms (including OAuth and OIDC). */
/** Provides the custom authentication mechanisms. */
@Provides
ImmutableList<AuthenticationMechanism> provideApiAuthenticationMechanisms(
OAuthAuthenticationMechanism oauthAuthenticationMechanism,
IapOidcAuthenticationMechanism iapOidcAuthenticationMechanism,
RegularOidcAuthenticationMechanism regularOidcAuthenticationMechanism) {
return ImmutableList.of(
oauthAuthenticationMechanism,
iapOidcAuthenticationMechanism,
regularOidcAuthenticationMechanism);
return ImmutableList.of(iapOidcAuthenticationMechanism, regularOidcAuthenticationMechanism);
}
@Qualifier
@@ -64,11 +55,8 @@ public class AuthModule {
@Qualifier
@interface RegularOidc {}
/** Provides the OAuthService instance. */
@Provides
OAuthService provideOauthService() {
return OAuthServiceFactory.getOAuthService();
}
@Qualifier
@interface RegularOidcFallback {}
@Provides
@IapOidc
@@ -86,6 +74,14 @@ public class AuthModule {
return TokenVerifier.newBuilder().setAudience(clientId).setIssuer(REGULAR_ISSUER_URL).build();
}
@Provides
@RegularOidcFallback
@Singleton
TokenVerifier provideFallbackRegularTokenVerifier(
@Config("fallbackOauthClientId") String clientId) {
return TokenVerifier.newBuilder().setAudience(clientId).setIssuer(REGULAR_ISSUER_URL).build();
}
@Provides
@IapOidc
@Singleton
@@ -98,11 +94,7 @@ public class AuthModule {
@Singleton
TokenExtractor provideRegularTokenExtractor() {
return request -> {
// TODO: only check the Authorizaiton header after the migration to OIDC is complete.
String rawToken = request.getHeader(PROXY_HEADER_NAME);
if (rawToken == null) {
rawToken = request.getHeader(AUTHORIZATION);
}
String rawToken = request.getHeader(AUTHORIZATION);
if (rawToken != null && rawToken.startsWith(BEARER_PREFIX)) {
return rawToken.substring(BEARER_PREFIX.length());
}
@@ -14,7 +14,9 @@
package google.registry.request.auth;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.request.auth.AuthSettings.AuthLevel.APP;
import static google.registry.request.auth.AuthSettings.AuthLevel.USER;
import com.google.auto.value.AutoValue;
import google.registry.request.auth.AuthSettings.AuthLevel;
@@ -22,8 +24,8 @@ import java.util.Optional;
import javax.annotation.Nullable;
/**
* Results of authentication for a given HTTP request, as emitted by an
* {@link AuthenticationMechanism}.
* Results of authentication for a given HTTP request, as emitted by an {@link
* AuthenticationMechanism}.
*/
@AutoValue
public abstract class AuthResult {
@@ -33,6 +35,10 @@ public abstract class AuthResult {
/** Information about the authenticated user, if there is one. */
public abstract Optional<UserAuthInfo> userAuthInfo();
/** Service account email of the authenticated app, if there is one. */
@SuppressWarnings("unused") // The service account will be logged upon successful login.
public abstract Optional<String> appServiceAccount();
public boolean isAuthenticated() {
return authLevel() != AuthLevel.NONE;
}
@@ -47,15 +53,27 @@ public abstract class AuthResult {
.orElse("<logged-out user>");
}
public static AuthResult create(AuthLevel authLevel) {
return new AutoValue_AuthResult(authLevel, Optional.empty());
public static AuthResult createApp(String email) {
return create(APP, null, email);
}
public static AuthResult create(AuthLevel authLevel, @Nullable UserAuthInfo userAuthInfo) {
if (authLevel == AuthLevel.USER) {
checkNotNull(userAuthInfo);
}
return new AutoValue_AuthResult(authLevel, Optional.ofNullable(userAuthInfo));
public static AuthResult createUser(UserAuthInfo userAuthInfo) {
return create(USER, userAuthInfo, null);
}
private static AuthResult create(
AuthLevel authLevel, @Nullable UserAuthInfo userAuthInfo, @Nullable String email) {
checkArgument(
userAuthInfo == null || email == null,
"User auth info and service account email cannot be specificed at the same time");
checkArgument(
authLevel != USER || userAuthInfo != null,
"User auth info must be specified for auth level USER");
checkArgument(
authLevel != APP || email != null,
"Service account email must be specified for auth level APP");
return new AutoValue_AuthResult(
authLevel, Optional.ofNullable(userAuthInfo), Optional.ofNullable(email));
}
/**
@@ -67,5 +85,5 @@ public abstract class AuthResult {
* returns NOT_AUTHENTICATED in this case, as opposed to absent() if authentication failed and was
* required. So as a return from an authorization check, this can be treated as a success.
*/
public static final AuthResult NOT_AUTHENTICATED = create(AuthLevel.NONE);
public static final AuthResult NOT_AUTHENTICATED = create(AuthLevel.NONE, null, null);
}
@@ -17,6 +17,7 @@ package google.registry.request.auth;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.Immutable;
import google.registry.model.console.UserRoles;
/**
* Parameters used to configure the authenticator.
@@ -42,7 +43,10 @@ public abstract class AuthSettings {
/** Available methods for authentication. */
public enum AuthMethod {
/** Authentication methods suitable for API-style access, such as OAuth 2. */
/**
* Authentication methods suitable for API-style access, such as {@link
* OidcTokenAuthenticationMechanism}.
*/
API,
/** Legacy authentication using cookie-based App Engine Users API. Must come last if present. */
@@ -68,10 +72,11 @@ public abstract class AuthSettings {
/**
* Authentication required, but user not required.
*
* <p>In Auth: Authentication is required, but app-internal authentication (which isn't
* associated with a specific user) is permitted.
* <p>In Auth: authentication is required, but App-internal authentication (which isn't
* associated with a specific user, but a service account) is permitted. Examples include
* requests from Cloud Tasks, Cloud Scheduler, and the proxy.
*
* <p>In AuthResult: App-internal authentication was successful.
* <p>In AuthResult: App-internal authentication (via service accounts) was successful.
*/
APP,
@@ -93,10 +98,14 @@ public abstract class AuthSettings {
PUBLIC,
/**
* If there is a user, it must be an admin, as determined by isUserAdmin().
* If there is a user, it must be an admin, as determined by {@link UserAuthInfo#isUserAdmin()}.
*
* <p>Note that, according to App Engine, anybody with access to the app in the GCP Console,
* <p>Note that, if the user returned is an App Engine {@link
* com.google.appengine.api.users.User} , anybody with access to the app in the GCP Console,
* including editors and viewers, is an admin.
*
* <p>On the other hand, if the user is a {@link google.registry.model.console.User}, the admin
* role is explicitly defined in that object via the {@link UserRoles#isAdmin()} method.
*/
ADMIN
}
@@ -19,7 +19,7 @@ import javax.servlet.http.HttpServletRequest;
/**
* A particular way to authenticate an HTTP request, returning an {@link AuthResult}.
*
* <p>For instance, a request could be authenticated using OAuth, via special request headers, etc.
* <p>For instance, a request could be authenticated using OIDC, via special request headers, etc.
*/
public interface AuthenticationMechanism {
@@ -16,8 +16,7 @@ package google.registry.request.auth;
import static com.google.common.base.Strings.emptyToNull;
import static com.google.common.base.Strings.nullToEmpty;
import static google.registry.request.auth.AuthSettings.AuthLevel.NONE;
import static google.registry.request.auth.AuthSettings.AuthLevel.USER;
import static google.registry.request.auth.AuthResult.NOT_AUTHENTICATED;
import static google.registry.security.XsrfTokenManager.P_CSRF_TOKEN;
import static google.registry.security.XsrfTokenManager.X_CSRF_TOKEN;
@@ -49,15 +48,14 @@ public class LegacyAuthenticationMechanism implements AuthenticationMechanism {
@Override
public AuthResult authenticate(HttpServletRequest request) {
if (!userService.isUserLoggedIn()) {
return AuthResult.create(NONE);
return NOT_AUTHENTICATED;
}
if (!SAFE_METHODS.contains(request.getMethod()) && !validateXsrf(request)) {
return AuthResult.create(NONE);
return NOT_AUTHENTICATED;
}
return AuthResult.create(
USER,
return AuthResult.createUser(
UserAuthInfo.create(userService.getCurrentUser(), userService.isUserAdmin()));
}
@@ -1,136 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.request.auth;
import static com.google.common.net.HttpHeaders.AUTHORIZATION;
import static google.registry.request.auth.AuthModule.BEARER_PREFIX;
import static google.registry.request.auth.AuthSettings.AuthLevel.NONE;
import static google.registry.request.auth.AuthSettings.AuthLevel.USER;
import com.google.appengine.api.oauth.OAuthRequestException;
import com.google.appengine.api.oauth.OAuthService;
import com.google.appengine.api.oauth.OAuthServiceFailureException;
import com.google.appengine.api.users.User;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryEnvironment;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
/**
* OAuth authentication mechanism, using the OAuthService interface.
*
* <p>Only OAuth version 2 is supported.
*/
public class OAuthAuthenticationMechanism implements AuthenticationMechanism {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final OAuthService oauthService;
/** The available OAuth scopes for which {@link OAuthService} should check. */
private final ImmutableSet<String> availableOauthScopes;
/** The OAuth scopes which must all be present for authentication to succeed. */
private final ImmutableSet<String> requiredOauthScopes;
private final ImmutableSet<String> allowedOauthClientIds;
@Inject
public OAuthAuthenticationMechanism(
OAuthService oauthService,
@Config("availableOauthScopes") ImmutableSet<String> availableOauthScopes,
@Config("requiredOauthScopes") ImmutableSet<String> requiredOauthScopes,
@Config("allowedOauthClientIds") ImmutableSet<String> allowedOauthClientIds) {
this.oauthService = oauthService;
this.availableOauthScopes = availableOauthScopes;
this.requiredOauthScopes = requiredOauthScopes;
this.allowedOauthClientIds = allowedOauthClientIds;
}
@Override
public AuthResult authenticate(HttpServletRequest request) {
// Make sure that there is an Authorization header in Bearer form. OAuthService also accepts
// tokens in the request body and URL string, but we should not use those, since they are more
// likely to be logged than the Authorization header. Checking to make sure there's a token also
// avoids unnecessary RPCs, since OAuthService itself does not check whether the header is
// present. In theory, there could be more than one Authorization header, but we only check the
// first one, because there's not a legitimate use case for having more than one, and
// OAuthService itself only looks at the first one anyway.
String header = request.getHeader(AUTHORIZATION);
if ((header == null) || !header.startsWith(BEARER_PREFIX)) {
if (header != null) {
logger.atInfo().log("Invalid authorization header.");
}
return AuthResult.create(NONE);
}
// Assume that, if a bearer token is found, it's what OAuthService will use to attempt
// authentication. This is not technically guaranteed by the contract of OAuthService; see
// OAuthTokenInfo for more information.
String rawAccessToken =
RegistryEnvironment.get() == RegistryEnvironment.PRODUCTION
? "Raw token redacted in prod"
: header.substring(BEARER_PREFIX.length());
// Get the OAuth information. The various oauthService method calls use a single cached
// authentication result, so we can call them one by one.
User currentUser;
boolean isUserAdmin;
String oauthClientId;
ImmutableSet<String> authorizedScopes;
try {
String[] availableOauthScopeArray = availableOauthScopes.toArray(new String[0]);
currentUser = oauthService.getCurrentUser(availableOauthScopeArray);
isUserAdmin = oauthService.isUserAdmin(availableOauthScopeArray);
logger.atInfo().log(
"Current user: %s (%s).", currentUser, isUserAdmin ? "admin" : "not admin");
oauthClientId = oauthService.getClientId(availableOauthScopeArray);
logger.atInfo().log("OAuth client ID: %s", oauthClientId);
authorizedScopes =
ImmutableSet.copyOf(oauthService.getAuthorizedScopes(availableOauthScopeArray));
logger.atInfo().log("Authorized scope(s): %s", authorizedScopes);
} catch (OAuthRequestException | OAuthServiceFailureException e) {
logger.atInfo().withCause(e).log("Unable to get OAuth information.");
return AuthResult.create(NONE);
}
if ((currentUser == null) || (oauthClientId == null) || (authorizedScopes == null)) {
return AuthResult.create(NONE);
}
// Make sure that the client ID matches, to avoid a confused deputy attack; see:
// http://stackoverflow.com/a/17439317/1179226
if (!allowedOauthClientIds.contains(oauthClientId)) {
logger.atInfo().log("OAuth client ID is not allowed.");
return AuthResult.create(NONE);
}
// Make sure that all required scopes are present.
if (!authorizedScopes.containsAll(requiredOauthScopes)) {
logger.atInfo().log("Missing required scope(s).");
return AuthResult.create(NONE);
}
// Create the {@link AuthResult}, including the OAuth token info.
return AuthResult.create(
USER,
UserAuthInfo.create(
currentUser,
isUserAdmin,
OAuthTokenInfo.create(
ImmutableSet.copyOf(authorizedScopes), oauthClientId, rawAccessToken)));
}
}
@@ -1,48 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.request.auth;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableSet;
/** Information provided by the OAuth authentication mechanism (only) about the session. */
@AutoValue
public abstract class OAuthTokenInfo {
/** Authorized OAuth scopes granted by the access token provided with the request. */
abstract ImmutableSet<String> authorizedScopes();
/** OAuth client ID from the access token provided with the request. */
abstract String oauthClientId();
/**
* Raw OAuth access token value provided with the request, for passing along to downstream APIs as
* appropriate.
*
* <p>Note that the request parsing code makes certain assumptions about whether the Authorization
* header was used as the source of the token. Because OAuthService could theoretically fall back
* to some other source of authentication, it might be possible for rawAccessToken not to have
* been the source of OAuth authentication. Looking at the code of OAuthService, that could not
* currently happen, but if OAuthService were modified in the future so that it tried the bearer
* token, and then when that failed, fell back to another, successful authentication path, then
* rawAccessToken might not be valid.
*/
abstract String rawAccessToken();
static OAuthTokenInfo create(
ImmutableSet<String> authorizedScopes, String oauthClientId, String rawAccessToken) {
return new AutoValue_OAuthTokenInfo(authorizedScopes, oauthClientId, rawAccessToken);
}
}
@@ -14,8 +14,6 @@
package google.registry.request.auth;
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;
@@ -27,6 +25,7 @@ import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.request.auth.AuthModule.IapOidc;
import google.registry.request.auth.AuthModule.RegularOidc;
import google.registry.request.auth.AuthModule.RegularOidcFallback;
import google.registry.request.auth.AuthSettings.AuthLevel;
import java.util.Optional;
import javax.annotation.Nullable;
@@ -55,6 +54,8 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
protected final TokenVerifier tokenVerifier;
protected final Optional<TokenVerifier> fallbackTokenVerifier;
protected final TokenExtractor tokenExtractor;
private final ImmutableSet<String> serviceAccountEmails;
@@ -62,9 +63,11 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
protected OidcTokenAuthenticationMechanism(
ImmutableSet<String> serviceAccountEmails,
TokenVerifier tokenVerifier,
@Nullable TokenVerifier fallbackTokenVerifier,
TokenExtractor tokenExtractor) {
this.serviceAccountEmails = serviceAccountEmails;
this.tokenVerifier = tokenVerifier;
this.fallbackTokenVerifier = Optional.ofNullable(fallbackTokenVerifier);
this.tokenExtractor = tokenExtractor;
}
@@ -79,7 +82,7 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
if (rawIdToken == null) {
return AuthResult.NOT_AUTHENTICATED;
}
JsonWebSignature token;
JsonWebSignature token = null;
try {
token = tokenVerifier.verify(rawIdToken);
} catch (Exception e) {
@@ -88,8 +91,25 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
RegistryEnvironment.get().equals(RegistryEnvironment.PRODUCTION)
? "Raw token redacted in prod"
: rawIdToken);
return AuthResult.NOT_AUTHENTICATED;
}
if (token == null) {
if (fallbackTokenVerifier.isPresent()) {
try {
token = fallbackTokenVerifier.get().verify(rawIdToken);
} catch (Exception e) {
logger.atInfo().withCause(e).log(
"Failed OIDC fallback verification attempt:\n%s",
RegistryEnvironment.get().equals(RegistryEnvironment.PRODUCTION)
? "Raw token redacted in prod"
: rawIdToken);
return AuthResult.NOT_AUTHENTICATED;
}
} else {
return AuthResult.NOT_AUTHENTICATED;
}
}
String email = (String) token.getPayload().get("email");
if (email == null) {
logger.atWarning().log("No email address from the OIDC token:\n%s", token.getPayload());
@@ -97,12 +117,11 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
}
Optional<User> maybeUser = UserDao.loadUser(email);
if (maybeUser.isPresent()) {
return AuthResult.create(AuthLevel.USER, UserAuthInfo.create(maybeUser.get()));
return AuthResult.createUser(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);
return AuthResult.createApp(email);
}
logger.atInfo().log("No service account found for email address %s", email);
logger.atWarning().log(
@@ -144,7 +163,7 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
@Config("allowedServiceAccountEmails") ImmutableSet<String> serviceAccountEmails,
@IapOidc TokenVerifier tokenVerifier,
@IapOidc TokenExtractor tokenExtractor) {
super(serviceAccountEmails, tokenVerifier, tokenExtractor);
super(serviceAccountEmails, tokenVerifier, null, tokenExtractor);
}
}
@@ -153,15 +172,8 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
*
* <p>If the endpoint is not behind IAP, we can try to authenticate the OIDC token supplied in the
* request header directly. Ideally we would like all endpoints to be behind IAP, but being able
* to authenticate the token directly provides us with the flexibility to do away with OAuth-based
* {@link OAuthAuthenticationMechanism} that is tied to App Engine runtime without having to turn
* on IAP, which is an all-or-nothing switch for each GAE service (i.e. no way to turn it on only
* for certain GAE endpoints).
*
* <p>Note that this mechanism will try to first extract the token under the "proxy-authorization"
* header, before trying "authorization". This is because currently the GAE OAuth service always
* uses "authorization", and we would like to provide a way for both auth mechanisms to be working
* at the same time for the same request.
* to authenticate the token directly provides us with some extra flexibility that comes in handy,
* at least during the migration to GKE.
*
* @see <a href=https://datatracker.ietf.org/doc/html/rfc6750>Bearer Token Usage</a>
*/
@@ -171,8 +183,9 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
protected RegularOidcAuthenticationMechanism(
@Config("allowedServiceAccountEmails") ImmutableSet<String> serviceAccountEmails,
@RegularOidc TokenVerifier tokenVerifier,
@RegularOidcFallback TokenVerifier fallbackTokenVerifier,
@RegularOidc TokenExtractor tokenExtractor) {
super(serviceAccountEmails, tokenVerifier, tokenExtractor);
super(serviceAccountEmails, tokenVerifier, fallbackTokenVerifier, tokenExtractor);
}
}
}
@@ -60,7 +60,8 @@ public class RequestAuthenticator {
if (auth.minimumLevel() == APP && !authResult.isAuthenticated()) {
logger.atWarning().log("Not authorized; no authentication found.");
return Optional.empty();
} else if (auth.minimumLevel() == USER && authResult.authLevel() != USER) {
}
if (auth.minimumLevel() == USER && authResult.authLevel() != USER) {
logger.atWarning().log("Not authorized; no authenticated user.");
return Optional.empty();
}
@@ -81,12 +82,12 @@ public class RequestAuthenticator {
* @param req the {@link HttpServletRequest}; some authentication mechanisms use HTTP headers
* @return an authentication result; if no authentication was made, returns NOT_AUTHENTICATED
*/
private AuthResult authenticate(AuthSettings auth, HttpServletRequest req) {
AuthResult authenticate(AuthSettings auth, HttpServletRequest req) {
checkAuthConfig(auth);
for (AuthMethod authMethod : auth.methods()) {
AuthResult authResult;
switch (authMethod) {
// API-based user authentication mechanisms, such as OAuth and OIDC.
// API-based user authentication mechanisms, such as OIDC.
case API:
for (AuthenticationMechanism authMechanism : apiAuthenticationMechanisms) {
authResult = authMechanism.authenticate(req);
@@ -113,10 +114,9 @@ public class RequestAuthenticator {
/** Validates an AuthSettings object, checking for invalid setting combinations. */
static void checkAuthConfig(AuthSettings auth) {
ImmutableList<AuthMethod> authMethods = ImmutableList.copyOf(auth.methods());
checkArgument(!authMethods.isEmpty(), "Must specify at least one auth method");
checkArgument(!auth.methods().isEmpty(), "Must specify at least one auth method");
checkArgument(
Ordering.explicit(AuthMethod.API, AuthMethod.LEGACY).isStrictlyOrdered(authMethods),
Ordering.explicit(AuthMethod.API, AuthMethod.LEGACY).isStrictlyOrdered(auth.methods()),
"Auth methods must be unique and strictly in order - API, LEGACY");
checkArgument(
(auth.minimumLevel() != NONE) || (auth.userPolicy() != ADMIN),

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