1
0
mirror of https://github.com/google/nomulus synced 2026-01-31 18:12:21 +00:00

Compare commits

...

24 Commits

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

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

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

* change standardSeconds to standardMinutes in test

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

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

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

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

* Add tests

* small fixes
2023-09-22 11:51:02 -04:00
sarahcaseybot
58c7e3a52c Change __REMOVEDOMAIN__ token to __REMOVE_BULK_PRICING__ (#2152) 2023-09-21 16:03:39 -04:00
Pavlo Tkach
dded258864 Add resources widget front-end (#2151) 2023-09-21 13:59:40 -04:00
Lai Jiang
759143535f Update proxy k8s manifest (#2153)
The beta API is deprecated.

TESTED=deployed the new manifest to alpha. Without the change, deploying
resulted in an error.

<!-- Reviewable:start -->
- - -
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/google/nomulus/2153)
<!-- Reviewable:end -->
2023-09-21 10:53:39 -04:00
Weimin Yu
46fdf2c996 Defend against deserialization-based attacks (#2150)
* Defend against deserialization-based attacks

Added the `SafeObjectInputStream` class that defends attacks using
malformed serialized data, including remote code execution and
denial-of-service attacks.

Started using the new class to handle EPP resource VKeys and
PendingDeposits, which are passed across credential-boundaries: between
TaskQueue and AppEngine server, and between AppEngine server and the RDE
pipeline on GCE. Note that the wireformat of VKeys do not change,
therefore existing tasks sitting in the TaskQueue are not affected.

Also removed an unused class: JaxbFragment.
2023-09-20 16:56:56 -04:00
139 changed files with 4950 additions and 4230 deletions

View File

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

View File

@@ -24,6 +24,10 @@ import SettingsSecurityComponent from './settings/security/security.component';
import { RegistrarGuard } from './registrar/registrar.guard';
import { RegistrarComponent } from './registrar/registrarsTable.component';
import { EmptyRegistrar } from './registrar/emptyRegistrar.component';
import ContactComponent from './settings/contact/contact.component';
import WhoisComponent from './settings/whois/whois.component';
import SecurityComponent from './settings/security/security.component';
import UsersComponent from './settings/users/users.component';
const routes: Routes = [
{ path: '', redirectTo: '/home', pathMatch: 'full' },
@@ -32,7 +36,7 @@ const routes: Routes = [
{ path: 'home', component: HomeComponent, canActivate: [RegistrarGuard] },
{ path: 'tlds', component: TldsComponent, canActivate: [RegistrarGuard] },
{
path: 'settings',
path: SettingsComponent.PATH,
component: SettingsComponent,
children: [
{
@@ -41,32 +45,27 @@ const routes: Routes = [
pathMatch: 'full',
},
{
path: 'contact',
path: ContactComponent.PATH,
component: SettingsContactComponent,
canActivate: [RegistrarGuard],
},
{
path: 'whois',
path: WhoisComponent.PATH,
component: SettingsWhoisComponent,
canActivate: [RegistrarGuard],
},
{
path: 'security',
path: SecurityComponent.PATH,
component: SettingsSecurityComponent,
canActivate: [RegistrarGuard],
},
{
path: 'epp-password',
component: SettingsSecurityComponent,
canActivate: [RegistrarGuard],
},
{
path: 'users',
path: UsersComponent.PATH,
component: SettingsUsersComponent,
canActivate: [RegistrarGuard],
},
{
path: 'registrars',
path: RegistrarComponent.PATH,
component: RegistrarComponent,
},
],

View File

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

View File

@@ -12,20 +12,29 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } 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';
import { NavigationEnd, Router } from '@angular/router';
import { MatSidenav } from '@angular/material/sidenav';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
export class AppComponent implements AfterViewInit {
renderRouter: boolean = true;
@ViewChild('sidenav')
sidenav!: MatSidenav;
constructor(
protected registrarService: RegistrarService,
protected globalLoader: GlobalLoaderService
protected userDataService: UserDataService,
protected globalLoader: GlobalLoaderService,
protected router: Router
) {
registrarService.activeRegistrarIdChange.subscribe(() => {
this.renderRouter = false;
@@ -34,4 +43,12 @@ export class AppComponent {
}, 400);
});
}
ngAfterViewInit() {
this.router.events.subscribe((event) => {
if (event instanceof NavigationEnd) {
this.sidenav.close();
}
});
}
}

View File

@@ -46,6 +46,9 @@ import { EppWidgetComponent } from './home/widgets/epp-widget.component';
import { BillingWidgetComponent } from './home/widgets/billing-widget.component';
import { DomainsWidgetComponent } from './home/widgets/domains-widget.component';
import { SettingsWidgetComponent } from './home/widgets/settings-widget.component';
import { UserDataService } from './shared/services/userData.service';
import WhoisComponent from './settings/whois/whois.component';
import { SnackBarModule } from './snackbar.module';
@NgModule({
declarations: [
@@ -68,6 +71,7 @@ import { SettingsWidgetComponent } from './home/widgets/settings-widget.componen
SettingsWidgetComponent,
TldsComponent,
TldsWidgetComponent,
WhoisComponent,
],
imports: [
AppRoutingModule,
@@ -76,11 +80,13 @@ import { SettingsWidgetComponent } from './home/widgets/settings-widget.componen
FormsModule,
HttpClientModule,
MaterialModule,
SnackBarModule,
],
providers: [
BackendService,
GlobalLoaderService,
RegistrarGuard,
UserDataService,
{
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
useValue: {

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,13 +13,13 @@
Give us a Call
</button>
<p class="secondary-text">
Call Google Registry support at +1 (404) 978 8419
Call Google Registry support at <b>+1 (404) 978 8419</b>
</p>
<button mat-button color="primary" class="console-app__widget-link">
Send us an Email
</button>
<p class="secondary-text">
Email Google Registry at support@google.com
Email Google Registry at <b>support@google.com</b>
</p>
</div>
</div>

View File

@@ -1,13 +1,17 @@
<mat-card>
<mat-card-content>
<div class="console-app__widget">
<div class="console-app__widget_left">
<a
class="console-app__widget_left"
href="{{ userDataService.userData?.technicalDocsUrl }}"
target="_blank"
>
<mat-icon class="console-app__widget-icon">menu_book</mat-icon>
<h1 class="console-app__widget-title">Resources</h1>
<h4 class="secondary-text text-center">
Use Google Drive to view onboarding FAQs, and technical documentation.
</h4>
</div>
</a>
</div>
</mat-card-content>
</mat-card>

View File

@@ -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-resources-widget]',
templateUrl: './resources-widget.component.html',
})
export class ResourcesWidgetComponent {
constructor() {}
constructor(public userDataService: UserDataService) {}
}

View File

@@ -10,11 +10,21 @@
</h4>
</div>
<div class="console-app__widget_right">
<button mat-button color="primary" class="console-app__widget-link">
<button
mat-button
color="primary"
class="console-app__widget-link"
(click)="openContactsPage()"
>
Contact Information
</button>
<p class="secondary-text">Manage Primary, Technical, etc contacts.</p>
<button mat-button color="primary" class="console-app__widget-link">
<button
mat-button
color="primary"
class="console-app__widget-link"
(click)="openSecurityPage()"
>
Security
</button>
<p class="secondary-text">
@@ -28,7 +38,12 @@
User Management
</button>
<p class="secondary-text">Create and manage console user accounts</p>
<button mat-button color="primary" class="console-app__widget-link">
<button
mat-button
color="primary"
class="console-app__widget-link"
(click)="openRegistrarsPage()"
>
Registrar Management
</button>
<p class="secondary-text">Create and manage registrar accounts</p>

View File

@@ -13,11 +13,32 @@
// limitations under the License.
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { RegistrarComponent } from 'src/app/registrar/registrarsTable.component';
import ContactComponent from 'src/app/settings/contact/contact.component';
import SecurityComponent from 'src/app/settings/security/security.component';
import { SettingsComponent } from 'src/app/settings/settings.component';
@Component({
selector: '[app-settings-widget]',
templateUrl: './settings-widget.component.html',
})
export class SettingsWidgetComponent {
constructor() {}
constructor(private router: Router) {}
openRegistrarsPage() {
this.navigate(RegistrarComponent.PATH);
}
openSecurityPage() {
this.navigate(SecurityComponent.PATH);
}
openContactsPage() {
this.navigate(ContactComponent.PATH);
}
private navigate(route: string) {
this.router.navigate([SettingsComponent.PATH, route]);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,14 +13,16 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { BackendService } from '../shared/services/backend.service';
import { Observable, Subject, tap } from 'rxjs';
import { BackendService } from '../shared/services/backend.service';
import {
GlobalLoader,
GlobalLoaderService,
} from '../shared/services/globalLoader.service';
import { MatSnackBar } from '@angular/material/snack-bar';
interface Address {
export interface Address {
street?: string[];
city?: string;
countryCode?: string;
@@ -30,16 +32,20 @@ interface Address {
export interface Registrar {
allowedTlds?: string[];
ipAddressAllowList?: string[];
emailAddress?: string;
billingAccountMap?: object;
driveFolderId?: string;
emailAddress?: string;
faxNumber?: string;
ianaIdentifier?: number;
icannReferralEmail?: string;
ipAddressAllowList?: string[];
localizedAddress?: Address;
phoneNumber?: string;
registrarId: string;
registrarName: string;
registryLockAllowed?: boolean;
url?: string;
whoisServer?: string;
}
@Injectable({
@@ -52,7 +58,8 @@ export class RegistrarService implements GlobalLoader {
constructor(
private backend: BackendService,
private globalLoader: GlobalLoaderService
private globalLoader: GlobalLoaderService,
private _snackBar: MatSnackBar
) {
this.loadRegistrars().subscribe((r) => {
this.globalLoader.stopGlobalLoader(this);
@@ -82,6 +89,6 @@ export class RegistrarService implements GlobalLoader {
}
loadingTimeout() {
// TODO: Decide what to do when timeout happens
this._snackBar.open('Timeout loading registrars');
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);
},
});
}
@@ -143,6 +141,8 @@ export class ContactDetailsDialogComponent {
styleUrls: ['./contact.component.scss'],
})
export default class ContactComponent {
public static PATH = 'contact';
loading: boolean = false;
constructor(
private dialog: MatDialog,
@@ -173,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);
},
});
}

View File

@@ -29,6 +29,8 @@ import { RegistrarService } from 'src/app/registrar/registrar.service';
providers: [SecurityService],
})
export default class SecurityComponent {
public static PATH = 'security';
loading: boolean = false;
inEdit: boolean = false;
dataSource: SecuritySettings = {};
@@ -62,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();

View File

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

View File

@@ -20,4 +20,6 @@ import { Component, ViewEncapsulation } from '@angular/core';
styleUrls: ['./settings.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export class SettingsComponent {}
export class SettingsComponent {
public static PATH = 'settings';
}

View File

@@ -19,4 +19,6 @@ import { Component } from '@angular/core';
templateUrl: './users.component.html',
styleUrls: ['./users.component.scss'],
})
export default class UsersComponent {}
export default class UsersComponent {
public static PATH = 'users';
}

View File

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

View File

@@ -11,3 +11,49 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
.settings-whois {
margin-top: 1.5rem;
&__section {
display: flex;
flex-wrap: wrap;
margin-bottom: 10px;
min-width: 400px;
}
&__section-address {
display: flex;
flex-wrap: wrap;
margin-bottom: 5px;
min-width: 400px;
width: 50%;
max-width: 50%;
}
&__section-description {
display: inline-block;
margin-block-start: 1em;
width: 160px;
}
&__section-form {
display: inline-block;
width: 70%;
mat-form-field {
width: 90%;
min-width: 300px;
}
input:disabled {
border: 0;
}
}
&__loading {
margin: 2rem 0;
}
&__actions {
margin-top: 50px;
display: flex;
justify-content: flex-end;
margin-right: 50px;
button {
margin-left: 20px;
}
}
}

View File

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

View File

@@ -0,0 +1,45 @@
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { switchMap } from 'rxjs';
import { Address, RegistrarService } from 'src/app/registrar/registrar.service';
import { BackendService } from 'src/app/shared/services/backend.service';
export interface WhoisRegistrarFields {
ianaIdentifier?: number;
icannReferralEmail?: string;
localizedAddress?: Address;
registrarId?: string;
url?: string;
whoisServer?: string;
}
@Injectable()
export class WhoisService {
whoisRegistrarFields: WhoisRegistrarFields = {};
constructor(
private backend: BackendService,
private registrarService: RegistrarService
) {}
saveChanges(newWhoisRegistrarFields: WhoisRegistrarFields) {
return this.backend.postWhoisRegistrarFields(newWhoisRegistrarFields).pipe(
switchMap(() => {
return this.registrarService.loadRegistrars();
})
);
}
}

View File

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

View File

@@ -0,0 +1,54 @@
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { Observable, tap } from 'rxjs';
import { BackendService } from './backend.service';
import { MatSnackBar } from '@angular/material/snack-bar';
import { GlobalLoader, GlobalLoaderService } from './globalLoader.service';
export interface UserData {
isAdmin: boolean;
globalRole: string;
technicalDocsUrl: string;
}
@Injectable({
providedIn: 'root',
})
export class UserDataService implements GlobalLoader {
public userData?: UserData;
constructor(
private backend: BackendService,
protected globalLoader: GlobalLoaderService,
private _snackBar: MatSnackBar
) {
this.getUserData().subscribe(() => {
this.globalLoader.stopGlobalLoader(this);
});
this.globalLoader.startGlobalLoader(this);
}
getUserData(): Observable<UserData> {
return this.backend.getUserData().pipe(
tap((userData: UserData) => {
this.userData = userData;
})
);
}
loadingTimeout() {
this._snackBar.open('Timeout loading user data');
}
}

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 {}

View File

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

View File

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

View File

@@ -27,6 +27,10 @@ import static google.registry.beam.rde.RdePipeline.TupleTags.REVISION_ID;
import static google.registry.beam.rde.RdePipeline.TupleTags.SUPERORDINATE_DOMAINS;
import static google.registry.model.reporting.HistoryEntryDao.RESOURCE_TYPES_TO_HISTORY_TYPES;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.SafeSerializationUtils.safeDeserializeCollection;
import static google.registry.util.SafeSerializationUtils.serializeCollection;
import static google.registry.util.SerializeUtils.decodeBase64;
import static google.registry.util.SerializeUtils.encodeBase64;
import static org.apache.beam.sdk.values.TypeDescriptors.kvs;
import com.google.common.collect.ImmutableList;
@@ -65,11 +69,7 @@ import google.registry.rde.PendingDeposit.PendingDepositCoder;
import google.registry.rde.RdeMarshaller;
import google.registry.util.UtilsModule;
import google.registry.xml.ValidationMode;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.HashSet;
import javax.inject.Inject;
@@ -658,14 +658,8 @@ public class RdePipeline implements Serializable {
*/
@SuppressWarnings("unchecked")
static ImmutableSet<PendingDeposit> decodePendingDeposits(String encodedPendingDeposits) {
try (ObjectInputStream ois =
new ObjectInputStream(
new ByteArrayInputStream(
BaseEncoding.base64Url().omitPadding().decode(encodedPendingDeposits)))) {
return (ImmutableSet<PendingDeposit>) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
throw new IllegalArgumentException("Unable to parse encoded pending deposit map.", e);
}
return ImmutableSet.copyOf(
safeDeserializeCollection(PendingDeposit.class, decodeBase64(encodedPendingDeposits)));
}
/**
@@ -674,12 +668,7 @@ public class RdePipeline implements Serializable {
*/
public static String encodePendingDeposits(ImmutableSet<PendingDeposit> pendingDeposits)
throws IOException {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(pendingDeposits);
oos.flush();
return BaseEncoding.base64Url().omitPadding().encode(baos.toByteArray());
}
return encodeBase64(serializeCollection(pendingDeposits));
}
public static void main(String[] args) throws IOException, ClassNotFoundException {

View File

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

View File

@@ -58,9 +58,6 @@ 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;
}
@@ -208,6 +205,7 @@ public class RegistryConfigSettings {
public static class Misc {
public String sheetExportId;
public boolean isEmailSendingEnabled;
public int emailThrottleSeconds;
public String alertRecipientEmailAddress;
// TODO(b/279671974): remove below field after migration
public String newAlertRecipientEmailAddress;

View File

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

View File

@@ -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

View File

@@ -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;

View File

@@ -53,8 +53,8 @@ import google.registry.flows.custom.DomainRenewFlowCustomLogic.BeforeResponseRet
import google.registry.flows.custom.DomainRenewFlowCustomLogic.BeforeSaveParameters;
import google.registry.flows.custom.EntityChanges;
import google.registry.flows.domain.token.AllocationTokenFlowUtils;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.MissingRemoveDomainTokenOnBulkPricingDomainException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.RemoveDomainTokenOnNonBulkPricingDomainException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.MissingRemoveBulkPricingTokenOnBulkPricingDomainException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.RemoveBulkPricingTokenOnNonBulkPricingDomainException;
import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingEvent;
@@ -121,8 +121,8 @@ import org.joda.time.Duration;
* @error {@link DomainFlowUtils.RegistrarMustBeActiveForThisOperationException}
* @error {@link DomainFlowUtils.UnsupportedFeeAttributeException}
* @error {@link DomainRenewFlow.IncorrectCurrentExpirationDateException}
* @error {@link MissingRemoveDomainTokenOnBulkPricingDomainException}
* @error {@link RemoveDomainTokenOnNonBulkPricingDomainException}
* @error {@link MissingRemoveBulkPricingTokenOnBulkPricingDomainException}
* @error {@link RemoveBulkPricingTokenOnNonBulkPricingDomainException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException}
* @error {@link
@@ -328,7 +328,7 @@ public final class DomainRenewFlow implements MutatingFlow {
checkHasBillingAccount(registrarId, existingDomain.getTld());
}
verifyUnitIsYears(command.getPeriod());
// We only allow __REMOVEDOMAIN__ token on bulk pricing domains for now
// We only allow __REMOVE_BULK_PRICING__ token on bulk pricing domains for now
verifyTokenAllowedOnDomain(existingDomain, allocationToken);
// If the date they specify doesn't match the expiration, fail. (This is an idempotence check).
if (!command.getCurrentExpirationDate().equals(

View File

@@ -243,21 +243,21 @@ public class AllocationTokenFlowUtils {
Domain domain, Optional<AllocationToken> allocationToken) throws EppException {
boolean domainHasBulkToken = domain.getCurrentBulkToken().isPresent();
boolean hasRemoveDomainToken =
boolean hasRemoveBulkPricingToken =
allocationToken.isPresent()
&& TokenBehavior.REMOVE_DOMAIN.equals(allocationToken.get().getTokenBehavior());
&& TokenBehavior.REMOVE_BULK_PRICING.equals(allocationToken.get().getTokenBehavior());
if (hasRemoveDomainToken && !domainHasBulkToken) {
throw new RemoveDomainTokenOnNonBulkPricingDomainException();
} else if (!hasRemoveDomainToken && domainHasBulkToken) {
throw new MissingRemoveDomainTokenOnBulkPricingDomainException();
if (hasRemoveBulkPricingToken && !domainHasBulkToken) {
throw new RemoveBulkPricingTokenOnNonBulkPricingDomainException();
} else if (!hasRemoveBulkPricingToken && domainHasBulkToken) {
throw new MissingRemoveBulkPricingTokenOnBulkPricingDomainException();
}
}
public static Domain maybeApplyBulkPricingRemovalToken(
Domain domain, Optional<AllocationToken> allocationToken) {
if (!allocationToken.isPresent()
|| !TokenBehavior.REMOVE_DOMAIN.equals(allocationToken.get().getTokenBehavior())) {
|| !TokenBehavior.REMOVE_BULK_PRICING.equals(allocationToken.get().getTokenBehavior())) {
return domain;
}
@@ -338,19 +338,19 @@ public class AllocationTokenFlowUtils {
}
}
/** The __REMOVEDOMAIN__ token is missing on a bulk pricing domain command */
public static class MissingRemoveDomainTokenOnBulkPricingDomainException
/** The __REMOVE_BULK_PRICING__ token is missing on a bulk pricing domain command */
public static class MissingRemoveBulkPricingTokenOnBulkPricingDomainException
extends AssociationProhibitsOperationException {
MissingRemoveDomainTokenOnBulkPricingDomainException() {
MissingRemoveBulkPricingTokenOnBulkPricingDomainException() {
super("Domains that are inside bulk pricing cannot be explicitly renewed or transferred");
}
}
/** The __REMOVEDOMAIN__ token is not allowed on non bulk pricing domains */
public static class RemoveDomainTokenOnNonBulkPricingDomainException
/** The __REMOVE_BULK_PRICING__ token is not allowed on non bulk pricing domains */
public static class RemoveBulkPricingTokenOnNonBulkPricingDomainException
extends AssociationProhibitsOperationException {
RemoveDomainTokenOnNonBulkPricingDomainException() {
super("__REMOVEDOMAIN__ token is not allowed on non bulk pricing domains");
RemoveBulkPricingTokenOnNonBulkPricingDomainException() {
super("__REMOVE_BULK_PRICING__ token is not allowed on non bulk pricing domains");
}
}
}

View File

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

View File

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

View File

@@ -77,10 +77,10 @@ import org.joda.time.DateTime;
public class AllocationToken extends UpdateAutoTimestampEntity implements Buildable {
private static final long serialVersionUID = -3954475393220876903L;
private static final String REMOVE_DOMAIN = "__REMOVEDOMAIN__";
private static final String REMOVE_BULK_PRICING = "__REMOVE_BULK_PRICING__";
private static final ImmutableMap<String, TokenBehavior> STATIC_TOKEN_BEHAVIORS =
ImmutableMap.of(REMOVE_DOMAIN, TokenBehavior.REMOVE_DOMAIN);
ImmutableMap.of(REMOVE_BULK_PRICING, TokenBehavior.REMOVE_BULK_PRICING);
// Promotions should only move forward, and ENDED / CANCELLED are terminal states.
private static final ImmutableMultimap<TokenStatus, TokenStatus> VALID_TOKEN_STATUS_TRANSITIONS =
@@ -91,10 +91,10 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda
private static final ImmutableMap<String, AllocationToken> BEHAVIORAL_TOKENS =
ImmutableMap.of(
REMOVE_DOMAIN,
REMOVE_BULK_PRICING,
new AllocationToken.Builder()
.setTokenType(TokenType.UNLIMITED_USE)
.setToken(REMOVE_DOMAIN)
.setToken(REMOVE_BULK_PRICING)
.build());
public static Optional<AllocationToken> maybeGetStaticTokenInstance(String name) {
@@ -142,10 +142,10 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda
/** No special behavior */
DEFAULT,
/**
* REMOVE_DOMAIN triggers domain removal from a bulk pricing package, bypasses DEFAULT token
* validations.
* REMOVE_BULK_PRICING triggers domain removal from a bulk pricing package, bypasses DEFAULT
* token validations.
*/
REMOVE_DOMAIN
REMOVE_BULK_PRICING
}
/** The status of this token with regard to any potential promotion. */

View File

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

View File

@@ -16,6 +16,8 @@ package google.registry.persistence;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import static google.registry.util.SafeSerializationUtils.safeDeserialize;
import static google.registry.util.SerializeUtils.decodeBase64;
import static java.util.function.Function.identity;
import com.google.common.base.Joiner;
@@ -97,7 +99,7 @@ public class VKey<T> extends ImmutableObject implements Serializable {
throw new IllegalArgumentException(
String.format("\"%s\" missing from the string: %s", LOOKUP_KEY, keyString));
}
return VKey.create(classType, SerializeUtils.parse(Serializable.class, kvs.get(LOOKUP_KEY)));
return VKey.create(classType, safeDeserialize(decodeBase64(kvs.get(LOOKUP_KEY))));
}
/** Returns the type of the entity. */

View File

@@ -14,18 +14,23 @@
package google.registry.rde;
import static com.google.common.base.Preconditions.checkState;
import com.google.auto.value.AutoValue;
import google.registry.model.common.Cursor.CursorType;
import google.registry.model.rde.RdeMode;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamException;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.Optional;
import javax.annotation.Nullable;
import org.apache.beam.sdk.coders.AtomicCoder;
import org.apache.beam.sdk.coders.BooleanCoder;
import org.apache.beam.sdk.coders.NullableCoder;
import org.apache.beam.sdk.coders.SerializableCoder;
import org.apache.beam.sdk.coders.StringUtf8Coder;
import org.apache.beam.sdk.coders.VarIntCoder;
import org.joda.time.DateTime;
@@ -35,6 +40,12 @@ import org.joda.time.Duration;
* Container representing a single RDE or BRDA XML escrow deposit that needs to be created.
*
* <p>There are some {@code @Nullable} fields here because Optionals aren't Serializable.
*
* <p>Note that this class is serialized in two ways: by Beam pipelines using custom serialization
* mechanism and the {@code Coder} API, and by Java serialization when passed as command-line
* arguments (see {@code RdePipeline#decodePendingDeposits}). The latter requires safe
* deserialization because the data crosses credential boundaries (See {@code
* SafeObjectInputStream}).
*/
@AutoValue
public abstract class PendingDeposit implements Serializable {
@@ -95,11 +106,61 @@ public abstract class PendingDeposit implements Serializable {
PendingDeposit() {}
/**
* Specifies that {@link SerializedForm} be used for {@code SafeObjectInputStream}-compatible
* custom-serialization of {@link AutoValue_PendingDeposit the AutoValue implementation class}.
*
* <p>This method is package-protected so that the AutoValue implementation class inherits this
* behavior.
*
* <p>This method leverages {@link PendingDepositCoder} to serializes an instance. However, it is
* not invoked in Beam pipelines.
*/
Object writeReplace() throws ObjectStreamException {
return new SerializedForm(this);
}
/**
* Proxy for custom-serialization of {@link PendingDeposit}. This is necessary because the actual
* class to be (de)serialized is the generated AutoValue implementation. See also {@link
* #writeReplace}.
*
* <p>This class leverages {@link PendingDepositCoder} to safely deserializes an instance.
* However, it is not used in Beam pipelines.
*/
private static class SerializedForm implements Serializable {
private static final long serialVersionUID = 3141095605225904433L;
private PendingDeposit value;
private SerializedForm(PendingDeposit value) {
this.value = value;
}
private void writeObject(ObjectOutputStream os) throws IOException {
checkState(value != null, "Non-null value expected for serialization.");
PendingDepositCoder.INSTANCE.encode(value, os);
}
private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException {
checkState(value == null, "Non-null value unexpected for deserialization.");
this.value = PendingDepositCoder.INSTANCE.decode(is);
}
@SuppressWarnings("unused")
private Object readResolve() throws ObjectStreamException {
return this.value;
}
}
/**
* A deterministic coder for {@link PendingDeposit} used during a GroupBy transform.
*
* <p>We cannot use a {@link SerializableCoder} directly because it does not guarantee
* determinism, which is required by GroupBy.
* <p>We cannot use a {@code SerializableCoder} directly for two reasons: the default
* serialization does not guarantee determinism, which is required by GroupBy in Beam; and the
* default deserialization is not robust against deserialization-based attacks (See {@code
* SafeObjectInputStream} for more information).
*/
public static class PendingDepositCoder extends AtomicCoder<PendingDeposit> {
@@ -117,10 +178,15 @@ public abstract class PendingDeposit implements Serializable {
public void encode(PendingDeposit value, OutputStream outStream) throws IOException {
BooleanCoder.of().encode(value.manual(), outStream);
StringUtf8Coder.of().encode(value.tld(), outStream);
SerializableCoder.of(DateTime.class).encode(value.watermark(), outStream);
SerializableCoder.of(RdeMode.class).encode(value.mode(), outStream);
NullableCoder.of(SerializableCoder.of(CursorType.class)).encode(value.cursor(), outStream);
NullableCoder.of(SerializableCoder.of(Duration.class)).encode(value.interval(), outStream);
StringUtf8Coder.of().encode(value.watermark().toString(), outStream);
StringUtf8Coder.of().encode(value.mode().name(), outStream);
NullableCoder.of(StringUtf8Coder.of())
.encode(
Optional.ofNullable(value.cursor()).map(CursorType::name).orElse(null), outStream);
NullableCoder.of(StringUtf8Coder.of())
.encode(
Optional.ofNullable(value.interval()).map(Duration::toString).orElse(null),
outStream);
NullableCoder.of(StringUtf8Coder.of()).encode(value.directoryWithTrailingSlash(), outStream);
NullableCoder.of(VarIntCoder.of()).encode(value.revision(), outStream);
}
@@ -130,10 +196,14 @@ public abstract class PendingDeposit implements Serializable {
return new AutoValue_PendingDeposit(
BooleanCoder.of().decode(inStream),
StringUtf8Coder.of().decode(inStream),
SerializableCoder.of(DateTime.class).decode(inStream),
SerializableCoder.of(RdeMode.class).decode(inStream),
NullableCoder.of(SerializableCoder.of(CursorType.class)).decode(inStream),
NullableCoder.of(SerializableCoder.of(Duration.class)).decode(inStream),
DateTime.parse(StringUtf8Coder.of().decode(inStream)),
RdeMode.valueOf(StringUtf8Coder.of().decode(inStream)),
Optional.ofNullable(NullableCoder.of(StringUtf8Coder.of()).decode(inStream))
.map(CursorType::valueOf)
.orElse(null),
Optional.ofNullable(NullableCoder.of(StringUtf8Coder.of()).decode(inStream))
.map(Duration::parse)
.orElse(null),
NullableCoder.of(StringUtf8Coder.of()).decode(inStream),
NullableCoder.of(VarIntCoder.of()).decode(inStream));
}

View File

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

View File

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

View File

@@ -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,9 @@ 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");
} finally {
requestMetrics.record(
new Duration(startTime, clock.nowUtc()),

View File

@@ -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;
@@ -48,30 +46,18 @@ public enum Auth {
* 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 only the app itself (via service accounts) or admins 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.
*/
AUTH_API_PUBLIC(ImmutableList.of(AuthMethod.API), AuthLevel.APP, UserPolicy.PUBLIC),
/**
* Allows only 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);

View File

@@ -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,12 +55,6 @@ public class AuthModule {
@Qualifier
@interface RegularOidc {}
/** Provides the OAuthService instance. */
@Provides
OAuthService provideOauthService() {
return OAuthServiceFactory.getOAuthService();
}
@Provides
@IapOidc
@Singleton
@@ -98,11 +83,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());
}

View File

@@ -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);
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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()));
}

View File

@@ -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)));
}
}

View File

@@ -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);
}
}

View File

@@ -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;
@@ -97,12 +95,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(
@@ -153,15 +150,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>
*/

View File

@@ -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),

View File

@@ -22,6 +22,8 @@ import java.util.Optional;
@AutoValue
public abstract class UserAuthInfo {
public abstract Optional<google.registry.model.console.User> consoleUser();
/** User object from the AppEngine Users API. */
public abstract Optional<User> appEngineUser();
@@ -34,11 +36,6 @@ public abstract class UserAuthInfo {
*/
public abstract boolean isUserAdmin();
public abstract Optional<google.registry.model.console.User> consoleUser();
/** Used by the OAuth authentication mechanism (only) to return information about the session. */
public abstract Optional<OAuthTokenInfo> oauthTokenInfo();
public String getEmailAddress() {
return appEngineUser()
.map(User::getEmail)
@@ -51,20 +48,12 @@ public abstract class UserAuthInfo {
.orElseGet(() -> consoleUser().get().getEmailAddress());
}
public static UserAuthInfo create(
User user, boolean isUserAdmin) {
return new AutoValue_UserAuthInfo(
Optional.of(user), isUserAdmin, Optional.empty(), Optional.empty());
}
public static UserAuthInfo create(
User user, boolean isUserAdmin, OAuthTokenInfo oauthTokenInfo) {
return new AutoValue_UserAuthInfo(
Optional.of(user), isUserAdmin, Optional.empty(), Optional.of(oauthTokenInfo));
public static UserAuthInfo create(User user, boolean isUserAdmin) {
return new AutoValue_UserAuthInfo(Optional.empty(), Optional.of(user), isUserAdmin);
}
public static UserAuthInfo create(google.registry.model.console.User user) {
return new AutoValue_UserAuthInfo(
Optional.empty(), user.getUserRoles().isAdmin(), Optional.of(user), Optional.empty());
Optional.of(user), Optional.empty(), user.getUserRoles().isAdmin());
}
}

View File

@@ -63,6 +63,14 @@ public class ConfigureTldCommand extends MutatingCommand {
required = true)
Path inputFile;
@Parameter(
names = {"-b", "--breakglass"},
description =
"Sets the breakglass field on the TLD to true, preventing Cloud Build from overwriting"
+ " these new changes until the TLD configuration file stored internally matches the"
+ " configuration in the database.")
boolean breakglass;
@Inject ObjectMapper mapper;
@Inject
@@ -70,13 +78,13 @@ public class ConfigureTldCommand extends MutatingCommand {
Set<String> validDnsWriterNames;
/** Indicates if the passed in file contains new changes to the TLD */
boolean newDiff = false;
boolean newDiff = true;
// TODO(sarahbot@): Add a breakglass setting to this tool to indicate when a TLD has been modified
// outside of source control
// TODO(sarahbot@): Add a check for diffs between passed in file and current TLD and exit if there
// is no diff. Treat nulls and empty sets as the same value.
/**
* Indicates if the existing TLD is currently in breakglass mode and should not be modified unless
* the breakglass flag is used
*/
boolean oldTldInBreakglass = false;
@Override
protected void init() throws Exception {
@@ -86,20 +94,47 @@ public class ConfigureTldCommand extends MutatingCommand {
checkForMissingFields(tldData);
Tld oldTld = getTlds().contains(name) ? Tld.get(name) : null;
Tld newTld = mapper.readValue(inputFile.toFile(), Tld.class);
if (oldTld != null && oldTld.equalYaml(newTld)) {
if (oldTld != null) {
oldTldInBreakglass = oldTld.getBreakglassMode();
newDiff = !oldTld.equalYaml(newTld);
}
if (!newDiff && !oldTldInBreakglass) {
// Don't construct a new object if there is no new diff
return;
}
newDiff = true;
if (oldTldInBreakglass && !breakglass) {
checkArgument(
!newDiff,
"Changes can not be applied since TLD is in breakglass mode but the breakglass flag was"
+ " not used");
// if there are no new diffs, then the YAML file has caught up to the database and the
// breakglass mode should be removed
logger.atInfo().log("Breakglass mode removed from TLD: %s", name);
}
checkPremiumList(newTld);
checkDnsWriters(newTld);
checkCurrency(newTld);
// Set the new TLD to breakglass mode if breakglass flag was used
if (breakglass) {
newTld = newTld.asBuilder().setBreakglassMode(true).build();
}
stageEntityChange(oldTld, newTld);
}
@Override
protected boolean dontRunCommand() {
if (!newDiff) {
if (oldTldInBreakglass && !breakglass) {
// Run command to remove breakglass mode
return false;
}
logger.atInfo().log("TLD YAML file contains no new changes");
checkArgument(
!breakglass || oldTldInBreakglass,
"Breakglass mode can only be set when making new changes to a TLD configuration");
return true;
}
return false;
@@ -141,7 +176,9 @@ public class ConfigureTldCommand extends MutatingCommand {
private void checkPremiumList(Tld newTld) {
Optional<String> premiumListName = newTld.getPremiumListName();
if (!premiumListName.isPresent()) return;
if (!premiumListName.isPresent()) {
return;
}
Optional<PremiumList> premiumList = PremiumListDao.getLatestRevision(premiumListName.get());
checkArgument(
premiumList.isPresent(),

View File

@@ -43,7 +43,8 @@ final class RegistryCli implements CommandRunner {
// The environment parameter is parsed twice: once here, and once with {@link
// RegistryToolEnvironment#parseFromArgs} in the {@link RegistryTool#main} function.
//
// The flag names must be in sync between the two, and also - this is ugly and we should feel bad.
// The flag names must be in sync between the two, and also - this is ugly, and we should feel
// bad.
@Parameter(
names = {"-e", "--environment"},
description = "Sets the default environment to run the command.")
@@ -55,22 +56,21 @@ final class RegistryCli implements CommandRunner {
private boolean showAllCommands;
@Parameter(
names = {"--credential"},
names = "--credential",
description =
"Name of a JSON file containing credential information used by the tool. "
+ "If not set, credentials saved by running `nomulus login' will be used.")
private String credentialJson = null;
@Parameter(
names = {"--sql_access_info"},
names = "--sql_access_info",
description =
"Name of a file containing space-separated SQL access info used when deploying "
+ "Beam pipelines")
private String sqlAccessInfoFile = null;
// Do not make this final - compile-time constant inlining may interfere with JCommander.
@ParametersDelegate
private LoggingParameters loggingParams = new LoggingParameters();
@ParametersDelegate private LoggingParameters loggingParams = new LoggingParameters();
RegistryToolComponent component;
@@ -105,8 +105,8 @@ final class RegistryCli implements CommandRunner {
jcommander.setProgramName(programName);
// Create all command instances. It would be preferrable to do this in the constructor, but
// JCommander mutates the command instances and doesn't reset them so we have to do it for every
// run.
// JCommander mutates the command instances and doesn't reset them, so we have to do it for
// every run.
try {
for (Map.Entry<String, ? extends Class<? extends Command>> entry : commands.entrySet()) {
Command command = entry.getValue().getDeclaredConstructor().newInstance();
@@ -169,7 +169,7 @@ final class RegistryCli implements CommandRunner {
Command command =
(Command)
Iterables.getOnlyElement(jcommander.getCommands().get(parsedCommand).getObjects());
loggingParams.configureLogging(); // Must be called after parameters are parsed.
loggingParams.configureLogging(); // Must be called after parameters are parsed.
try {
runCommand(command);

View File

@@ -123,6 +123,7 @@ interface RegistryToolComponent {
void inject(GetDomainCommand command);
void inject(GetHostCommand command);
void inject(GetKeyringSecretCommand command);
void inject(GetSqlCredentialCommand command);

View File

@@ -14,7 +14,7 @@
package google.registry.tools;
import static com.google.common.net.HttpHeaders.PROXY_AUTHORIZATION;
import static com.google.common.net.HttpHeaders.AUTHORIZATION;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.javanet.NetHttpTransport;
@@ -54,13 +54,11 @@ final class RequestFactoryModule {
return new NetHttpTransport()
.createRequestFactory(
request -> {
// Use the standard credential initializer to set the Authorization header
credentialsBundle.getHttpRequestInitializer().initialize(request);
// Set OIDC token as the alternative bearer token.
// Set OIDC token as the bearer token.
request
.getHeaders()
.set(
PROXY_AUTHORIZATION,
AUTHORIZATION,
"Bearer "
+ OidcTokenUtils.createOidcToken(credentialsBundle, oauthClientId));
// GAE request times out after 10 min, so here we set the timeout to 10 min. This is

View File

@@ -51,7 +51,7 @@ import org.joda.time.DateTime;
service = Action.Service.PUBAPI,
path = "/_dr/whois",
method = POST,
auth = Auth.AUTH_API_PUBLIC)
auth = Auth.AUTH_API_ADMIN)
public class WhoisAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();

View File

@@ -1,105 +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.xjc;
import static java.nio.charset.StandardCharsets.UTF_8;
import google.registry.xml.XmlException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
/**
* JAXB element wrapper for java object serialization.
*
* Instances of {@link JaxbFragment} wrap a non-serializable JAXB element instance, and provide
* hooks into the java object serialization process that allow the elements to be safely
* marshalled and unmarshalled using {@link ObjectOutputStream} and {@link ObjectInputStream},
* respectively.
*/
public class JaxbFragment<T> implements Serializable {
private static final long serialVersionUID = 5651243983008818813L;
private T instance;
/** Stores a JAXB element in a {@link JaxbFragment} */
public static <T> JaxbFragment<T> create(T object) {
JaxbFragment<T> fragment = new JaxbFragment<>();
fragment.instance = object;
return fragment;
}
/** Serializes a JAXB element into xml bytes. */
private static <T> byte[] freezeInstance(T instance) throws IOException {
try {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
XjcXmlTransformer.marshalLenient(instance, bout, UTF_8);
return bout.toByteArray();
} catch (XmlException e) {
throw new IOException(e);
}
}
/** Deserializes a JAXB element from xml bytes. */
private static <T> T unfreezeInstance(byte[] instanceData, Class<T> instanceType)
throws IOException {
try {
ByteArrayInputStream bin = new ByteArrayInputStream(instanceData);
return XjcXmlTransformer.unmarshal(instanceType, bin);
} catch (XmlException e) {
throw new IOException(e);
}
}
/**
* Retrieves the JAXB element that is wrapped by this fragment.
*/
public T getInstance() {
return instance;
}
@Override
public String toString() {
try {
return new String(freezeInstance(instance), UTF_8);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void writeObject(ObjectOutputStream out) throws IOException {
// write instanceType, then instanceData
out.writeObject(instance.getClass());
out.writeObject(freezeInstance(instance));
}
@SuppressWarnings("unchecked")
private void readObject(ObjectInputStream in) throws IOException {
// read instanceType, then instanceData
Class<T> instanceType;
byte[] instanceData;
try {
instanceType = (Class<T>) in.readObject();
instanceData = (byte[]) in.readObject();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
instance = unfreezeInstance(instanceData, instanceType);
}
}

View File

@@ -77,8 +77,8 @@ import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTok
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.MissingRemoveDomainTokenOnBulkPricingDomainException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.RemoveDomainTokenOnNonBulkPricingDomainException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.MissingRemoveBulkPricingTokenOnBulkPricingDomainException;
import google.registry.flows.domain.token.AllocationTokenFlowUtils.RemoveBulkPricingTokenOnNonBulkPricingDomainException;
import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException;
import google.registry.model.billing.BillingBase.Flag;
import google.registry.model.billing.BillingBase.Reason;
@@ -1276,12 +1276,13 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2", "TOKEN", "token"));
EppException thrown =
assertThrows(MissingRemoveDomainTokenOnBulkPricingDomainException.class, this::runFlow);
assertThrows(
MissingRemoveBulkPricingTokenOnBulkPricingDomainException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailsToRenewBulkPricingDomainNoRemoveDomainToken() throws Exception {
void testFailsToRenewBulkPricingDomainNoRemoveBulkPricingToken() throws Exception {
AllocationToken token =
persistResource(
new AllocationToken.Builder()
@@ -1299,25 +1300,26 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
setEppInput("domain_renew.xml", ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "5"));
EppException thrown =
assertThrows(MissingRemoveDomainTokenOnBulkPricingDomainException.class, this::runFlow);
assertThrows(
MissingRemoveBulkPricingTokenOnBulkPricingDomainException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailsToRenewNonBulkPricingDomainWithRemoveDomainToken() throws Exception {
void testFailsToRenewNonBulkPricingDomainWithRemoveBulkPricingToken() throws Exception {
persistDomain();
setEppInput(
"domain_renew_allocationtoken.xml",
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2", "TOKEN", "__REMOVEDOMAIN__"));
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2", "TOKEN", "__REMOVE_BULK_PRICING__"));
EppException thrown =
assertThrows(RemoveDomainTokenOnNonBulkPricingDomainException.class, this::runFlow);
assertThrows(RemoveBulkPricingTokenOnNonBulkPricingDomainException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccesfullyAppliesRemoveDomainToken() throws Exception {
void testSuccesfullyAppliesRemoveBulkPricingToken() throws Exception {
AllocationToken token =
persistResource(
new AllocationToken.Builder()
@@ -1333,7 +1335,7 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
reloadResourceByForeignKey().asBuilder().setCurrentBulkToken(token.createVKey()).build());
setEppInput(
"domain_renew_allocationtoken.xml",
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2", "TOKEN", "__REMOVEDOMAIN__"));
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2", "TOKEN", "__REMOVE_BULK_PRICING__"));
doSuccessfulTest(
"domain_renew_response.xml",
@@ -1347,7 +1349,7 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
}
@Test
void testDryRunRemoveDomainToken() throws Exception {
void testDryRunRemoveBulkPricingToken() throws Exception {
AllocationToken token =
persistResource(
new AllocationToken.Builder()
@@ -1364,7 +1366,7 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
setEppInput(
"domain_renew_allocationtoken.xml",
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2", "TOKEN", "__REMOVEDOMAIN__"));
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2", "TOKEN", "__REMOVE_BULK_PRICING__"));
dryRunFlowAssertResponse(
loadFile(

View File

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

View File

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

View File

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

View File

@@ -28,7 +28,6 @@ import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.request.Actions;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.AuthSettings.AuthLevel;
import google.registry.request.auth.UserAuthInfo;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeResponse;
@@ -48,13 +47,11 @@ abstract class RdapActionBaseTestCase<A extends RdapActionBase> {
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
protected static final AuthResult AUTH_RESULT =
AuthResult.create(
AuthLevel.USER,
AuthResult.createUser(
UserAuthInfo.create(new User("rdap.user@user.com", "gmail.com", "12345"), false));
protected static final AuthResult AUTH_RESULT_ADMIN =
AuthResult.create(
AuthLevel.USER,
AuthResult.createUser(
UserAuthInfo.create(new User("rdap.admin@google.com", "gmail.com", "12345"), true));
protected FakeResponse response = new FakeResponse();

View File

@@ -0,0 +1,59 @@
// 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.rde;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.common.Cursor.CursorType.RDE_STAGING;
import static google.registry.model.rde.RdeMode.FULL;
import static google.registry.util.SafeSerializationUtils.safeDeserialize;
import static google.registry.util.SerializeUtils.deserialize;
import static google.registry.util.SerializeUtils.serialize;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link PendingDeposit}. */
public class PendingDepositTest {
private final DateTime now = DateTime.parse("2000-01-01TZ");
PendingDeposit pendingDeposit =
PendingDeposit.create("soy", now, FULL, RDE_STAGING, Duration.standardDays(1));
PendingDeposit manualPendingDeposit =
PendingDeposit.createInManualOperation("soy", now, FULL, "/", null);
@Test
void deserialize_normalDeposit_success() {
assertThat(deserialize(PendingDeposit.class, serialize(pendingDeposit)))
.isEqualTo(pendingDeposit);
}
@Test
void deserialize_manualDeposit_success() {
assertThat(deserialize(PendingDeposit.class, serialize(manualPendingDeposit)))
.isEqualTo(manualPendingDeposit);
}
@Test
void safeDeserialize_normalDeposit_success() {
assertThat(safeDeserialize(serialize(pendingDeposit))).isEqualTo(pendingDeposit);
}
@Test
void safeDeserialize_manualDeposit_success() {
assertThat(safeDeserialize(serialize(manualPendingDeposit))).isEqualTo(manualPendingDeposit);
}
}

View File

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

View File

@@ -20,6 +20,7 @@ import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.POST;
import static google.registry.request.auth.Auth.AUTH_API_ADMIN;
import static google.registry.request.auth.Auth.AUTH_PUBLIC;
import static google.registry.request.auth.AuthResult.NOT_AUTHENTICATED;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
@@ -228,7 +229,7 @@ public final class RequestHandlerTest {
when(req.getMethod()).thenReturn("GET");
when(req.getRequestURI()).thenReturn("/bumblebee");
when(requestAuthenticator.authorize(AUTH_PUBLIC.authSettings(), req))
.thenReturn(Optional.of(AuthResult.create(AuthLevel.NONE)));
.thenReturn(Optional.of(NOT_AUTHENTICATED));
handler.handleRequest(req, rsp);
@@ -242,7 +243,7 @@ public final class RequestHandlerTest {
when(req.getMethod()).thenReturn("POST");
when(req.getRequestURI()).thenReturn("/bumblebee");
when(requestAuthenticator.authorize(AUTH_PUBLIC.authSettings(), req))
.thenReturn(Optional.of(AuthResult.create(AuthLevel.NONE)));
.thenReturn(Optional.of(NOT_AUTHENTICATED));
handler.handleRequest(req, rsp);
@@ -255,7 +256,7 @@ public final class RequestHandlerTest {
when(req.getMethod()).thenReturn("GET");
when(req.getRequestURI()).thenReturn("/bumblebee/hive");
when(requestAuthenticator.authorize(AUTH_PUBLIC.authSettings(), req))
.thenReturn(Optional.of(AuthResult.create(AuthLevel.NONE)));
.thenReturn(Optional.of(NOT_AUTHENTICATED));
handler.handleRequest(req, rsp);
@@ -268,7 +269,7 @@ public final class RequestHandlerTest {
when(req.getMethod()).thenReturn("POST");
when(req.getRequestURI()).thenReturn("/sloth");
when(requestAuthenticator.authorize(AUTH_PUBLIC.authSettings(), req))
.thenReturn(Optional.of(AuthResult.create(AuthLevel.NONE)));
.thenReturn(Optional.of(NOT_AUTHENTICATED));
handler.handleRequest(req, rsp);
@@ -284,7 +285,7 @@ public final class RequestHandlerTest {
when(req.getMethod()).thenReturn("POST");
when(req.getRequestURI()).thenReturn("/sloth/nest");
when(requestAuthenticator.authorize(AUTH_PUBLIC.authSettings(), req))
.thenReturn(Optional.of(AuthResult.create(AuthLevel.NONE)));
.thenReturn(Optional.of(NOT_AUTHENTICATED));
handler.handleRequest(req, rsp);
@@ -296,7 +297,7 @@ public final class RequestHandlerTest {
when(req.getMethod()).thenReturn("GET");
when(req.getRequestURI()).thenReturn("/fail");
when(requestAuthenticator.authorize(AUTH_PUBLIC.authSettings(), req))
.thenReturn(Optional.of(AuthResult.create(AuthLevel.NONE)));
.thenReturn(Optional.of(NOT_AUTHENTICATED));
handler.handleRequest(req, rsp);
@@ -311,7 +312,7 @@ public final class RequestHandlerTest {
when(req.getMethod()).thenReturn("GET");
when(req.getRequestURI()).thenReturn("/failAtConstruction");
when(requestAuthenticator.authorize(AUTH_PUBLIC.authSettings(), req))
.thenReturn(Optional.of(AuthResult.create(AuthLevel.NONE)));
.thenReturn(Optional.of(NOT_AUTHENTICATED));
handler.handleRequest(req, rsp);
@@ -324,7 +325,7 @@ public final class RequestHandlerTest {
when(req.getMethod()).thenReturn("GET");
when(req.getRequestURI()).thenReturn("/bogus");
when(requestAuthenticator.authorize(AUTH_PUBLIC.authSettings(), req))
.thenReturn(Optional.of(AuthResult.create(AuthLevel.NONE)));
.thenReturn(Optional.of(NOT_AUTHENTICATED));
handler.handleRequest(req, rsp);
@@ -336,7 +337,7 @@ public final class RequestHandlerTest {
when(req.getMethod()).thenReturn("POST");
when(req.getRequestURI()).thenReturn("/fail");
when(requestAuthenticator.authorize(AUTH_PUBLIC.authSettings(), req))
.thenReturn(Optional.of(AuthResult.create(AuthLevel.NONE)));
.thenReturn(Optional.of(NOT_AUTHENTICATED));
handler.handleRequest(req, rsp);
@@ -348,7 +349,7 @@ public final class RequestHandlerTest {
when(req.getMethod()).thenReturn("FIREAWAY");
when(req.getRequestURI()).thenReturn("/fail");
when(requestAuthenticator.authorize(AUTH_PUBLIC.authSettings(), req))
.thenReturn(Optional.of(AuthResult.create(AuthLevel.NONE)));
.thenReturn(Optional.of(NOT_AUTHENTICATED));
handler.handleRequest(req, rsp);
@@ -364,7 +365,7 @@ public final class RequestHandlerTest {
when(req.getMethod()).thenReturn("get");
when(req.getRequestURI()).thenReturn("/bumblebee");
when(requestAuthenticator.authorize(AUTH_PUBLIC.authSettings(), req))
.thenReturn(Optional.of(AuthResult.create(AuthLevel.NONE)));
.thenReturn(Optional.of(NOT_AUTHENTICATED));
handler.handleRequest(req, rsp);
@@ -386,7 +387,7 @@ public final class RequestHandlerTest {
when(req.getMethod()).thenReturn("POST");
when(req.getRequestURI()).thenReturn("/safe-sloth");
when(requestAuthenticator.authorize(AUTH_PUBLIC.authSettings(), req))
.thenReturn(Optional.of(AuthResult.create(AuthLevel.NONE)));
.thenReturn(Optional.of(NOT_AUTHENTICATED));
handler.handleRequest(req, rsp);
@@ -399,7 +400,7 @@ public final class RequestHandlerTest {
when(req.getMethod()).thenReturn("GET");
when(req.getRequestURI()).thenReturn("/safe-sloth");
when(requestAuthenticator.authorize(AUTH_PUBLIC.authSettings(), req))
.thenReturn(Optional.of(AuthResult.create(AuthLevel.NONE)));
.thenReturn(Optional.of(NOT_AUTHENTICATED));
handler.handleRequest(req, rsp);
@@ -412,7 +413,7 @@ public final class RequestHandlerTest {
when(req.getMethod()).thenReturn("GET");
when(req.getRequestURI()).thenReturn("/auth/none");
when(requestAuthenticator.authorize(AUTH_PUBLIC.authSettings(), req))
.thenReturn(Optional.of(AuthResult.create(AuthLevel.NONE)));
.thenReturn(Optional.of(NOT_AUTHENTICATED));
handler.handleRequest(req, rsp);
@@ -440,8 +441,7 @@ public final class RequestHandlerTest {
when(req.getMethod()).thenReturn("GET");
when(req.getRequestURI()).thenReturn("/auth/adminUser");
when(requestAuthenticator.authorize(AUTH_API_ADMIN.authSettings(), req))
.thenReturn(
Optional.of(AuthResult.create(AuthLevel.USER, UserAuthInfo.create(testUser, true))));
.thenReturn(Optional.of(AuthResult.createUser(UserAuthInfo.create(testUser, true))));
handler.handleRequest(req, rsp);
@@ -449,7 +449,6 @@ public final class RequestHandlerTest {
assertThat(providedAuthResult.authLevel()).isEqualTo(AuthLevel.USER);
assertThat(providedAuthResult.userAuthInfo()).isPresent();
assertThat(providedAuthResult.userAuthInfo().get().appEngineUser()).hasValue(testUser);
assertThat(providedAuthResult.userAuthInfo().get().oauthTokenInfo()).isEmpty();
assertMetric("/auth/adminUser", GET, AuthLevel.USER, true);
}
}

View File

@@ -15,6 +15,7 @@
package google.registry.request.auth;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.request.auth.AuthResult.NOT_AUTHENTICATED;
import static google.registry.request.auth.AuthenticatedRegistrarAccessor.Role.ADMIN;
import static google.registry.request.auth.AuthenticatedRegistrarAccessor.Role.OWNER;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
@@ -40,7 +41,6 @@ import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.Registrar.State;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.request.auth.AuthSettings.AuthLevel;
import google.registry.request.auth.AuthenticatedRegistrarAccessor.RegistrarAccessDeniedException;
import google.registry.util.JdkLoggerConfig;
import java.util.Optional;
@@ -75,7 +75,7 @@ class AuthenticatedRegistrarAccessorTest {
private static final AuthResult USER = createAuthResult(false);
private static final AuthResult GAE_ADMIN = createAuthResult(true);
private static final AuthResult NO_USER = AuthResult.create(AuthLevel.NONE);
private static final AuthResult NO_USER = NOT_AUTHENTICATED;
private static final Optional<String> SUPPORT_GROUP = Optional.of("support@registry.example");
/** Registrar ID of a REAL registrar with a RegistrarContact for USER and GAE_ADMIN. */
private static final String REGISTRAR_ID_WITH_CONTACT = "TheRegistrar";
@@ -94,8 +94,7 @@ class AuthenticatedRegistrarAccessorTest {
* @param isAdmin if true, the user is an administrator for the app-engine project.
*/
private static AuthResult createAuthResult(boolean isAdmin) {
return AuthResult.create(
AuthLevel.USER,
return AuthResult.createUser(
UserAuthInfo.create(new User("johndoe@theregistrar.com", "theregistrar.com"), isAdmin));
}
@@ -295,8 +294,7 @@ class AuthenticatedRegistrarAccessorTest {
void testGetRegistrarForUser_inContacts_isNotAdmin_caseInsensitive() throws Exception {
expectGetRegistrarSuccess(
REGISTRAR_ID_WITH_CONTACT,
AuthResult.create(
AuthLevel.USER,
AuthResult.createUser(
UserAuthInfo.create(new User("JohnDoe@theregistrar.com", "theregistrar.com"), false)),
"user JohnDoe@theregistrar.com has [OWNER] access to registrar TheRegistrar");
verify(lazyGroupsConnection).get();
@@ -417,12 +415,11 @@ class AuthenticatedRegistrarAccessorTest {
void testConsoleUser_admin() {
google.registry.model.console.User consoleUser =
new google.registry.model.console.User.Builder()
.setGaiaId("gaiaId")
.setEmailAddress("email@email.com")
.setUserRoles(
new UserRoles.Builder().setIsAdmin(true).setGlobalRole(GlobalRole.FTE).build())
.build();
AuthResult authResult = AuthResult.create(AuthLevel.USER, UserAuthInfo.create(consoleUser));
AuthResult authResult = AuthResult.createUser(UserAuthInfo.create(consoleUser));
AuthenticatedRegistrarAccessor registrarAccessor =
new AuthenticatedRegistrarAccessor(
authResult, ADMIN_REGISTRAR_ID, SUPPORT_GROUP, lazyGroupsConnection);
@@ -444,11 +441,10 @@ class AuthenticatedRegistrarAccessorTest {
// not admins
google.registry.model.console.User consoleUser =
new google.registry.model.console.User.Builder()
.setGaiaId("gaiaId")
.setEmailAddress("email@email.com")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.SUPPORT_AGENT).build())
.build();
AuthResult authResult = AuthResult.create(AuthLevel.USER, UserAuthInfo.create(consoleUser));
AuthResult authResult = AuthResult.createUser(UserAuthInfo.create(consoleUser));
AuthenticatedRegistrarAccessor registrarAccessor =
new AuthenticatedRegistrarAccessor(
authResult, ADMIN_REGISTRAR_ID, SUPPORT_GROUP, lazyGroupsConnection);
@@ -462,7 +458,6 @@ class AuthenticatedRegistrarAccessorTest {
// Registrar employees should have OWNER access to their registrars
google.registry.model.console.User consoleUser =
new google.registry.model.console.User.Builder()
.setGaiaId("gaiaId")
.setEmailAddress("email@email.com")
.setUserRoles(
new UserRoles.Builder()
@@ -474,7 +469,7 @@ class AuthenticatedRegistrarAccessorTest {
RegistrarRole.ACCOUNT_MANAGER))
.build())
.build();
AuthResult authResult = AuthResult.create(AuthLevel.USER, UserAuthInfo.create(consoleUser));
AuthResult authResult = AuthResult.createUser(UserAuthInfo.create(consoleUser));
AuthenticatedRegistrarAccessor registrarAccessor =
new AuthenticatedRegistrarAccessor(
authResult, ADMIN_REGISTRAR_ID, SUPPORT_GROUP, lazyGroupsConnection);

View File

@@ -18,7 +18,6 @@ import static com.google.common.net.HttpHeaders.AUTHORIZATION;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.request.auth.AuthModule.BEARER_PREFIX;
import static google.registry.request.auth.AuthModule.IAP_HEADER_NAME;
import static google.registry.request.auth.AuthModule.PROXY_HEADER_NAME;
import static google.registry.testing.DatabaseHelper.insertInDb;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
@@ -61,7 +60,6 @@ public class OidcTokenAuthenticationMechanismTest {
private final User user =
new User.Builder()
.setEmailAddress(email)
.setGaiaId(gaiaId)
.setUserRoles(
new UserRoles.Builder().setIsAdmin(true).setGlobalRole(GlobalRole.FTE).build())
.build();
@@ -93,9 +91,8 @@ public class OidcTokenAuthenticationMechanismTest {
@Test
void testAuthResultBypass() {
OidcTokenAuthenticationMechanism.setAuthResultForTesting(AuthResult.create(AuthLevel.APP));
assertThat(authenticationMechanism.authenticate(null))
.isEqualTo(AuthResult.create(AuthLevel.APP));
OidcTokenAuthenticationMechanism.setAuthResultForTesting(AuthResult.NOT_AUTHENTICATED);
assertThat(authenticationMechanism.authenticate(null)).isEqualTo(AuthResult.NOT_AUTHENTICATED);
}
@Test
@@ -141,7 +138,6 @@ public class OidcTokenAuthenticationMechanismTest {
User serviceUser =
new User.Builder()
.setEmailAddress("service@email.test")
.setGaiaId("service-gaia-id")
.setUserRoles(
new UserRoles.Builder().setIsAdmin(true).setGlobalRole(GlobalRole.FTE).build())
.build();
@@ -171,16 +167,10 @@ public class OidcTokenAuthenticationMechanismTest {
void testRegular_tokenExtractor() throws Exception {
useRegularOidcMechanism();
// The token does not have the "Bearer " prefix.
when(request.getHeader(PROXY_HEADER_NAME)).thenReturn(rawToken);
when(request.getHeader(AUTHORIZATION)).thenReturn(rawToken);
assertThat(authenticationMechanism.tokenExtractor.extract(request)).isNull();
// The token is in the correct format.
when(request.getHeader(PROXY_HEADER_NAME))
.thenReturn(String.format("%s%s", BEARER_PREFIX, rawToken));
assertThat(authenticationMechanism.tokenExtractor.extract(request)).isEqualTo(rawToken);
// The token is in the correct format, and under the alternative header.
when(request.getHeader(PROXY_HEADER_NAME)).thenReturn(null);
when(request.getHeader(AUTHORIZATION))
.thenReturn(String.format("%s%s", BEARER_PREFIX, rawToken));
assertThat(authenticationMechanism.tokenExtractor.extract(request)).isEqualTo(rawToken);

View File

@@ -14,361 +14,276 @@
package google.registry.request.auth;
import static com.google.common.net.HttpHeaders.AUTHORIZATION;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static google.registry.request.auth.AuthResult.NOT_AUTHENTICATED;
import static google.registry.request.auth.AuthSettings.AuthLevel.APP;
import static google.registry.request.auth.AuthSettings.AuthLevel.NONE;
import static google.registry.request.auth.AuthSettings.AuthLevel.USER;
import static google.registry.request.auth.AuthSettings.AuthMethod.API;
import static google.registry.request.auth.AuthSettings.AuthMethod.LEGACY;
import static google.registry.request.auth.AuthSettings.UserPolicy.ADMIN;
import static google.registry.request.auth.AuthSettings.UserPolicy.PUBLIC;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.request.auth.AuthSettings.AuthLevel;
import google.registry.request.auth.AuthSettings.AuthMethod;
import google.registry.request.auth.AuthSettings.UserPolicy;
import google.registry.security.XsrfTokenManager;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeOAuthService;
import google.registry.testing.FakeUserService;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link RequestAuthenticator}. */
class RequestAuthenticatorTest {
@RegisterExtension
final JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
private static final AuthResult APP_AUTH = AuthResult.createApp("app@registry.example");
private static final AuthSettings AUTH_NONE =
AuthSettings.create(ImmutableList.of(AuthMethod.API), AuthLevel.NONE, UserPolicy.PUBLIC);
private static final AuthResult USER_PUBLIC_AUTH =
AuthResult.createUser(
UserAuthInfo.create(
new User.Builder()
.setEmailAddress("user@registry.example")
.setUserRoles(
new UserRoles.Builder()
.setIsAdmin(false)
.setGlobalRole(GlobalRole.NONE)
.build())
.build()));
private static final AuthSettings AUTH_ANY_USER_ANY_METHOD =
AuthSettings.create(
ImmutableList.of(AuthMethod.API, AuthMethod.LEGACY), AuthLevel.USER, UserPolicy.PUBLIC);
private static final AuthResult USER_ADMIN_AUTH =
AuthResult.createUser(
UserAuthInfo.create(
new User.Builder()
.setEmailAddress("admin@registry.example")
.setUserRoles(
new UserRoles.Builder()
.setIsAdmin(true)
.setGlobalRole(GlobalRole.FTE)
.build())
.build()));
private static final AuthSettings AUTH_ANY_USER_NO_LEGACY =
AuthSettings.create(ImmutableList.of(AuthMethod.API), AuthLevel.USER, UserPolicy.PUBLIC);
private static final AuthSettings AUTH_ADMIN_USER_ANY_METHOD =
AuthSettings.create(
ImmutableList.of(AuthMethod.API, AuthMethod.LEGACY), AuthLevel.USER, UserPolicy.ADMIN);
private static final AuthSettings AUTH_NO_METHODS =
AuthSettings.create(ImmutableList.of(), AuthLevel.APP, UserPolicy.PUBLIC);
private static final AuthSettings AUTH_WRONG_METHOD_ORDERING =
AuthSettings.create(
ImmutableList.of(AuthMethod.LEGACY, AuthMethod.API), AuthLevel.APP, UserPolicy.PUBLIC);
private static final AuthSettings AUTH_DUPLICATE_METHODS =
AuthSettings.create(
ImmutableList.of(AuthMethod.API, AuthMethod.API), AuthLevel.APP, UserPolicy.PUBLIC);
private static final AuthSettings AUTH_NONE_REQUIRES_ADMIN =
AuthSettings.create(ImmutableList.of(AuthMethod.API), AuthLevel.NONE, UserPolicy.ADMIN);
private final UserService mockUserService = mock(UserService.class);
private final HttpServletRequest req = mock(HttpServletRequest.class);
private final User testUser = new User("test@google.com", "test@google.com");
private final FakeUserService fakeUserService = new FakeUserService();
private final XsrfTokenManager xsrfTokenManager =
new XsrfTokenManager(new FakeClock(), fakeUserService);
private final FakeOAuthService fakeOAuthService =
new FakeOAuthService(
false /* isOAuthEnabled */,
testUser,
false /* isUserAdmin */,
"test-client-id",
ImmutableList.of("test-scope1", "test-scope2", "nontest-scope"));
private final AuthenticationMechanism apiAuthenticationMechanism1 =
mock(AuthenticationMechanism.class);
private final AuthenticationMechanism apiAuthenticationMechanism2 =
mock(AuthenticationMechanism.class);
private final LegacyAuthenticationMechanism legacyAuthenticationMechanism =
mock(LegacyAuthenticationMechanism.class);
private Optional<AuthResult> authorize(AuthLevel authLevel, UserPolicy userPolicy) {
return new RequestAuthenticator(
ImmutableList.of(apiAuthenticationMechanism1, apiAuthenticationMechanism2),
legacyAuthenticationMechanism)
.authorize(AuthSettings.create(ImmutableList.of(API, LEGACY), authLevel, userPolicy), req);
}
private AuthResult authenticate(AuthMethod... methods) {
return new RequestAuthenticator(
ImmutableList.of(apiAuthenticationMechanism1, apiAuthenticationMechanism2),
legacyAuthenticationMechanism)
.authenticate(AuthSettings.create(ImmutableList.copyOf(methods), NONE, PUBLIC), req);
}
@BeforeEach
void beforeEach() {
when(req.getMethod()).thenReturn("POST");
}
private RequestAuthenticator createRequestAuthenticator(UserService userService) {
return new RequestAuthenticator(
ImmutableList.of(
new OAuthAuthenticationMechanism(
fakeOAuthService,
ImmutableSet.of("test-scope1", "test-scope2", "test-scope3"),
ImmutableSet.of("test-scope1", "test-scope2"),
ImmutableSet.of("test-client-id", "other-test-client-id"))),
new LegacyAuthenticationMechanism(userService, xsrfTokenManager));
}
private Optional<AuthResult> runTest(UserService userService, AuthSettings auth) {
return createRequestAuthenticator(userService).authorize(auth, req);
when(apiAuthenticationMechanism1.authenticate(req)).thenReturn(NOT_AUTHENTICATED);
when(apiAuthenticationMechanism2.authenticate(req)).thenReturn(NOT_AUTHENTICATED);
when(legacyAuthenticationMechanism.authenticate(req)).thenReturn(NOT_AUTHENTICATED);
}
@Test
void testNoAuthNeeded_noneFound() {
Optional<AuthResult> authResult = runTest(mockUserService, AUTH_NONE);
verifyNoInteractions(mockUserService);
assertThat(authResult).isPresent();
assertThat(authResult.get().authLevel()).isEqualTo(AuthLevel.NONE);
void testAuthorize_noneRequired() {
for (AuthResult resultFound :
ImmutableList.of(NOT_AUTHENTICATED, APP_AUTH, USER_ADMIN_AUTH, USER_PUBLIC_AUTH)) {
when(apiAuthenticationMechanism1.authenticate(req)).thenReturn(resultFound);
assertThat(authorize(NONE, PUBLIC)).hasValue(resultFound);
}
}
@Test
void testAnyUserAnyMethod_notLoggedIn() {
Optional<AuthResult> authResult = runTest(fakeUserService, AUTH_ANY_USER_ANY_METHOD);
void testAuthorize_appPublicRequired() {
authorize(APP, PUBLIC);
assertThat(authorize(APP, PUBLIC)).isEmpty();
assertThat(authResult).isEmpty();
for (AuthResult resultFound : ImmutableList.of(APP_AUTH, USER_ADMIN_AUTH, USER_PUBLIC_AUTH)) {
when(apiAuthenticationMechanism1.authenticate(req)).thenReturn(resultFound);
assertThat(authorize(APP, PUBLIC)).hasValue(resultFound);
}
}
@Test
void testAnyUserAnyMethod_xsrfFailure() {
fakeUserService.setUser(testUser, false);
void testAuthorize_appAdminRequired() {
for (AuthResult resultFound : ImmutableList.of(NOT_AUTHENTICATED, USER_PUBLIC_AUTH)) {
when(apiAuthenticationMechanism1.authenticate(req)).thenReturn(resultFound);
assertThat(authorize(APP, ADMIN)).isEmpty();
}
Optional<AuthResult> authResult = runTest(fakeUserService, AUTH_ANY_USER_ANY_METHOD);
assertThat(authResult).isEmpty();
for (AuthResult resultFound : ImmutableList.of(APP_AUTH, USER_ADMIN_AUTH)) {
when(apiAuthenticationMechanism1.authenticate(req)).thenReturn(resultFound);
assertThat(authorize(APP, ADMIN)).hasValue(resultFound);
}
}
@Test
void testAnyUserAnyMethod_success() {
fakeUserService.setUser(testUser, false /* isAdmin */);
when(req.getHeader(XsrfTokenManager.X_CSRF_TOKEN))
.thenReturn(xsrfTokenManager.generateToken(testUser.getEmail()));
void testAuthorize_userPublicRequired() {
for (AuthResult resultFound : ImmutableList.of(NOT_AUTHENTICATED, APP_AUTH)) {
when(apiAuthenticationMechanism1.authenticate(req)).thenReturn(resultFound);
assertThat(authorize(USER, PUBLIC)).isEmpty();
}
Optional<AuthResult> authResult = runTest(fakeUserService, AUTH_ANY_USER_ANY_METHOD);
assertThat(authResult).isPresent();
assertThat(authResult.get().authLevel()).isEqualTo(AuthLevel.USER);
assertThat(authResult.get().userAuthInfo()).isPresent();
assertThat(authResult.get().userAuthInfo().get().appEngineUser()).hasValue(testUser);
assertThat(authResult.get().userAuthInfo().get().isUserAdmin()).isFalse();
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo()).isEmpty();
for (AuthResult resultFound : ImmutableList.of(USER_PUBLIC_AUTH, USER_ADMIN_AUTH)) {
when(apiAuthenticationMechanism1.authenticate(req)).thenReturn(resultFound);
assertThat(authorize(USER, PUBLIC)).hasValue(resultFound);
}
}
@Test
void testAnyUserAnyMethod_xsrfNotRequiredForGet() {
fakeUserService.setUser(testUser, false);
when(req.getMethod()).thenReturn("GET");
void testAuthorize_userAdminRequired() {
for (AuthResult resultFound : ImmutableList.of(NOT_AUTHENTICATED, APP_AUTH, USER_PUBLIC_AUTH)) {
when(apiAuthenticationMechanism1.authenticate(req)).thenReturn(resultFound);
assertThat(authorize(USER, ADMIN)).isEmpty();
}
Optional<AuthResult> authResult = runTest(fakeUserService, AUTH_ANY_USER_ANY_METHOD);
assertThat(authResult).isPresent();
assertThat(authResult.get().authLevel()).isEqualTo(AuthLevel.USER);
assertThat(authResult.get().userAuthInfo()).isPresent();
assertThat(authResult.get().userAuthInfo().get().appEngineUser()).hasValue(testUser);
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo()).isEmpty();
when(apiAuthenticationMechanism1.authenticate(req)).thenReturn(USER_ADMIN_AUTH);
assertThat(authorize(USER, ADMIN)).hasValue(USER_ADMIN_AUTH);
}
@Test
void testAdminUserAnyMethod_notLoggedIn() {
Optional<AuthResult> authResult = runTest(fakeUserService, AUTH_ADMIN_USER_ANY_METHOD);
assertThat(authResult).isEmpty();
void testAuthenticate_apiFirst() {
when(apiAuthenticationMechanism1.authenticate(req)).thenReturn(APP_AUTH);
assertThat(authenticate(API, LEGACY)).isEqualTo(APP_AUTH);
verify(apiAuthenticationMechanism1).authenticate(req);
verifyNoMoreInteractions(apiAuthenticationMechanism1);
verifyNoMoreInteractions(apiAuthenticationMechanism2);
verifyNoMoreInteractions(legacyAuthenticationMechanism);
}
@Test
void testAdminUserAnyMethod_notAdminUser() {
fakeUserService.setUser(testUser, false /* isAdmin */);
Optional<AuthResult> authResult = runTest(fakeUserService, AUTH_ADMIN_USER_ANY_METHOD);
assertThat(authResult).isEmpty();
void testAuthenticate_apiSecond() {
when(apiAuthenticationMechanism2.authenticate(req)).thenReturn(APP_AUTH);
assertThat(authenticate(API, LEGACY)).isEqualTo(APP_AUTH);
verify(apiAuthenticationMechanism1).authenticate(req);
verify(apiAuthenticationMechanism2).authenticate(req);
verifyNoMoreInteractions(apiAuthenticationMechanism1);
verifyNoMoreInteractions(apiAuthenticationMechanism2);
verifyNoMoreInteractions(legacyAuthenticationMechanism);
}
@Test
void testAdminUserAnyMethod_xsrfFailure() {
fakeUserService.setUser(testUser, true);
Optional<AuthResult> authResult = runTest(fakeUserService, AUTH_ADMIN_USER_ANY_METHOD);
assertThat(authResult).isEmpty();
void testAuthenticate_legacy() {
when(legacyAuthenticationMechanism.authenticate(req)).thenReturn(APP_AUTH);
assertThat(authenticate(API, LEGACY)).isEqualTo(APP_AUTH);
verify(apiAuthenticationMechanism1).authenticate(req);
verify(apiAuthenticationMechanism2).authenticate(req);
verify(legacyAuthenticationMechanism).authenticate(req);
verifyNoMoreInteractions(apiAuthenticationMechanism1);
verifyNoMoreInteractions(apiAuthenticationMechanism2);
verifyNoMoreInteractions(legacyAuthenticationMechanism);
}
@Test
void testAdminUserAnyMethod_success() {
fakeUserService.setUser(testUser, true /* isAdmin */);
when(req.getHeader(XsrfTokenManager.X_CSRF_TOKEN))
.thenReturn(xsrfTokenManager.generateToken(testUser.getEmail()));
Optional<AuthResult> authResult = runTest(fakeUserService, AUTH_ADMIN_USER_ANY_METHOD);
assertThat(authResult).isPresent();
assertThat(authResult.get().authLevel()).isEqualTo(AuthLevel.USER);
assertThat(authResult.get().userAuthInfo()).isPresent();
assertThat(authResult.get().userAuthInfo().get().appEngineUser()).hasValue(testUser);
assertThat(authResult.get().userAuthInfo().get().isUserAdmin()).isTrue();
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo()).isEmpty();
void testAuthenticate_returnFirstResult() {
// API auth 2 returns an authenticted auth result, so we don't bother trying the next auth
// (legacy auth).
when(apiAuthenticationMechanism2.authenticate(req)).thenReturn(APP_AUTH);
when(legacyAuthenticationMechanism.authenticate(req)).thenReturn(USER_PUBLIC_AUTH);
assertThat(authenticate(API, LEGACY)).isEqualTo(APP_AUTH);
verify(apiAuthenticationMechanism1).authenticate(req);
verify(apiAuthenticationMechanism2).authenticate(req);
verifyNoMoreInteractions(apiAuthenticationMechanism1);
verifyNoMoreInteractions(apiAuthenticationMechanism2);
verifyNoMoreInteractions(legacyAuthenticationMechanism);
}
@Test
void testOAuth_success() {
fakeOAuthService.setUser(testUser);
fakeOAuthService.setOAuthEnabled(true);
when(req.getHeader(AUTHORIZATION)).thenReturn("Bearer TOKEN");
Optional<AuthResult> authResult = runTest(fakeUserService, AUTH_ANY_USER_NO_LEGACY);
assertThat(authResult).isPresent();
assertThat(authResult.get().authLevel()).isEqualTo(AuthLevel.USER);
assertThat(authResult.get().userAuthInfo()).isPresent();
assertThat(authResult.get().userAuthInfo().get().appEngineUser()).hasValue(testUser);
assertThat(authResult.get().userAuthInfo().get().isUserAdmin()).isFalse();
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo()).isPresent();
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo().get().authorizedScopes())
.containsAtLeast("test-scope1", "test-scope2");
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo().get().oauthClientId())
.isEqualTo("test-client-id");
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo().get().rawAccessToken())
.isEqualTo("TOKEN");
void testAuthenticate_notAuthenticated() {
assertThat(authenticate(API, LEGACY)).isEqualTo(NOT_AUTHENTICATED);
verify(apiAuthenticationMechanism1).authenticate(req);
verify(apiAuthenticationMechanism2).authenticate(req);
verify(legacyAuthenticationMechanism).authenticate(req);
verifyNoMoreInteractions(apiAuthenticationMechanism1);
verifyNoMoreInteractions(apiAuthenticationMechanism2);
verifyNoMoreInteractions(legacyAuthenticationMechanism);
}
@Test
void testOAuthAdmin_success() {
fakeOAuthService.setUser(testUser);
fakeOAuthService.setUserAdmin(true);
fakeOAuthService.setOAuthEnabled(true);
when(req.getHeader(AUTHORIZATION)).thenReturn("Bearer TOKEN");
Optional<AuthResult> authResult = runTest(fakeUserService, AUTH_ANY_USER_NO_LEGACY);
assertThat(authResult).isPresent();
assertThat(authResult.get().authLevel()).isEqualTo(AuthLevel.USER);
assertThat(authResult.get().userAuthInfo()).isPresent();
assertThat(authResult.get().userAuthInfo().get().appEngineUser()).hasValue(testUser);
assertThat(authResult.get().userAuthInfo().get().isUserAdmin()).isTrue();
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo()).isPresent();
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo().get().authorizedScopes())
.containsAtLeast("test-scope1", "test-scope2");
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo().get().oauthClientId())
.isEqualTo("test-client-id");
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo().get().rawAccessToken())
.isEqualTo("TOKEN");
void testAuthenticate_apiOnly() {
when(legacyAuthenticationMechanism.authenticate(req)).thenReturn(USER_PUBLIC_AUTH);
assertThat(authenticate(API)).isEqualTo(NOT_AUTHENTICATED);
verify(apiAuthenticationMechanism1).authenticate(req);
verify(apiAuthenticationMechanism2).authenticate(req);
verifyNoMoreInteractions(apiAuthenticationMechanism1);
verifyNoMoreInteractions(apiAuthenticationMechanism2);
verifyNoMoreInteractions(legacyAuthenticationMechanism);
}
@Test
void testOAuthMissingAuthenticationToken_failure() {
fakeOAuthService.setUser(testUser);
fakeOAuthService.setOAuthEnabled(true);
Optional<AuthResult> authResult = runTest(fakeUserService, AUTH_ANY_USER_NO_LEGACY);
assertThat(authResult).isEmpty();
void testAuthenticate_legacyOnly() {
when(apiAuthenticationMechanism1.authenticate(req)).thenReturn(USER_PUBLIC_AUTH);
assertThat(authenticate(LEGACY)).isEqualTo(NOT_AUTHENTICATED);
verify(legacyAuthenticationMechanism).authenticate(req);
verifyNoMoreInteractions(apiAuthenticationMechanism1);
verifyNoMoreInteractions(apiAuthenticationMechanism2);
verifyNoMoreInteractions(legacyAuthenticationMechanism);
}
@Test
void testOAuthClientIdMismatch_failure() {
fakeOAuthService.setUser(testUser);
fakeOAuthService.setOAuthEnabled(true);
fakeOAuthService.setClientId("wrong-client-id");
when(req.getHeader(AUTHORIZATION)).thenReturn("Bearer TOKEN");
Optional<AuthResult> authResult = runTest(fakeUserService, AUTH_ANY_USER_NO_LEGACY);
assertThat(authResult).isEmpty();
}
@Test
void testOAuthNoScopes_failure() {
fakeOAuthService.setUser(testUser);
fakeOAuthService.setOAuthEnabled(true);
fakeOAuthService.setAuthorizedScopes();
when(req.getHeader(AUTHORIZATION)).thenReturn("Bearer TOKEN");
Optional<AuthResult> authResult = runTest(fakeUserService, AUTH_ANY_USER_NO_LEGACY);
assertThat(authResult).isEmpty();
}
@Test
void testOAuthMissingScope_failure() {
fakeOAuthService.setUser(testUser);
fakeOAuthService.setOAuthEnabled(true);
fakeOAuthService.setAuthorizedScopes("test-scope1", "test-scope3");
when(req.getHeader(AUTHORIZATION)).thenReturn("Bearer TOKEN");
Optional<AuthResult> authResult = runTest(fakeUserService, AUTH_ANY_USER_NO_LEGACY);
assertThat(authResult).isEmpty();
}
@Test
void testOAuthExtraScope_success() {
fakeOAuthService.setUser(testUser);
fakeOAuthService.setOAuthEnabled(true);
fakeOAuthService.setAuthorizedScopes("test-scope1", "test-scope2", "test-scope3");
when(req.getHeader(AUTHORIZATION)).thenReturn("Bearer TOKEN");
Optional<AuthResult> authResult = runTest(fakeUserService, AUTH_ANY_USER_NO_LEGACY);
assertThat(authResult).isPresent();
assertThat(authResult.get().authLevel()).isEqualTo(AuthLevel.USER);
assertThat(authResult.get().userAuthInfo()).isPresent();
assertThat(authResult.get().userAuthInfo().get().appEngineUser()).hasValue(testUser);
assertThat(authResult.get().userAuthInfo().get().isUserAdmin()).isFalse();
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo()).isPresent();
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo().get().authorizedScopes())
.containsAtLeast("test-scope1", "test-scope2", "test-scope3");
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo().get().oauthClientId())
.isEqualTo("test-client-id");
assertThat(authResult.get().userAuthInfo().get().oauthTokenInfo().get().rawAccessToken())
.isEqualTo("TOKEN");
}
@Test
void testAnyUserNoLegacy_failureWithLegacyUser() {
fakeUserService.setUser(testUser, false /* isAdmin */);
Optional<AuthResult> authResult = runTest(fakeUserService, AUTH_ANY_USER_NO_LEGACY);
assertThat(authResult).isEmpty();
}
@Test
void testCheckAuthConfig_noMethods_failure() {
void testFailure_checkAuthConfig_noMethods() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() -> RequestAuthenticator.checkAuthConfig(AUTH_NO_METHODS));
() ->
RequestAuthenticator.checkAuthConfig(
AuthSettings.create(ImmutableList.of(), NONE, PUBLIC)));
assertThat(thrown).hasMessageThat().contains("Must specify at least one auth method");
}
@Test
void testCheckAuthConfig_wrongMethodOrdering_failure() {
void testFailure_checkAuthConfig_wrongMethodOrder() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() -> RequestAuthenticator.checkAuthConfig(AUTH_WRONG_METHOD_ORDERING));
() ->
RequestAuthenticator.checkAuthConfig(
AuthSettings.create(ImmutableList.of(LEGACY, API), NONE, PUBLIC)));
assertThat(thrown)
.hasMessageThat()
.contains("Auth methods must be unique and strictly in order - API, LEGACY");
}
@Test
void testCheckAuthConfig_noneAuthLevelRequiresAdmin_failure() {
void testFailure_CheckAuthConfig_duplicateMethods() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() -> RequestAuthenticator.checkAuthConfig(AUTH_NONE_REQUIRES_ADMIN));
() ->
RequestAuthenticator.checkAuthConfig(
AuthSettings.create(ImmutableList.of(API, API), NONE, PUBLIC)));
assertThat(thrown)
.hasMessageThat()
.contains("Auth methods must be unique and strictly in order - API, LEGACY");
}
@Test
void testFailure_checkAuthConfig_noneAuthLevelRequiresAdmin() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
RequestAuthenticator.checkAuthConfig(
AuthSettings.create(ImmutableList.of(API, LEGACY), NONE, ADMIN)));
assertThat(thrown)
.hasMessageThat()
.contains("Actions with minimal auth level at NONE should not specify ADMIN user policy");
}
@Test
void testCheckAuthConfig_DuplicateMethods_failure() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() -> RequestAuthenticator.checkAuthConfig(AUTH_DUPLICATE_METHODS));
assertThat(thrown)
.hasMessageThat()
.contains("Auth methods must be unique and strictly in order - API, LEGACY");
}
}

View File

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

View File

@@ -16,13 +16,15 @@ package google.registry.security;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.common.base.Splitter;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeUserService;
import org.joda.time.Duration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -37,14 +39,16 @@ class XsrfTokenManagerTest {
private final User testUser = new User("test@example.com", "test@example.com");
private final FakeClock clock = new FakeClock(START_OF_TIME);
private final FakeUserService userService = new FakeUserService();
private final UserService userService = mock(UserService.class);
private final XsrfTokenManager xsrfTokenManager = new XsrfTokenManager(clock, userService);
private String token;
@BeforeEach
void beforeEach() {
userService.setUser(testUser, false);
when(userService.isUserLoggedIn()).thenReturn(true);
when(userService.getCurrentUser()).thenReturn(testUser);
when(userService.isUserAdmin()).thenReturn(false);
token = xsrfTokenManager.generateToken(testUser.getEmail());
}

View File

@@ -84,7 +84,7 @@ public final class RegistryTestServer {
private final TestServer server;
/** @see TestServer#TestServer(HostAndPort, ImmutableMap, ImmutableList, ImmutableList) */
/** @see TestServer#TestServer(HostAndPort, ImmutableMap, ImmutableList) */
public RegistryTestServer(HostAndPort address) {
server = new TestServer(address, RUNFILES, ROUTES);
}
@@ -104,7 +104,7 @@ public final class RegistryTestServer {
server.stop();
}
/** @see TestServer#getUrl(java.lang.String) */
/** @see TestServer#getUrl(String) */
public URL getUrl(String path) {
return server.getUrl(path);
}

View File

@@ -25,7 +25,6 @@ import google.registry.model.console.UserRoles;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTransactionManagerExtension;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.AuthSettings.AuthLevel;
import google.registry.request.auth.OidcTokenAuthenticationMechanism;
import google.registry.request.auth.UserAuthInfo;
import google.registry.testing.UserInfo;
@@ -144,12 +143,11 @@ public final class RegistryTestServerMain {
User user =
new User.Builder()
.setEmailAddress(loginEmail)
.setGaiaId("123457890")
.setUserRoles(userRoles)
.setRegistryLockPassword("registryLockPassword")
.build();
OidcTokenAuthenticationMechanism.setAuthResultForTesting(
AuthResult.create(AuthLevel.USER, UserAuthInfo.create(user)));
AuthResult.createUser(UserAuthInfo.create(user)));
new JpaTestExtensions.Builder().buildIntegrationTestExtension().beforeEach(null);
JpaTransactionManagerExtension.loadInitialData();
System.out.printf("%sLoading fixtures...%s\n", BLUE, RESET);

View File

@@ -1,130 +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.testing;
import com.google.appengine.api.oauth.OAuthRequestException;
import com.google.appengine.api.oauth.OAuthService;
import com.google.appengine.api.users.User;
import com.google.common.collect.ImmutableList;
import java.util.List;
/** A fake {@link OAuthService} implementation for testing. */
public class FakeOAuthService implements OAuthService {
private boolean isOAuthEnabled;
private User currentUser;
private boolean isUserAdmin;
private String clientId;
private ImmutableList<String> authorizedScopes;
public FakeOAuthService(
boolean isOAuthEnabled,
User currentUser,
boolean isUserAdmin,
String clientId,
List<String> authorizedScopes) {
this.isOAuthEnabled = isOAuthEnabled;
this.currentUser = currentUser;
this.isUserAdmin = isUserAdmin;
this.clientId = clientId;
this.authorizedScopes = ImmutableList.copyOf(authorizedScopes);
}
public void setOAuthEnabled(boolean isOAuthEnabled) {
this.isOAuthEnabled = isOAuthEnabled;
}
public void setUser(User currentUser) {
this.currentUser = currentUser;
}
public void setUserAdmin(boolean isUserAdmin) {
this.isUserAdmin = isUserAdmin;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public void setAuthorizedScopes(String... scopes) {
this.authorizedScopes = ImmutableList.copyOf(scopes);
}
@Override
public User getCurrentUser() throws OAuthRequestException {
if (!isOAuthEnabled) {
throw new OAuthRequestException("invalid OAuth request");
}
return currentUser;
}
@Override
public User getCurrentUser(String scope) throws OAuthRequestException {
return getCurrentUser();
}
@Override
public User getCurrentUser(String... scopes) throws OAuthRequestException {
return getCurrentUser();
}
@Override
public boolean isUserAdmin() throws OAuthRequestException {
if (!isOAuthEnabled) {
throw new OAuthRequestException("invalid OAuth request");
}
return isUserAdmin;
}
@Override
public boolean isUserAdmin(String scope) throws OAuthRequestException {
return isUserAdmin();
}
@Override
public boolean isUserAdmin(String... scopes) throws OAuthRequestException {
return isUserAdmin();
}
@Override
public String getClientId(String scope) throws OAuthRequestException {
if (!isOAuthEnabled) {
throw new OAuthRequestException("invalid OAuth request");
}
return clientId;
}
@Override
public String getClientId(String... scopes) throws OAuthRequestException {
if (!isOAuthEnabled) {
throw new OAuthRequestException("invalid OAuth request");
}
return clientId;
}
@Override
public String[] getAuthorizedScopes(String... scopes) throws OAuthRequestException {
if (!isOAuthEnabled) {
throw new OAuthRequestException("invalid OAuth request");
}
return authorizedScopes.toArray(new String[0]);
}
@Deprecated
@Override
public String getOAuthConsumerKey() {
throw new UnsupportedOperationException();
}
}

View File

@@ -1,76 +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.testing;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import google.registry.model.annotations.DeleteAfterMigration;
import java.util.Set;
import javax.annotation.Nullable;
/** Fake implementation of {@link UserService} for testing. */
@DeleteAfterMigration
public class FakeUserService implements UserService {
@Nullable private User user = null;
private boolean isAdmin = false;
public void setUser(@Nullable User user, boolean isAdmin) {
this.user = user;
this.isAdmin = isAdmin;
}
@Override
public String createLoginURL(String destinationURL) {
return String.format("/login?dest=%s", destinationURL);
}
@Override
public String createLoginURL(String destinationURL, String authDomain) {
return createLoginURL(destinationURL);
}
@Deprecated
@Override
public String createLoginURL(String destinationURL, String authDomain, String federatedIdentity,
Set<String> attributesRequest) {
throw new UnsupportedOperationException();
}
@Override
public String createLogoutURL(String destinationURL) {
return String.format("/logout?dest=%s", destinationURL);
}
@Override
public String createLogoutURL(String destinationURL, String authDomain) {
return createLogoutURL(destinationURL);
}
@Override
public boolean isUserLoggedIn() {
return user != null;
}
@Override
public boolean isUserAdmin() {
return isAdmin;
}
@Override
public User getCurrentUser() {
return user;
}
}

View File

@@ -26,7 +26,7 @@ import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.client.util.store.AbstractDataStoreFactory;
import com.google.api.client.util.store.DataStore;
import com.google.common.collect.ImmutableList;
@@ -72,9 +72,9 @@ class AuthModuleTest {
}
})
// We need to set the following fields because they are checked when
// Credential#setRefreshToken is called. However they are not actually persisted in the
// Credential#setRefreshToken is called. However, they are not actually persisted in the
// DataStore and not actually used in tests.
.setJsonFactory(new JacksonFactory())
.setJsonFactory(new GsonFactory())
.setTransport(new NetHttpTransport())
.setTokenServerUrl(new GenericUrl("https://accounts.google.com/o/oauth2/token"))
.setClientAuthentication(new ClientParametersAuthentication(CLIENT_ID, CLIENT_SECRET))
@@ -104,7 +104,7 @@ class AuthModuleTest {
AuthModule.provideClientScopeQualifier("client-id", ImmutableList.of("foo", "bar"));
// If we change the way we encode client id and scopes, this assertion will break. That's
// probably ok and you can just change the text. The things you have to be aware of are:
// probably ok, and you can just change the text. The things you have to be aware of are:
// - Names in the new encoding should have a low risk of collision with the old encoding.
// - Changing the encoding will force all OAuth users of the nomulus tool to do a new login
// (existing credentials will not be used).
@@ -146,7 +146,7 @@ class AuthModuleTest {
private Credential getCredential() {
// Reconstruct the entire dependency graph, injecting FakeDataStoreFactory and credential
// parameters.
JacksonFactory jsonFactory = new JacksonFactory();
GsonFactory jsonFactory = new GsonFactory();
GoogleClientSecrets clientSecrets = getSecrets();
ImmutableList<String> scopes = ImmutableList.of("scope1");
return AuthModule.provideCredential(
@@ -155,7 +155,7 @@ class AuthModuleTest {
AuthModule.provideClientScopeQualifier(AuthModule.provideClientId(clientSecrets), scopes));
}
private GoogleClientSecrets getSecrets() {
private static GoogleClientSecrets getSecrets() {
return new GoogleClientSecrets()
.setInstalled(
AuthModule.provideDefaultInstalledDetails()
@@ -166,7 +166,8 @@ class AuthModuleTest {
@Test
void test_provideLocalCredentialJson() {
String credentialJson =
AuthModule.provideLocalCredentialJson(this::getSecrets, this::getCredential, null);
AuthModule.provideLocalCredentialJson(
AuthModuleTest::getSecrets, this::getCredential, null);
Map<String, String> jsonMap =
new Gson().fromJson(credentialJson, new TypeToken<Map<String, String>>() {}.getType());
assertThat(jsonMap.get("type")).isEqualTo("authorized_user");
@@ -182,7 +183,7 @@ class AuthModuleTest {
Files.write(credentialFile.toPath(), "{some_field: some_value}".getBytes(UTF_8));
String credentialJson =
AuthModule.provideLocalCredentialJson(
this::getSecrets, this::getCredential, credentialFile.getCanonicalPath());
AuthModuleTest::getSecrets, this::getCredential, credentialFile.getCanonicalPath());
assertThat(credentialJson).isEqualTo("{some_field: some_value}");
}

View File

@@ -64,6 +64,7 @@ public class ConfigureTldCommandTest extends CommandTestCase<ConfigureTldCommand
command.mapper = objectMapper;
premiumList = persistPremiumList("test", USD, "silver,USD 50", "gold,USD 80");
command.validDnsWriterNames = ImmutableSet.of("VoidDnsWriter", "FooDnsWriter");
logger.addHandler(logHandler);
}
private void testTldConfiguredSuccessfully(Tld tld, String filename)
@@ -82,6 +83,7 @@ public class ConfigureTldCommandTest extends CommandTestCase<ConfigureTldCommand
assertThat(tld.getDriveFolderId()).isEqualTo("driveFolder");
assertThat(tld.getCreateBillingCost()).isEqualTo(Money.of(USD, 25));
testTldConfiguredSuccessfully(tld, "tld.yaml");
assertThat(tld.getBreakglassMode()).isFalse();
}
@Test
@@ -94,19 +96,17 @@ public class ConfigureTldCommandTest extends CommandTestCase<ConfigureTldCommand
Tld updatedTld = Tld.get("tld");
assertThat(updatedTld.getCreateBillingCost()).isEqualTo(Money.of(USD, 25));
testTldConfiguredSuccessfully(updatedTld, "tld.yaml");
assertThat(updatedTld.getBreakglassMode()).isFalse();
}
@Test
void testSuccess_noDiff() throws Exception {
logger.addHandler(logHandler);
Tld tld = createTld("idns");
tld =
persistResource(
tld.asBuilder()
.setIdnTables(ImmutableSet.of(JA, UNCONFUSABLE_LATIN, EXTENDED_LATIN))
.setAllowedFullyQualifiedHostNames(
ImmutableSet.of("zeta", "alpha", "gamma", "beta"))
.build());
persistResource(
tld.asBuilder()
.setIdnTables(ImmutableSet.of(JA, UNCONFUSABLE_LATIN, EXTENDED_LATIN))
.setAllowedFullyQualifiedHostNames(ImmutableSet.of("zeta", "alpha", "gamma", "beta"))
.build());
File tldFile = tmpDir.resolve("idns.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "idns.yaml"));
runCommandForced("--input=" + tldFile);
@@ -473,4 +473,101 @@ public class ConfigureTldCommandTest extends CommandTestCase<ConfigureTldCommand
assertThrows(IllegalArgumentException.class, () -> runCommandForced("--input=" + tldFile));
assertThat(thrown.getMessage()).isEqualTo("The premium list must use the TLD's currency");
}
@Test
void testFailure_breakglassFlag_NoChanges() throws Exception {
Tld tld = createTld("idns");
persistResource(
tld.asBuilder()
.setIdnTables(ImmutableSet.of(JA, UNCONFUSABLE_LATIN, EXTENDED_LATIN))
.setAllowedFullyQualifiedHostNames(ImmutableSet.of("zeta", "alpha", "gamma", "beta"))
.build());
File tldFile = tmpDir.resolve("idns.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "idns.yaml"));
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class, () -> runCommandForced("--input=" + tldFile, "-b"));
assertThat(thrown.getMessage())
.isEqualTo(
"Breakglass mode can only be set when making new changes to a TLD configuration");
}
@Test
void testSuccess_breakglassFlag_startsBreakglassMode() throws Exception {
Tld tld = createTld("tld");
assertThat(tld.getCreateBillingCost()).isEqualTo(Money.of(USD, 13));
File tldFile = tmpDir.resolve("tld.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "tld.yaml"));
runCommandForced("--input=" + tldFile, "--breakglass");
Tld updatedTld = Tld.get("tld");
assertThat(updatedTld.getCreateBillingCost()).isEqualTo(Money.of(USD, 25));
testTldConfiguredSuccessfully(updatedTld, "tld.yaml");
assertThat(updatedTld.getBreakglassMode()).isTrue();
}
@Test
void testSuccess_breakglassFlag_continuesBreakglassMode() throws Exception {
Tld tld = createTld("tld");
assertThat(tld.getCreateBillingCost()).isEqualTo(Money.of(USD, 13));
persistResource(tld.asBuilder().setBreakglassMode(true).build());
File tldFile = tmpDir.resolve("tld.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "tld.yaml"));
runCommandForced("--input=" + tldFile, "--breakglass");
Tld updatedTld = Tld.get("tld");
assertThat(updatedTld.getCreateBillingCost()).isEqualTo(Money.of(USD, 25));
testTldConfiguredSuccessfully(updatedTld, "tld.yaml");
assertThat(updatedTld.getBreakglassMode()).isTrue();
}
@Test
void testSuccess_NoDiffNoBreakglassFlag_endsBreakglassMode() throws Exception {
Tld tld = createTld("idns");
persistResource(
tld.asBuilder()
.setIdnTables(ImmutableSet.of(JA, UNCONFUSABLE_LATIN, EXTENDED_LATIN))
.setAllowedFullyQualifiedHostNames(ImmutableSet.of("zeta", "alpha", "gamma", "beta"))
.setBreakglassMode(true)
.build());
File tldFile = tmpDir.resolve("idns.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "idns.yaml"));
runCommandForced("--input=" + tldFile);
Tld updatedTld = Tld.get("idns");
assertThat(updatedTld.getBreakglassMode()).isFalse();
assertAboutLogs()
.that(logHandler)
.hasLogAtLevelWithMessage(INFO, "Breakglass mode removed from TLD: idns");
}
@Test
void testSuccess_noDiffBreakglassFlag_continuesBreakglassMode() throws Exception {
Tld tld = createTld("idns");
persistResource(
tld.asBuilder()
.setIdnTables(ImmutableSet.of(JA, UNCONFUSABLE_LATIN, EXTENDED_LATIN))
.setAllowedFullyQualifiedHostNames(ImmutableSet.of("zeta", "alpha", "gamma", "beta"))
.setBreakglassMode(true)
.build());
File tldFile = tmpDir.resolve("idns.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "idns.yaml"));
runCommandForced("--input=" + tldFile, "-b");
Tld updatedTld = Tld.get("idns");
assertThat(updatedTld.getBreakglassMode()).isTrue();
assertAboutLogs()
.that(logHandler)
.hasLogAtLevelWithMessage(INFO, "TLD YAML file contains no new changes");
}
@Test
void testFailure_noBreakglassFlag_inBreakglassMode() throws Exception {
Tld tld = createTld("tld");
persistResource(tld.asBuilder().setBreakglassMode(true).build());
File tldFile = tmpDir.resolve("tld.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "tld.yaml"));
IllegalArgumentException thrown =
assertThrows(IllegalArgumentException.class, () -> runCommandForced("--input=" + tldFile));
assertThat(thrown.getMessage())
.isEqualTo(
"Changes can not be applied since TLD is in breakglass mode but the breakglass flag"
+ " was not used");
}
}

View File

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

View File

@@ -18,9 +18,6 @@ import static com.google.common.truth.Truth.assertThat;
import static google.registry.tools.RequestFactoryModule.REQUEST_TIMEOUT_MS;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import com.google.api.client.http.GenericUrl;
@@ -35,6 +32,7 @@ import com.google.auth.oauth2.UserCredentials;
import google.registry.config.RegistryConfig;
import google.registry.testing.SystemPropertyExtension;
import google.registry.util.GoogleCredentialsBundle;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -50,7 +48,6 @@ public class RequestFactoryModuleTest {
final SystemPropertyExtension systemPropertyExtension = new SystemPropertyExtension();
@Mock public GoogleCredentialsBundle credentialsBundle;
@Mock public HttpRequestInitializer httpRequestInitializer;
@BeforeEach
void beforeEach() {
@@ -69,7 +66,6 @@ public class RequestFactoryModuleTest {
assertThat(initializer).isNotNull();
HttpRequest request = factory.buildGetRequest(new GenericUrl("http://localhost"));
initializer.initialize(request);
verifyNoInteractions(httpRequestInitializer);
} finally {
RegistryConfig.CONFIG_SETTINGS.get().gcpProject.isLocal = origIsLocal;
}
@@ -77,7 +73,6 @@ public class RequestFactoryModuleTest {
@Test
void test_provideHttpRequestFactory_remote() throws Exception {
when(credentialsBundle.getHttpRequestInitializer()).thenReturn(httpRequestInitializer);
// Mock the request/response to/from the OIDC server requesting an ID token
UserCredentials mockUserCredentials = mock(UserCredentials.class);
when(credentialsBundle.getGoogleCredentials()).thenReturn(mockUserCredentials);
@@ -100,11 +95,12 @@ public class RequestFactoryModuleTest {
HttpRequestFactory factory =
RequestFactoryModule.provideHttpRequestFactory(credentialsBundle, "clientId");
HttpRequest request = factory.buildGetRequest(new GenericUrl("http://localhost"));
assertThat(request.getHeaders().get("Proxy-Authorization")).isEqualTo("Bearer oidc.token");
@SuppressWarnings("unchecked")
List<String> authHeaders = (List<String>) request.getHeaders().get("Authorization");
assertThat(authHeaders.size()).isEqualTo(1);
assertThat(authHeaders.get(0)).isEqualTo("Bearer oidc.token");
assertThat(request.getConnectTimeout()).isEqualTo(REQUEST_TIMEOUT_MS);
assertThat(request.getReadTimeout()).isEqualTo(REQUEST_TIMEOUT_MS);
verify(httpRequestInitializer).initialize(request);
verifyNoMoreInteractions(httpRequestInitializer);
} finally {
RegistryConfig.CONFIG_SETTINGS.get().gcpProject.isLocal = origIsLocal;
}

View File

@@ -27,7 +27,6 @@ import google.registry.model.console.UserRoles;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.request.RequestModule;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.AuthSettings.AuthLevel;
import google.registry.request.auth.UserAuthInfo;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.FakeResponse;
@@ -55,8 +54,7 @@ public class ConsoleDomainGetActionTest {
void testSuccess_fullJsonRepresentation() {
ConsoleDomainGetAction action =
createAction(
AuthResult.create(
AuthLevel.USER,
AuthResult.createUser(
UserAuthInfo.create(
createUser(
new UserRoles.Builder()
@@ -85,7 +83,8 @@ public class ConsoleDomainGetActionTest {
@Test
void testFailure_appAuth() {
ConsoleDomainGetAction action = createAction(AuthResult.create(AuthLevel.APP), "exists.tld");
ConsoleDomainGetAction action =
createAction(AuthResult.createApp("service@registry.example"), "exists.tld");
action.run();
assertThat(RESPONSE.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED);
}
@@ -94,8 +93,7 @@ public class ConsoleDomainGetActionTest {
void testFailure_wrongTypeOfUser() {
ConsoleDomainGetAction action =
createAction(
AuthResult.create(
AuthLevel.USER,
AuthResult.createUser(
UserAuthInfo.create(mock(com.google.appengine.api.users.User.class), false)),
"exists.tld");
action.run();
@@ -106,8 +104,7 @@ public class ConsoleDomainGetActionTest {
void testFailure_noAccessToRegistrar() {
ConsoleDomainGetAction action =
createAction(
AuthResult.create(
AuthLevel.USER, UserAuthInfo.create(createUser(new UserRoles.Builder().build()))),
AuthResult.createUser(UserAuthInfo.create(createUser(new UserRoles.Builder().build()))),
"exists.tld");
action.run();
assertThat(RESPONSE.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_NOT_FOUND);
@@ -117,8 +114,7 @@ public class ConsoleDomainGetActionTest {
void testFailure_nonexistentDomain() {
ConsoleDomainGetAction action =
createAction(
AuthResult.create(
AuthLevel.USER,
AuthResult.createUser(
UserAuthInfo.create(createUser(new UserRoles.Builder().setIsAdmin(true).build()))),
"nonexistent.tld");
action.run();
@@ -128,7 +124,6 @@ public class ConsoleDomainGetActionTest {
private User createUser(UserRoles userRoles) {
return new User.Builder()
.setEmailAddress("email@email.com")
.setGaiaId("gaiaId")
.setUserRoles(userRoles)
.build();
}

View File

@@ -15,31 +15,25 @@
package google.registry.ui.server.console;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import com.google.api.client.http.HttpStatusCodes;
import com.google.gson.Gson;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.request.RequestModule;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.AuthSettings.AuthLevel;
import google.registry.request.auth.UserAuthInfo;
import google.registry.testing.FakeResponse;
import java.io.IOException;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Tests for {@link google.registry.ui.server.console.ConsoleUserDataAction}. */
class ConsoleUserDataActionTest {
private final HttpServletRequest request = mock(HttpServletRequest.class);
private RegistrarPoc testRegistrarPoc;
private static final Gson GSON = RequestModule.provideGson();
private FakeResponse response = new FakeResponse();
@@ -52,12 +46,10 @@ class ConsoleUserDataActionTest {
User user =
new User.Builder()
.setEmailAddress("email@email.com")
.setGaiaId("gaiaId")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build())
.build();
ConsoleUserDataAction action =
createAction(AuthResult.create(AuthLevel.USER, UserAuthInfo.create(user)));
ConsoleUserDataAction action = createAction(AuthResult.createUser(UserAuthInfo.create(user)));
action.run();
assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_OK);
Map jsonObject = GSON.fromJson(response.getPayload(), Map.class);
@@ -69,8 +61,7 @@ class ConsoleUserDataActionTest {
void testFailure_notAConsoleUser() throws IOException {
ConsoleUserDataAction action =
createAction(
AuthResult.create(
AuthLevel.USER,
AuthResult.createUser(
UserAuthInfo.create(
new com.google.appengine.api.users.User(
"JohnDoe@theregistrar.com", "theregistrar.com"),

View File

@@ -38,7 +38,6 @@ import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.request.Action;
import google.registry.request.RequestModule;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.AuthSettings.AuthLevel;
import google.registry.request.auth.UserAuthInfo;
import google.registry.testing.DeterministicStringGenerator;
import google.registry.testing.FakeResponse;
@@ -108,8 +107,7 @@ class RegistrarsActionTest {
RegistrarsAction action =
createAction(
Action.Method.GET,
AuthResult.create(
AuthLevel.USER,
AuthResult.createUser(
UserAuthInfo.create(
createUser(
new UserRoles.Builder().setGlobalRole(GlobalRole.SUPPORT_LEAD).build()))));
@@ -129,8 +127,7 @@ class RegistrarsActionTest {
RegistrarsAction action =
createAction(
Action.Method.GET,
AuthResult.create(
AuthLevel.USER,
AuthResult.createUser(
UserAuthInfo.create(
createUser(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()))));
action.run();
@@ -151,8 +148,7 @@ class RegistrarsActionTest {
RegistrarsAction action =
createAction(
Action.Method.POST,
AuthResult.create(
AuthLevel.USER,
AuthResult.createUser(
UserAuthInfo.create(createUser(new UserRoles.Builder().setIsAdmin(true).build()))));
action.run();
assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_OK);
@@ -180,8 +176,7 @@ class RegistrarsActionTest {
RegistrarsAction action =
createAction(
Action.Method.POST,
AuthResult.create(
AuthLevel.USER,
AuthResult.createUser(
UserAuthInfo.create(
createUser(new UserRoles.Builder().setIsAdmin(true).build()))));
action.run();
@@ -200,8 +195,7 @@ class RegistrarsActionTest {
RegistrarsAction action =
createAction(
Action.Method.POST,
AuthResult.create(
AuthLevel.USER,
AuthResult.createUser(
UserAuthInfo.create(createUser(new UserRoles.Builder().setIsAdmin(true).build()))));
action.run();
assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_BAD_REQUEST);
@@ -215,8 +209,7 @@ class RegistrarsActionTest {
RegistrarsAction action =
createAction(
Action.Method.GET,
AuthResult.create(
AuthLevel.USER,
AuthResult.createUser(
UserAuthInfo.create(
createUser(
new UserRoles.Builder()
@@ -232,7 +225,6 @@ class RegistrarsActionTest {
private User createUser(UserRoles userRoles) {
return new User.Builder()
.setEmailAddress("email@email.com")
.setGaiaId("gaiaId")
.setUserRoles(userRoles)
.build();
}

View File

@@ -37,7 +37,6 @@ import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.request.Action;
import google.registry.request.RequestModule;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.AuthSettings.AuthLevel;
import google.registry.request.auth.UserAuthInfo;
import google.registry.testing.FakeResponse;
import google.registry.ui.server.registrar.RegistrarConsoleModule;
@@ -103,8 +102,7 @@ class ContactActionTest {
ContactAction action =
createAction(
Action.Method.GET,
AuthResult.create(
AuthLevel.USER,
AuthResult.createUser(
UserAuthInfo.create(
createUser(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()))),
testRegistrar.getRegistrarId(),
@@ -121,8 +119,7 @@ class ContactActionTest {
ContactAction action =
createAction(
Action.Method.GET,
AuthResult.create(
AuthLevel.USER,
AuthResult.createUser(
UserAuthInfo.create(
createUser(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()))),
testRegistrar.getRegistrarId(),
@@ -137,8 +134,7 @@ class ContactActionTest {
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.create(
AuthLevel.USER,
AuthResult.createUser(
UserAuthInfo.create(
createUser(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()))),
testRegistrar.getRegistrarId(),
@@ -160,8 +156,7 @@ class ContactActionTest {
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.create(
AuthLevel.USER,
AuthResult.createUser(
UserAuthInfo.create(
createUser(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()))),
testRegistrar.getRegistrarId(),
@@ -186,8 +181,7 @@ class ContactActionTest {
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.create(
AuthLevel.USER,
AuthResult.createUser(
UserAuthInfo.create(
createUser(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()))),
testRegistrar.getRegistrarId(),
@@ -208,8 +202,7 @@ class ContactActionTest {
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.create(
AuthLevel.USER,
AuthResult.createUser(
UserAuthInfo.create(
createUser(
new UserRoles.Builder()
@@ -226,7 +219,6 @@ class ContactActionTest {
private User createUser(UserRoles userRoles) {
return new User.Builder()
.setEmailAddress("email@email.com")
.setGaiaId("gaiaId")
.setUserRoles(userRoles)
.build();
}

View File

@@ -35,7 +35,6 @@ import google.registry.model.registrar.Registrar;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.request.RequestModule;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.AuthSettings.AuthLevel;
import google.registry.request.auth.AuthenticatedRegistrarAccessor;
import google.registry.request.auth.UserAuthInfo;
import google.registry.testing.FakeClock;
@@ -92,8 +91,7 @@ class SecurityActionTest {
clock.setTo(DateTime.parse("2020-11-01T00:00:00Z"));
SecurityAction action =
createAction(
AuthResult.create(
AuthLevel.USER,
AuthResult.createUser(
UserAuthInfo.create(
createUser(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()))),
testRegistrar.getRegistrarId());
@@ -109,7 +107,6 @@ class SecurityActionTest {
private User createUser(UserRoles userRoles) {
return new User.Builder()
.setEmailAddress("email@email.com")
.setGaiaId("TestUserId")
.setUserRoles(userRoles)
.build();
}

View File

@@ -33,7 +33,6 @@ import google.registry.model.registrar.Registrar;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.request.RequestModule;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.AuthSettings.AuthLevel;
import google.registry.request.auth.AuthenticatedRegistrarAccessor;
import google.registry.request.auth.AuthenticatedRegistrarAccessor.Role;
import google.registry.request.auth.UserAuthInfo;
@@ -127,8 +126,7 @@ public class WhoisRegistrarFieldsActionTest {
void testFailure_noAccessToRegistrar() throws Exception {
Registrar newRegistrar = Registrar.loadByRegistrarIdCached("NewRegistrar").get();
AuthResult onlyTheRegistrar =
AuthResult.create(
AuthLevel.USER,
AuthResult.createUser(
UserAuthInfo.create(
new User.Builder()
.setEmailAddress("email@email.example")
@@ -147,8 +145,7 @@ public class WhoisRegistrarFieldsActionTest {
}
private AuthResult defaultUserAuth() {
return AuthResult.create(
AuthLevel.USER,
return AuthResult.createUser(
UserAuthInfo.create(
new User.Builder()
.setEmailAddress("email@email.example")

View File

@@ -37,7 +37,6 @@ import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.request.Action.Method;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.AuthSettings.AuthLevel;
import google.registry.request.auth.AuthenticatedRegistrarAccessor;
import google.registry.request.auth.UserAuthInfo;
import google.registry.security.XsrfTokenManager;
@@ -93,7 +92,7 @@ public final class ConsoleOteSetupActionTest {
ImmutableSetMultimap.of("unused", AuthenticatedRegistrarAccessor.Role.ADMIN));
action.userService = UserServiceFactory.getUserService();
action.xsrfTokenManager = new XsrfTokenManager(new FakeClock(), action.userService);
action.authResult = AuthResult.create(AuthLevel.USER, UserAuthInfo.create(user, false));
action.authResult = AuthResult.createUser(UserAuthInfo.create(user, false));
action.sendEmailUtils =
new SendEmailUtils(
new InternetAddress("outgoing@registry.example"),

View File

@@ -37,7 +37,6 @@ import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.request.Action.Method;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.AuthSettings.AuthLevel;
import google.registry.request.auth.AuthenticatedRegistrarAccessor;
import google.registry.request.auth.UserAuthInfo;
import google.registry.security.XsrfTokenManager;
@@ -93,7 +92,7 @@ final class ConsoleRegistrarCreatorActionTest {
ImmutableSetMultimap.of("unused", AuthenticatedRegistrarAccessor.Role.ADMIN));
action.userService = UserServiceFactory.getUserService();
action.xsrfTokenManager = new XsrfTokenManager(new FakeClock(), action.userService);
action.authResult = AuthResult.create(AuthLevel.USER, UserAuthInfo.create(user, false));
action.authResult = AuthResult.createUser(UserAuthInfo.create(user, false));
action.sendEmailUtils =
new SendEmailUtils(
new InternetAddress("outgoing@registry.example"),

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