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

Compare commits

...

22 Commits

Author SHA1 Message Date
Ben McIlwain ec3804e87e Make domain update flow handle null auth data (#2530)
It's valid for the auth data to be null (although it only happens 10 times
across our entire registry), so the domain update flow should not fail out with
a NullPointerException when the existing state of the data is null and the
update isn't adding that data either.

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

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

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

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

* Run feature flag query in a transaction

---------

Co-authored-by: ptkach <ptkach@google.com>
2024-07-29 21:55:12 +00:00
Pavlo Tkach fa721e82ff Mark console state field on new registrar form as required (#2509)
CodeQL / Analyze (java) (push) Failing after 58s
CodeQL / Analyze (javascript) (push) Failing after 56s
CodeQL / Analyze (python) (push) Failing after 53s
Dependency Submission / dependency-submission (push) Successful in 2m12s
2024-07-26 18:46:43 +00:00
Lai Jiang d4faa77ee4 Remove login_email_address from RegistrarPoc (#2507) 2024-07-26 17:56:34 +00:00
sarahcaseybot 96d3d88c2f Remove TODOs assigned to sarahbot (#2508) 2024-07-26 17:50:35 +00:00
Pavlo Tkach 213e06f02e Add registry lock ui (#2500) 2024-07-26 16:02:19 +00:00
gbrodman d5445dd049 Allow new-console use for users with perm to the admin registrar ID (#2506)
For instance, on sandbox this will allow us to remove our global roles
but keep roles to the CharlestonRoad admin registrar. Then, when we view
the console, it will be as if we were a registrar user.
2024-07-25 20:25:55 +00:00
Lai Jiang af5adcb0ba Upgrade to Gradle 8.9 (#2504) 2024-07-25 19:01:06 +00:00
gbrodman ca238a8578 Change RL input to be a POST body (#2503) 2024-07-25 18:18:10 +00:00
Pavlo Tkach 1a8f133d54 Filter console registrars per user role (#2501) 2024-07-24 18:31:23 +00:00
138 changed files with 1915 additions and 1514 deletions
+2
View File
@@ -27,6 +27,7 @@ import { provideHttpClient } from '@angular/common/http';
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
import { BillingInfoComponent } from './billingInfo/billingInfo.component';
import { DomainListComponent } from './domains/domainList.component';
import { RegistryLockComponent } from './domains/registryLock.component';
import { HeaderComponent } from './header/header.component';
import { HomeComponent } from './home/home.component';
import { NavigationComponent } from './navigation/navigation.component';
@@ -71,6 +72,7 @@ import { RegistryLockVerifyComponent } from './lock/registryLockVerify.component
NotificationsComponent,
RegistrarComponent,
RegistrarDetailsComponent,
RegistryLockComponent,
RegistrarSelectorComponent,
RegistryLockVerifyComponent,
ResourcesComponent,
@@ -2,6 +2,17 @@
<div class="console-app-domains">
<h1 class="mat-headline-4">Domains</h1>
<div
class="console-app-domains__actions-wrapper"
[hidden]="!domainListService.activeActionComponent"
>
<ng-container
v-if="domainListService.activeActionComponent"
*ngComponentOutlet="domainListService.activeActionComponent"
>
</ng-container>
</div>
@if (!isLoading && totalResults == 0) {
<div class="console-app__empty-domains">
<h1>
@@ -12,7 +23,18 @@
<h1>No domains found</h1>
</div>
} @else {
<div class="console-app__domains-table-parent">
<mat-menu #actions="matMenu">
<ng-template matMenuContent let-domainName="domainName">
<button mat-menu-item (click)="openRegistryLock(domainName)">
<mat-icon>key</mat-icon>
<span>Registry Lock</span>
</button>
</ng-template>
</mat-menu>
<div
class="console-app__domains-table-parent"
[hidden]="domainListService.activeActionComponent"
>
<div class="console-app__scrollable-wrapper">
<div class="console-app__scrollable">
@if (isLoading) {
@@ -78,6 +100,29 @@
}}</mat-cell>
</ng-container>
<ng-container matColumnDef="registryLock">
<mat-header-cell *matHeaderCellDef
>Registry-Locked</mat-header-cell
>
<mat-cell *matCellDef="let element">{{
isDomainLocked(element.domainName)
}}</mat-cell>
</ng-container>
<ng-container matColumnDef="actions">
<mat-header-cell *matHeaderCellDef>Actions</mat-header-cell>
<mat-cell *matCellDef="let element">
<button
mat-icon-button
[matMenuTriggerFor]="actions"
[matMenuTriggerData]="{ domainName: element.domainName }"
aria-label="Domain actions"
>
<mat-icon>more_horiz</mat-icon>
</button>
</mat-cell>
</ng-container>
<mat-header-row
*matHeaderRowDef="displayedColumns"
></mat-header-row>
@@ -85,7 +130,7 @@
<!-- Row shown when there is no matching data. -->
<mat-row *matNoDataRow>
<mat-cell colspan="4">No domains found</mat-cell>
<mat-cell colspan="6">No domains found</mat-cell>
</mat-row>
</mat-table>
<mat-paginator
@@ -30,6 +30,12 @@
&__domains-table {
min-width: $min-width !important;
.mat-column-actions {
max-width: 100px;
}
.mat-column-registryLock {
max-width: 150px;
}
}
&__domains-spinner {
@@ -19,14 +19,14 @@ import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTableDataSource } from '@angular/material/table';
import { Subject, debounceTime } from 'rxjs';
import { RegistrarService } from '../registrar/registrar.service';
import { BackendService } from '../shared/services/backend.service';
import { Domain, DomainListService } from './domainList.service';
import { RegistryLockComponent } from './registryLock.component';
import { RegistryLockService } from './registryLock.service';
@Component({
selector: 'app-domain-list',
templateUrl: './domainList.component.html',
styleUrls: ['./domainList.component.scss'],
providers: [DomainListService],
})
export class DomainListComponent {
public static PATH = 'domain-list';
@@ -37,6 +37,8 @@ export class DomainListComponent {
'creationTime',
'registrationExpirationTime',
'statuses',
'registryLock',
'actions',
];
dataSource: MatTableDataSource<Domain> = new MatTableDataSource();
@@ -52,15 +54,16 @@ export class DomainListComponent {
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
constructor(
private backendService: BackendService,
private domainListService: DomainListService,
protected domainListService: DomainListService,
protected registrarService: RegistrarService,
protected registryLockService: RegistryLockService,
private _snackBar: MatSnackBar
) {
effect(() => {
this.pageNumber = 0;
this.totalResults = 0;
if (this.registrarService.registrarId()) {
this.loadLocks();
this.reloadData();
}
});
@@ -80,6 +83,25 @@ export class DomainListComponent {
this.searchTermSubject.complete();
}
openRegistryLock(domainName: string) {
this.domainListService.selectedDomain = domainName;
this.domainListService.activeActionComponent = RegistryLockComponent;
}
loadLocks() {
this.registryLockService.retrieveLocks().subscribe({
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.message);
},
});
}
isDomainLocked(domainName: string) {
return this.registryLockService.domainsLocks.some(
(d) => d.domainName === domainName
);
}
reloadData() {
this.isLoading = true;
this.domainListService
@@ -95,7 +117,7 @@ export class DomainListComponent {
this.isLoading = false;
},
next: (domainListResult) => {
this.dataSource.data = (domainListResult || {}).domains;
this.dataSource.data = this.domainListService.domainsList;
this.totalResults = (domainListResult || {}).totalResults || 0;
this.isLoading = false;
},
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { Injectable, Type } from '@angular/core';
import { tap } from 'rxjs';
import { RegistrarService } from '../registrar/registrar.service';
import { BackendService } from '../shared/services/backend.service';
@@ -35,9 +35,14 @@ export interface DomainListResult {
totalResults: number;
}
@Injectable()
@Injectable({
providedIn: 'root',
})
export class DomainListService {
checkpointTime?: string;
selectedDomain?: string;
public activeActionComponent: Type<any> | null = null;
public domainsList: Domain[] = [];
constructor(
private backendService: BackendService,
@@ -62,6 +67,7 @@ export class DomainListService {
.pipe(
tap((domainListResult: DomainListResult) => {
this.checkpointTime = domainListResult?.checkpointTime;
this.domainsList = domainListResult?.domains;
})
);
}
@@ -0,0 +1,81 @@
<div class="console-app__registry-lock">
<p>
<button
mat-icon-button
aria-label="Back to domains list"
(click)="goBack()"
>
<mat-icon>arrow_back</mat-icon>
</button>
</p>
@if(!registrarService.registrar()?.registryLockAllowed) {
<h1>
Sorry, your registrar hasn't enrolled in registry lock yet. To do so, please
contact {{ userDataService.userData()?.supportEmail }}.
</h1>
} @else if (isLocked()) {
<h1>Unlock the domain {{ domainListService.selectedDomain }}</h1>
<form (ngSubmit)="save(false)" [formGroup]="unlockDomain">
<p>
<mat-label for="password">Password: </mat-label>
<mat-form-field name="password" appearance="outline">
<input matInput type="text" formControlName="password" required />
</mat-form-field>
</p>
<p>
<mat-label for="relockTime"
>Automatically re-lock the domain after:</mat-label
>
<mat-radio-group
name="relockTime"
formControlName="relockTime"
aria-label="Automatically relock option"
>
@for (option of relockOptions; track option.name) {
<mat-radio-button [value]="option.duration">{{
option.name
}}</mat-radio-button>
}
</mat-radio-group>
</p>
<div class="console-app__registry-lock-notification">
<mat-icon>priority_high</mat-icon>Confirmation email will be sent to your
email address to confirm the unlock
</div>
<button
mat-flat-button
color="primary"
type="submit"
[disabled]="!unlockDomain.valid"
>
Save
</button>
</form>
} @else {
<h1>Lock the domain {{ domainListService.selectedDomain }}</h1>
<form (ngSubmit)="save(true)" [formGroup]="lockDomain">
<p>
<mat-label for="password">Password: </mat-label>
<mat-form-field name="password" appearance="outline">
<input matInput type="text" formControlName="password" required />
</mat-form-field>
</p>
<div class="console-app__registry-lock-notification">
<mat-icon>priority_high</mat-icon>The lock will not take effect until you
click the confirmation link that will be emailed to you. When it takes
effect, you will be billed the standard server status change billing cost.
</div>
<button
mat-flat-button
color="primary"
type="submit"
[disabled]="!lockDomain.valid"
>
Save
</button>
</form>
}
</div>
@@ -0,0 +1,20 @@
.console-app {
&__registry-lock {
mat-label {
display: block;
margin-bottom: 10px;
}
p {
margin-bottom: 40px;
}
}
&__registry-lock-notification {
padding: 20px;
border-radius: 10px;
background-color: var(--light-highlight);
margin-bottom: 20px;
width: max-content;
display: flex;
align-items: center;
}
}
@@ -0,0 +1,92 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { HttpErrorResponse } from '@angular/common/http';
import { Component, computed } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { MatSnackBar } from '@angular/material/snack-bar';
import { RegistrarService } from '../registrar/registrar.service';
import { UserDataService } from '../shared/services/userData.service';
import { DomainListService } from './domainList.service';
import { RegistryLockService } from './registryLock.service';
@Component({
selector: 'app-registry-lock',
templateUrl: './registryLock.component.html',
styleUrls: ['./registryLock.component.scss'],
})
export class RegistryLockComponent {
readonly isLocked = computed(() =>
this.registryLockService.domainsLocks.some(
(dl) => dl.domainName === this.domainListService.selectedDomain
)
);
relockOptions = [
{ name: '1 hour', duration: 3600000 },
{ name: '6 hours', duration: 21600000 },
{ name: '24 hours', duration: 86400000 },
{ name: 'Never', duration: undefined },
];
lockDomain = new FormGroup({
password: new FormControl(''),
});
unlockDomain = new FormGroup({
password: new FormControl(''),
relockTime: new FormControl(undefined),
});
constructor(
protected registrarService: RegistrarService,
protected domainListService: DomainListService,
protected registryLockService: RegistryLockService,
protected userDataService: UserDataService,
private _snackBar: MatSnackBar
) {}
goBack() {
this.domainListService.selectedDomain = undefined;
this.domainListService.activeActionComponent = null;
}
save(isLock: boolean) {
let request;
if (!isLock) {
request = this.registryLockService.registryLockDomain(
this.domainListService.selectedDomain || '',
this.unlockDomain.value.password || '',
this.unlockDomain.value.relockTime || undefined,
isLock
);
} else {
request = this.registryLockService.registryLockDomain(
this.domainListService.selectedDomain || '',
this.lockDomain.value.password || '',
undefined,
isLock
);
}
request.subscribe({
complete: () => {
this.goBack();
},
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error);
},
});
}
}
@@ -0,0 +1,59 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { tap } from 'rxjs';
import { RegistrarService } from '../registrar/registrar.service';
import { BackendService } from '../shared/services/backend.service';
export interface DomainLocksResult {
domainName: string;
}
@Injectable({
providedIn: 'root',
})
export class RegistryLockService {
public domainsLocks: DomainLocksResult[] = [];
constructor(
private backendService: BackendService,
private registrarService: RegistrarService
) {}
retrieveLocks() {
return this.backendService
.getLocks(this.registrarService.registrarId())
.pipe(
tap((domainLocksResult) => {
this.domainsLocks = domainLocksResult;
})
);
}
registryLockDomain(
domainName: string,
password: string,
relockDurationMillis: number | undefined,
isLock: boolean
) {
return this.backendService.registryLockDomain(
domainName,
password,
relockDurationMillis,
this.registrarService.registrarId(),
isLock
);
}
}
@@ -34,7 +34,10 @@
</mat-card-actions>
</mat-card>
<mat-card appearance="outlined">
<mat-card
appearance="outlined"
[elementId]="getElementIdForRegistrarsBlock()"
>
<mat-card-content>
<h3>
<mat-icon class="secondary-text">account_circle</mat-icon>
@@ -18,6 +18,7 @@ import { DomainListComponent } from '../domains/domainList.component';
import { RegistrarComponent } from '../registrar/registrarsTable.component';
import SecurityComponent from '../settings/security/security.component';
import { SettingsComponent } from '../settings/settings.component';
import { RESTRICTED_ELEMENTS } from '../shared/directives/userLevelVisiblity.directive';
import { BreakPointObserverService } from '../shared/services/breakPoint.service';
@Component({
@@ -30,6 +31,9 @@ export class HomeComponent {
protected breakPointObserverService: BreakPointObserverService,
private router: Router
) {}
getElementIdForRegistrarsBlock() {
return RESTRICTED_ELEMENTS.REGISTRAR_ELEMENT;
}
viewRegistrars() {
this.router.navigate([RegistrarComponent.PATH], {
queryParamsHandling: 'merge',
@@ -142,7 +142,7 @@ JPY=billing-id-for-yen"
<mat-label>State/Region: </mat-label>
<input
matInput
[required]="false"
[required]="true"
[(ngModel)]="newRegistrar.localizedAddress.state"
[ngModelOptions]="{ standalone: true }"
/>
@@ -4,7 +4,7 @@
<div class="console-app__resources-subhead">Technical resources</div>
<a
class="text-l"
href="{{ userDataService.userData.technicalDocsUrl }}"
href="{{ userDataService.userData()?.technicalDocsUrl }}"
target="_blank"
>View onboarding FAQs, TLD information, and technical documentation on
Google Drive</a
@@ -18,7 +18,11 @@
<mat-icon>edit</mat-icon>
Edit
</button>
<button mat-icon-button aria-label="Delete Contact">
<button
mat-icon-button
aria-label="Delete Contact"
(click)="deleteContact()"
>
<mat-icon>delete</mat-icon>
</button>
}
@@ -50,6 +50,9 @@ export class ContactDetailsComponent {
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error);
},
complete: () => {
this.goBack();
},
});
}
}
@@ -132,6 +132,18 @@
/>
</mat-form-field>
@if((userDataService.userData()?.globalRole || 'NONE') !== "NONE") {
<mat-form-field appearance="outline">
<mat-label>ICANN Referral Email: </mat-label>
<input
matInput
type="text"
[(ngModel)]="registrarInEdit.icannReferralEmail"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
}
<button mat-flat-button color="primary" type="submit">Save</button>
</form>
</div>
@@ -19,6 +19,7 @@ import {
Registrar,
RegistrarService,
} from 'src/app/registrar/registrar.service';
import { UserDataService } from 'src/app/shared/services/userData.service';
import { WhoisService } from './whois.service';
@Component({
@@ -30,6 +31,7 @@ export default class WhoisEditComponent {
registrarInEdit: Registrar | undefined;
constructor(
public userDataService: UserDataService,
public whoisService: WhoisService,
public registrarService: RegistrarService,
private _snackBar: MatSnackBar
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Directive, ElementRef, Input, OnChanges } from '@angular/core';
import { Directive, ElementRef, Input, effect } from '@angular/core';
import { UserDataService } from '../services/userData.service';
export enum RESTRICTED_ELEMENTS {
@@ -26,29 +26,28 @@ export const DISABLED_ELEMENTS_PER_ROLE = {
@Directive({
selector: '[elementId]',
})
export class UserLevelVisibility implements OnChanges {
export class UserLevelVisibility {
@Input() elementId!: RESTRICTED_ELEMENTS | null;
constructor(
private userDataService: UserDataService,
private el: ElementRef
) {}
ngOnChanges() {
this.processElement();
) {
effect(this.processElement.bind(this));
}
processElement() {
const globalRole = this.userDataService?.userData()?.globalRole || 'NONE';
if (this.elementId === null) {
return;
}
const globalRole = this.userDataService?.userData?.globalRole || 'NONE';
if (
// @ts-ignore
(DISABLED_ELEMENTS_PER_ROLE[globalRole] || []).includes(this.elementId)
) {
this.el.nativeElement.style.display = 'none';
} else {
this.el.nativeElement.style.display = '';
}
}
}
@@ -17,6 +17,8 @@ import { Injectable } from '@angular/core';
import { Observable, catchError, of, throwError } from 'rxjs';
import { DomainListResult } from 'src/app/domains/domainList.service';
import { DomainLocksResult } from 'src/app/domains/registryLock.service';
import { RegistryLockVerificationResponse } from 'src/app/lock/registryLockVerify.service';
import {
Registrar,
SecuritySettingsBackendModel,
@@ -25,7 +27,6 @@ import {
import { Contact } from '../../settings/contact/contact.service';
import { EppPasswordBackendModel } from '../../settings/security/security.service';
import { UserData } from './userData.service';
import { RegistryLockVerificationResponse } from 'src/app/lock/registryLockVerify.service';
@Injectable()
export class BackendService {
@@ -171,6 +172,32 @@ export class BackendService {
);
}
registryLockDomain(
domainName: string,
password: string | undefined,
relockDurationMillis: number | undefined,
registrarId: string,
isLock: boolean
) {
return this.http.post(
`/console-api/registry-lock?registrarId=${registrarId}`,
{
domainName,
password,
isLock,
relockDurationMillis,
}
);
}
getLocks(registrarId: string): Observable<DomainLocksResult[]> {
return this.http
.get<DomainLocksResult[]>(
`/console-api/registry-lock?registrarId=${registrarId}`
)
.pipe(catchError((err) => this.errorCatcher<DomainLocksResult[]>(err)));
}
verifyRegistryLockRequest(
lockVerificationCode: string
): Observable<RegistryLockVerificationResponse> {
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { Injectable, signal } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Observable, tap } from 'rxjs';
import { BackendService } from './backend.service';
@@ -33,7 +33,7 @@ export interface UserData {
providedIn: 'root',
})
export class UserDataService implements GlobalLoader {
public userData!: UserData;
userData = signal<UserData | undefined>(undefined);
constructor(
private backend: BackendService,
protected globalLoader: GlobalLoaderService,
@@ -48,7 +48,7 @@ export class UserDataService implements GlobalLoader {
getUserData(): Observable<UserData> {
return this.backend.getUserData().pipe(
tap((userData: UserData) => {
this.userData = userData;
this.userData.set(userData);
})
);
}
@@ -17,9 +17,11 @@
For general purpose questions once you are integrated with our registry
system. If the issue is urgent, please put "Urgent" in the email title.
</p>
<a class="text-l" href="mailto:{{ userDataService.userData.supportEmail }}">{{
userDataService.userData.supportEmail
}}</a>
<a
class="text-l"
href="mailto:{{ userDataService.userData()?.supportEmail }}"
>{{ userDataService.userData()?.supportEmail }}</a
>
<p class="secondary-text">
Note: You may receive occasional service announcements via
registrar-announcement&#64;google.com. You will not be able to reply to
@@ -29,13 +31,13 @@
<p class="text-l">For general support inquiries 24/7:</p>
<a
class="text-l"
href="tel:{{ userDataService.userData.supportPhoneNumber }}"
>{{ userDataService.userData.supportPhoneNumber }}</a
href="tel:{{ userDataService.userData()?.supportPhoneNumber }}"
>{{ userDataService.userData()?.supportPhoneNumber }}</a
>
@if (userDataService.userData.passcode) {
@if (userDataService.userData()?.passcode) {
<p class="text-l">Your telephone passcode:</p>
<p class="text-l console-app__support-passcode">
{{ userDataService.userData.passcode }}
{{ userDataService.userData()?.passcode }}
</p>
<p class="secondary-text">
Note: Please be ready with your account name and telephone passcode when
@@ -77,6 +77,7 @@ public class CloudTasksUtils implements Serializable {
@Config("projectId") String projectId,
@Config("locationId") String locationId,
@Config("oauthClientId") String oauthClientId,
// Note that this has to be a service account, due to limitations of the Cloud Tasks API.
@ApplicationDefaultCredential GoogleCredentialsBundle credential,
SerializableCloudTasksClient client) {
this.retrier = retrier;
@@ -64,7 +64,7 @@ import org.joda.time.Duration;
auth = Auth.AUTH_ADMIN)
public class DeleteProberDataAction implements Runnable {
// TODO(b/323026070): Add email alert on failure of this action
// TODO(b/346390641): Add email alert on failure of this action
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/**
@@ -1,17 +0,0 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<delete>
<domain:delete
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>%DOMAIN%</domain:name>
</domain:delete>
</delete>
<extension>
<metadata:metadata xmlns:metadata="urn:google:params:xml:ns:metadata-1.0">
<metadata:reason>Non-renewing domain has reached expiration date.</metadata:reason>
<metadata:requestedByRegistrar>false</metadata:requestedByRegistrar>
</metadata:metadata>
</extension>
<clTRID>ABC-12345</clTRID>
</command>
</epp>
@@ -266,7 +266,6 @@ public final class DomainCreateFlow implements MutatingFlow {
validateLaunchCreateNotice(launchCreate.get().getNotice(), domainLabel, isSuperuser, now);
}
boolean isSunriseCreate = hasSignedMarks && (tldState == START_DATE_SUNRISE);
// TODO(sarahbot@): Add check for valid EPP actions on the token
Optional<AllocationToken> allocationToken =
allocationTokenFlowUtils.verifyAllocationTokenCreateIfPresent(
command,
@@ -14,7 +14,6 @@
package google.registry.flows.domain;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
import static com.google.common.collect.Sets.symmetricDifference;
@@ -282,7 +281,7 @@ public final class DomainUpdateFlow implements MutatingFlow {
.removeContacts(remove.getContacts())
.addContacts(add.getContacts())
.setRegistrant(determineUpdatedRegistrant(change, domain))
.setAuthInfo(firstNonNull(change.getAuthInfo(), domain.getAuthInfo()));
.setAuthInfo(Optional.ofNullable(change.getAuthInfo()).orElse(domain.getAuthInfo()));
if (!add.getNameservers().isEmpty()) {
domainBuilder.addNameservers(add.getNameservers().stream().collect(toImmutableSet()));
@@ -32,7 +32,6 @@ import com.google.common.collect.Streams;
import google.registry.batch.CloudTasksUtils;
import google.registry.model.console.RegistrarRole;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.model.console.UserRoles;
import google.registry.model.pricing.StaticPremiumListPricingEngine;
import google.registry.model.registrar.Registrar;
@@ -48,9 +47,7 @@ import google.registry.util.CidrAddressBlock;
import google.registry.util.RegistryEnvironment;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.regex.Pattern;
@@ -271,29 +268,21 @@ public final class OteAccountBuilder {
Optional<String> groupEmailAddress, CloudTasksUtils cloudTasksUtils, IamClient iamClient) {
for (User user : users) {
User.grantIapPermission(
user.getEmailAddress(), groupEmailAddress, cloudTasksUtils, iamClient);
user.getEmailAddress(), groupEmailAddress, cloudTasksUtils, null, iamClient);
}
}
/** Saves all the OT&amp;E entities we created. */
private void saveAllEntities() {
ImmutableList<Tld> registries = ImmutableList.of(sunriseTld, gaTld, eapTld);
Map<String, User> existingUsers = new HashMap<>();
users.forEach(
user ->
UserDao.loadUser(user.getEmailAddress())
.ifPresent(
existingUser ->
existingUsers.put(existingUser.getEmailAddress(), existingUser)));
if (!replaceExisting) {
checkState(existingUsers.isEmpty(), "Found existing users: %s", existingUsers);
}
tm().transact(
() -> {
if (!replaceExisting) {
ImmutableMap<String, User> existingUsers =
tm().loadByEntitiesIfPresent(users).stream()
.collect(toImmutableMap(User::getEmailAddress, u -> u));
checkState(existingUsers.isEmpty(), "Found existing users: %s", existingUsers);
ImmutableList<VKey<? extends ImmutableObject>> keys =
Streams.concat(
registries.stream().map(tld -> Tld.createVKey(tld.getTldStr())),
@@ -317,16 +306,7 @@ public final class OteAccountBuilder {
tm().putAll(registrars);
});
for (User user : users) {
String email = user.getEmailAddress();
if (existingUsers.containsKey(email)) {
// Note that other roles for the existing user are reset. We do this instead of simply
// saving the new user is that UserDao does not allow us to save the new user with the same
// email as the existing user.
user = existingUsers.get(email).asBuilder().setUserRoles(user.getUserRoles()).build();
}
UserDao.saveUser(user);
}
tm().transact(() -> tm().putAll(ImmutableList.copyOf(users)));
}
private Registrar addAllowedTld(Registrar registrar) {
@@ -63,7 +63,8 @@ public class FeatureFlag extends ImmutableObject implements Buildable {
public enum FeatureName {
TEST_FEATURE,
MINIMUM_DATASET_CONTACTS_OPTIONAL,
MINIMUM_DATASET_CONTACTS_PROHIBITED
MINIMUM_DATASET_CONTACTS_PROHIBITED,
NEW_CONSOLE
}
/** The name of the flag/feature. */
@@ -14,33 +14,36 @@
package google.registry.model.console;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.tools.server.UpdateUserGroupAction.GROUP_UPDATE_QUEUE;
import com.google.cloud.tasks.v2.Task;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import google.registry.batch.CloudTasksUtils;
import google.registry.persistence.VKey;
import google.registry.request.Action.Service;
import google.registry.tools.IamClient;
import google.registry.tools.ServiceConnection;
import google.registry.tools.server.UpdateUserGroupAction;
import google.registry.tools.server.UpdateUserGroupAction.Mode;
import google.registry.util.RegistryEnvironment;
import java.io.IOException;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Embeddable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.Table;
/** A console user, either a registry employee or a registrar partner. */
@Embeddable
@Entity
@Table(indexes = {@Index(columnList = "emailAddress", name = "user_email_address_idx")})
@Table
public class User extends UserBase {
public static final String IAP_SECURED_WEB_APP_USER_ROLE = "roles/iap.httpsResourceAccessor";
@@ -55,18 +58,30 @@ public class User extends UserBase {
public static void grantIapPermission(
String emailAddress,
Optional<String> groupEmailAddress,
CloudTasksUtils cloudTasksUtils,
@Nullable CloudTasksUtils cloudTasksUtils,
@Nullable ServiceConnection connection,
IamClient iamClient) {
if (RegistryEnvironment.isInTestServer()) {
return;
}
checkArgument(
cloudTasksUtils != null || connection != null,
"At least one of cloudTasksUtils or connection must be set");
checkArgument(
cloudTasksUtils == null || connection == null,
"Only one of cloudTasksUtils or connection can be set");
if (groupEmailAddress.isEmpty()) {
logger.atInfo().log("Granting IAP role to user %s", emailAddress);
iamClient.addBinding(emailAddress, IAP_SECURED_WEB_APP_USER_ROLE);
} else {
logger.atInfo().log("Adding %s to group %s", emailAddress, groupEmailAddress.get());
modifyGroupMembershipAsync(
emailAddress, groupEmailAddress.get(), cloudTasksUtils, UpdateUserGroupAction.Mode.ADD);
if (cloudTasksUtils != null) {
modifyGroupMembershipAsync(
emailAddress, groupEmailAddress.get(), cloudTasksUtils, UpdateUserGroupAction.Mode.ADD);
} else {
modifyGroupMembershipSync(
emailAddress, groupEmailAddress.get(), connection, UpdateUserGroupAction.Mode.ADD);
}
}
}
@@ -79,18 +94,29 @@ public class User extends UserBase {
public static void revokeIapPermission(
String emailAddress,
Optional<String> groupEmailAddress,
CloudTasksUtils cloudTasksUtils,
@Nullable CloudTasksUtils cloudTasksUtils,
@Nullable ServiceConnection connection,
IamClient iamClient) {
if (RegistryEnvironment.isInTestServer()) {
return;
}
checkArgument(
cloudTasksUtils != null || connection != null,
"At least one of cloudTasksUtils or connection must be set");
checkArgument(
cloudTasksUtils == null || connection == null,
"Only one of cloudTasksUtils or connection can be set");
if (groupEmailAddress.isEmpty()) {
logger.atInfo().log("Removing IAP role from user %s", emailAddress);
iamClient.removeBinding(emailAddress, IAP_SECURED_WEB_APP_USER_ROLE);
} else {
logger.atInfo().log("Removing %s from group %s", emailAddress, groupEmailAddress.get());
modifyGroupMembershipAsync(
emailAddress, groupEmailAddress.get(), cloudTasksUtils, Mode.REMOVE);
if (cloudTasksUtils != null) {
modifyGroupMembershipAsync(
emailAddress, groupEmailAddress.get(), cloudTasksUtils, Mode.REMOVE);
} else {
modifyGroupMembershipSync(emailAddress, groupEmailAddress.get(), connection, Mode.REMOVE);
}
}
}
@@ -113,12 +139,30 @@ public class User extends UserBase {
cloudTasksUtils.enqueue(GROUP_UPDATE_QUEUE, task);
}
@Override
private static void modifyGroupMembershipSync(
String userEmailAddress, String groupEmailAddress, ServiceConnection connection, Mode mode) {
try {
connection.sendPostRequest(
UpdateUserGroupAction.PATH,
ImmutableMap.of(
"userEmailAddress",
userEmailAddress,
"groupEmailAddress",
groupEmailAddress,
"groupUpdateMode",
mode.name()),
MediaType.PLAIN_TEXT_UTF_8,
new byte[0]);
} catch (IOException e) {
throw new RuntimeException("Cannot send request to server", e);
}
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Override
@Access(AccessType.PROPERTY)
public Long getId() {
return super.getId();
public String getEmailAddress() {
return super.getEmailAddress();
}
@Override
@@ -128,7 +172,7 @@ public class User extends UserBase {
@Override
public VKey<User> createVKey() {
return VKey.create(User.class, getId());
return VKey.create(User.class, getEmailAddress());
}
/** Builder for constructing immutable {@link User} objects. */
@@ -49,12 +49,8 @@ public class UserBase extends UpdateAutoTimestampEntity implements Buildable {
private static final long serialVersionUID = 6936728603828566721L;
/** Autogenerated unique ID of this user. */
@Transient private Long id;
/** Email address of the user in question. */
@Column(nullable = false)
String emailAddress;
@Transient String emailAddress;
/** Optional external email address to use for registry lock confirmation emails. */
@Column String registryLockEmailAddress;
@@ -72,22 +68,17 @@ public class UserBase extends UpdateAutoTimestampEntity implements Buildable {
/** Randomly generated hash salt. */
String registryLockPasswordSalt;
public Long getId() {
return id;
}
/**
* Sets the user ID.
* Sets the user email address.
*
* <p>This should only be used for restoring the user id of an object being loaded in a PostLoad
* method (effectively, when it is still under construction by Hibernate). In all other cases, the
* object should be regarded as immutable and changes should go through a Builder.
* <p>This should only be used for restoring an object being loaded in a PostLoad method
* (effectively, when it is still under construction by Hibernate). In all other cases, the object
* should be regarded as immutable and changes should go through a Builder.
*
* <p>In addition to this special case use, this method must exist to satisfy Hibernate.
*/
@SuppressWarnings("unused")
void setId(Long id) {
this.id = id;
void setEmailAddress(String emailAddress) {
this.emailAddress = emailAddress;
}
public String getEmailAddress() {
@@ -1,54 +0,0 @@
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.console;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import java.util.Optional;
/** Data access object for {@link User} objects to simplify saving and retrieval. */
public class UserDao {
/** Retrieves the one user with this email address if it exists. */
public static Optional<User> loadUser(String emailAddress) {
return tm().transact(
() ->
tm().query("FROM User WHERE emailAddress = :emailAddress", User.class)
.setParameter("emailAddress", emailAddress)
.getResultStream()
.findFirst());
}
/** Saves the given user, checking that no existing user already exists with this email. */
public static void saveUser(User user) {
tm().transact(
() -> {
// Check for an existing user (the unique constraint protects us, but this gives a
// nicer exception)
Optional<User> maybeSavedUser = loadUser(user.getEmailAddress());
if (maybeSavedUser.isPresent()) {
User savedUser = maybeSavedUser.get();
checkArgument(
savedUser.getId().equals(user.getId()),
String.format(
"Attempted save of User with email address %s and ID %s, user with that"
+ " email already exists with ID %s",
user.getEmailAddress(), user.getId(), savedUser.getId()));
}
tm().put(user);
});
}
}
@@ -38,9 +38,8 @@ public class UserUpdateHistory extends ConsoleUpdateHistory {
UserBase user;
// This field exists so that it's populated in the SQL table
@Column(nullable = false, name = "userId")
Long id;
@Column(nullable = false, name = "emailAddress")
String emailAddress;
public UserBase getUser() {
return user;
@@ -48,7 +47,7 @@ public class UserUpdateHistory extends ConsoleUpdateHistory {
@PostLoad
void postLoad() {
user.setId(id);
user.setEmailAddress(emailAddress);
}
/** Creates a {@link VKey} instance for this entity. */
@@ -79,7 +78,7 @@ public class UserUpdateHistory extends ConsoleUpdateHistory {
public Builder setUser(User user) {
getInstance().user = user;
getInstance().id = user.getId();
getInstance().emailAddress = user.getEmailAddress();
return this;
}
}
@@ -144,6 +144,7 @@ public class DomainBase extends EppResource
@AttributeOverride(name = "pw.value", column = @Column(name = "auth_info_value")),
@AttributeOverride(name = "pw.repoId", column = @Column(name = "auth_info_repo_id")),
})
@Nullable
DomainAuthInfo authInfo;
/** Data used to construct DS records for this domain. */
@@ -620,6 +621,7 @@ public class DomainBase extends EppResource
return getAllContacts(true);
}
@Nullable
public DomainAuthInfo getAuthInfo() {
return authInfo;
}
@@ -139,7 +139,7 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda
/** Token saved on a TLD to use if no other token is passed from the client */
DEFAULT_PROMO(/* isOneTimeUse= */ false),
/** This is the old name for what is now BULK_PRICING. */
// TODO(sarahbot@): Remove this type once all tokens of this type have been scrubbed from the
// TODO(b/261763205): Remove this type once all tokens of this type have been scrubbed from the
// database
@Deprecated
PACKAGE(/* isOneTimeUse= */ false),
@@ -16,6 +16,7 @@ package google.registry.model.eppcommon;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import javax.annotation.Nullable;
import javax.persistence.Embeddable;
import javax.persistence.Embedded;
import javax.persistence.MappedSuperclass;
@@ -49,10 +50,12 @@ public abstract class AuthInfo extends ImmutableObject implements UnsafeSerializ
public static class PasswordAuth extends ImmutableObject implements UnsafeSerializable {
@XmlValue
@XmlJavaTypeAdapter(NormalizedStringAdapter.class)
@Nullable
String value;
@XmlAttribute(name = "roid")
@XmlJavaTypeAdapter(CollapsedStringAdapter.class)
@Nullable
String repoId;
public String getValue() {
@@ -63,14 +66,14 @@ public abstract class AuthInfo extends ImmutableObject implements UnsafeSerializ
return repoId;
}
public static PasswordAuth create(String value, String repoId) {
public static PasswordAuth create(@Nullable String value, @Nullable String repoId) {
PasswordAuth instance = new PasswordAuth();
instance.value = value;
instance.repoId = repoId;
return instance;
}
public static PasswordAuth create(String value) {
public static PasswordAuth create(@Nullable String value) {
return create(value, null);
}
}
@@ -25,8 +25,6 @@ import javax.persistence.AccessType;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Index;
import javax.persistence.Table;
/**
* A contact for a Registrar. Note, equality, hashCode and comparable have been overridden to only
@@ -37,7 +35,6 @@ import javax.persistence.Table;
* set to true.
*/
@Entity
@Table(indexes = @Index(columnList = "loginEmailAddress", name = "registrarpoc_login_email_idx"))
@IdClass(RegistrarPocId.class)
@Access(AccessType.FIELD)
public class RegistrarPoc extends RegistrarPocBase {
@@ -98,12 +98,7 @@ public class RegistrarPocBase extends ImmutableObject implements Jsonifiable, Un
/** The name of the contact. */
@Expose String name;
/**
* The contact email address of the contact.
*
* <p>This is different from the login email which is assgined to the regstrar and cannot be
* changed.
*/
/** The contact email address of the contact. */
@Expose @Transient String emailAddress;
@Expose @Transient public String registrarId;
@@ -123,9 +118,6 @@ public class RegistrarPocBase extends ImmutableObject implements Jsonifiable, Un
*/
@Expose Set<Type> types;
/** A GAIA email address that was assigned to the registrar for console login purpose. */
String loginEmailAddress;
/**
* Whether this contact is publicly visible in WHOIS registrar query results as an Admin contact.
*/
@@ -219,10 +211,6 @@ public class RegistrarPocBase extends ImmutableObject implements Jsonifiable, Un
return visibleInDomainWhoisAsAbuse;
}
public String getLoginEmailAddress() {
return loginEmailAddress;
}
public Builder<? extends RegistrarPocBase, ?> asBuilder() {
return new Builder<>(clone(this));
}
@@ -286,13 +274,6 @@ public class RegistrarPocBase extends ImmutableObject implements Jsonifiable, Un
+ "Registrar Abuse contact info: ")
.append(getVisibleInDomainWhoisAsAbuse() ? "Yes" : "No")
.append('\n');
result
.append("Registrar-Console access: ")
.append(getLoginEmailAddress() != null ? "Yes" : "No")
.append('\n');
if (getLoginEmailAddress() != null) {
result.append("Login Email Address: ").append(getLoginEmailAddress()).append('\n');
}
return result.toString();
}
@@ -310,7 +291,6 @@ public class RegistrarPocBase extends ImmutableObject implements Jsonifiable, Un
.put("visibleInDomainWhoisAsAbuse", visibleInDomainWhoisAsAbuse)
.put("allowedToSetRegistryLockPassword", allowedToSetRegistryLockPassword)
.put("registryLockAllowed", isRegistryLockAllowed())
.put("loginEmailAddress", loginEmailAddress)
.build();
}
@@ -411,11 +391,6 @@ public class RegistrarPocBase extends ImmutableObject implements Jsonifiable, Un
return thisCastToDerived();
}
public B setLoginEmailAddress(String loginEmailAddress) {
getInstance().loginEmailAddress = loginEmailAddress;
return thisCastToDerived();
}
public B setAllowedToSetRegistryLockPassword(boolean allowedToSetRegistryLockPassword) {
if (allowedToSetRegistryLockPassword) {
getInstance().registryLockPasswordSalt = null;
@@ -15,6 +15,7 @@
package google.registry.request.auth;
import static com.google.common.base.Preconditions.checkState;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.api.client.json.webtoken.JsonWebSignature;
import com.google.auth.oauth2.TokenVerifier;
@@ -23,7 +24,7 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.persistence.VKey;
import google.registry.request.auth.AuthModule.IapOidc;
import google.registry.request.auth.AuthModule.RegularOidc;
import google.registry.request.auth.AuthModule.RegularOidcFallback;
@@ -117,7 +118,8 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
logger.atWarning().log("No email address from the OIDC token:\n%s", token.getPayload());
return AuthResult.NOT_AUTHENTICATED;
}
Optional<User> maybeUser = UserDao.loadUser(email);
Optional<User> maybeUser =
tm().transact(() -> tm().loadByKeyIfPresent(VKey.create(User.class, email)));
if (maybeUser.isPresent()) {
return AuthResult.createUser(maybeUser.get());
}
@@ -22,7 +22,6 @@ import com.google.common.collect.ImmutableMap;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.RegistrarRole;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.model.console.UserRoles;
import google.registry.tools.params.KeyValueMapParameter.StringToRegistrarRoleMap;
import java.util.Optional;
@@ -95,8 +94,6 @@ public abstract class CreateOrUpdateUserCommand extends ConfirmingCommand {
builder.setRegistryLockEmailAddress(registryLockEmailAddress);
}
}
User newUser = builder.build();
UserDao.saveUser(newUser);
tm().put(builder.build());
}
}
@@ -16,24 +16,24 @@ package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.model.console.User.grantIapPermission;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.beust.jcommander.Parameters;
import google.registry.batch.CloudTasksUtils;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.persistence.VKey;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.inject.Inject;
/** Command to create a new User. */
@Parameters(separators = " =", commandDescription = "Update a user account")
public class CreateUserCommand extends CreateOrUpdateUserCommand {
public class CreateUserCommand extends CreateOrUpdateUserCommand implements CommandWithConnection {
private ServiceConnection connection;
@Inject IamClient iamClient;
@Inject CloudTasksUtils cloudTasksUtils;
@Inject
@Config("gSuiteConsoleUserGroupEmailAddress")
Optional<String> maybeGroupEmailAddress;
@@ -41,14 +41,20 @@ public class CreateUserCommand extends CreateOrUpdateUserCommand {
@Nullable
@Override
User getExistingUser(String email) {
checkArgument(UserDao.loadUser(email).isEmpty(), "A user with email %s already exists", email);
checkArgument(
!tm().exists(VKey.create(User.class, email)), "A user with email %s already exists", email);
return null;
}
@Override
protected String execute() throws Exception {
String ret = super.execute();
grantIapPermission(email, maybeGroupEmailAddress, cloudTasksUtils, iamClient);
grantIapPermission(email, maybeGroupEmailAddress, null, connection, iamClient);
return ret;
}
@Override
public void setConnection(ServiceConnection connection) {
this.connection = connection;
}
}
@@ -14,28 +14,27 @@
package google.registry.tools;
import static com.google.api.client.util.Preconditions.checkArgument;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import google.registry.batch.CloudTasksUtils;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.persistence.VKey;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.inject.Inject;
/** Deletes a {@link User}. */
@Parameters(separators = " =", commandDescription = "Delete a user account")
public class DeleteUserCommand extends ConfirmingCommand {
public class DeleteUserCommand extends ConfirmingCommand implements CommandWithConnection {
private ServiceConnection connection;
@Inject IamClient iamClient;
@Inject CloudTasksUtils cloudTasksUtils;
@Inject
@Config("gSuiteConsoleUserGroupEmailAddress")
Optional<String> maybeGroupEmailAddress;
@@ -47,7 +46,9 @@ public class DeleteUserCommand extends ConfirmingCommand {
@Override
protected String prompt() {
checkArgumentNotNull(email, "Email must be provided");
checkArgumentPresent(UserDao.loadUser(email), "Email does not correspond to a valid user");
checkArgument(
tm().transact(() -> tm().exists(VKey.create(User.class, email))),
"Email does not correspond to a valid user");
return String.format("Delete user with email %s?", email);
}
@@ -55,11 +56,16 @@ public class DeleteUserCommand extends ConfirmingCommand {
protected String execute() throws Exception {
tm().transact(
() -> {
Optional<User> optionalUser = UserDao.loadUser(email);
checkArgumentPresent(optionalUser, "Email no longer corresponds to a valid user");
tm().delete(optionalUser.get());
VKey<User> key = VKey.create(User.class, email);
checkArgument(tm().exists(key), "Email no longer corresponds to a valid user");
tm().delete(key);
});
User.revokeIapPermission(email, maybeGroupEmailAddress, cloudTasksUtils, iamClient);
User.revokeIapPermission(email, maybeGroupEmailAddress, null, connection, iamClient);
return String.format("Deleted user with email %s", email);
}
@Override
public void setConnection(ServiceConnection connection) {
this.connection = connection;
}
}
@@ -293,7 +293,7 @@ class GenerateAllocationTokensCommand implements Command {
// tokens should only be scheduled to end with a brief time period before the status
// transition occurs so that no new domains are registered using that token between when the
// status is scheduled and when the transition occurs.
// TODO(@sarahbot): Create a cleaner way to handle ending bulk pricing packages once we
// TODO(b/261763205): Create a cleaner way to handle ending bulk pricing packages once we
// actually have customers using them
boolean hasEnding =
tokenStatusTransitions.containsValue(TokenStatus.ENDED)
@@ -14,10 +14,12 @@
package google.registry.tools;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.persistence.VKey;
import java.util.List;
/** Command to display one or more users. */
@@ -29,11 +31,14 @@ public class GetUserCommand implements Command {
@Override
public void run() throws Exception {
for (String emailAddress : mainParameters) {
System.out.println(
UserDao.loadUser(emailAddress)
.map(User::toString)
.orElse(String.format("No user with email address %s", emailAddress)));
}
tm().transact(
() -> {
for (String emailAddress : mainParameters) {
System.out.println(
tm().loadByKeyIfPresent(VKey.create(User.class, emailAddress))
.map(User::toString)
.orElse(String.format("No user with email address %s", emailAddress)));
}
});
}
}
@@ -41,7 +41,6 @@ import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nullable;
@@ -87,12 +86,6 @@ final class RegistrarPocCommand extends MutatingCommand {
+ " and will be used as the console login email, if --login_email is not specified.")
String email;
@Nullable
@Parameter(
names = "--login_email",
description = "Console login email address. If not specified, --email will be used.")
String loginEmail;
@Nullable
@Parameter(
names = "--registry_lock_email",
@@ -115,13 +108,6 @@ final class RegistrarPocCommand extends MutatingCommand {
validateWith = OptionalPhoneNumberParameter.class)
private Optional<String> fax;
@Nullable
@Parameter(
names = "--allow_console_access",
description = "Enable or disable access to the registrar console for this contact.",
arity = 1)
Boolean allowConsoleAccess;
@Nullable
@Parameter(
names = "--visible_in_whois_as_admin",
@@ -173,7 +159,7 @@ final class RegistrarPocCommand extends MutatingCommand {
protected void init() throws Exception {
checkArgument(mainParameters.size() == 1,
"Must specify exactly one client identifier: %s", ImmutableList.copyOf(mainParameters));
String clientId = mainParameters.get(0);
String clientId = mainParameters.getFirst();
Registrar registrar =
checkArgumentPresent(
Registrar.loadByRegistrarId(clientId), "Registrar %s not found", clientId);
@@ -261,9 +247,6 @@ final class RegistrarPocCommand extends MutatingCommand {
}
builder.setTypes(nullToEmpty(contactTypes));
if (Objects.equals(allowConsoleAccess, Boolean.TRUE)) {
builder.setLoginEmailAddress(loginEmail == null ? email : loginEmail);
}
if (visibleInWhoisAsAdmin != null) {
builder.setVisibleInWhoisAsAdmin(visibleInWhoisAsAdmin);
}
@@ -308,13 +291,6 @@ final class RegistrarPocCommand extends MutatingCommand {
if (visibleInDomainWhoisAsAbuse != null) {
builder.setVisibleInDomainWhoisAsAbuse(visibleInDomainWhoisAsAbuse);
}
if (allowConsoleAccess != null) {
if (allowConsoleAccess.equals(Boolean.TRUE)) {
builder.setLoginEmailAddress(loginEmail == null ? email : loginEmail);
} else {
builder.setLoginEmailAddress(null);
}
}
if (allowedToSetRegistryLockPassword != null) {
builder.setAllowedToSetRegistryLockPassword(allowedToSetRegistryLockPassword);
}
@@ -14,20 +14,20 @@
package google.registry.tools;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
import com.beust.jcommander.Parameters;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import javax.annotation.Nullable;
import google.registry.persistence.VKey;
/** Updates a user, assuming that the user in question already exists. */
@Parameters(separators = " =", commandDescription = "Update a user account")
public class UpdateUserCommand extends CreateOrUpdateUserCommand {
@Nullable
@Override
User getExistingUser(String email) {
return checkArgumentPresent(UserDao.loadUser(email), "User %s not found", email);
return checkArgumentPresent(
tm().loadByKeyIfPresent(VKey.create(User.class, email)), "User %s not found", email);
}
}
@@ -193,9 +193,6 @@ public final class RegistrarFormFields {
public static final FormField<String, String> CONTACT_FAX_NUMBER_FIELD =
FormFields.PHONE_NUMBER.asBuilderNamed("faxNumber").build();
public static final FormField<String, String> CONTACT_LOGIN_EMAIL_ADDRESS_FIELD =
FormFields.NAME.asBuilderNamed("loginEmailAddress").build();
public static final FormField<Object, Boolean> CONTACT_ALLOWED_TO_SET_REGISTRY_LOCK_PASSWORD =
FormField.named("allowedToSetRegistryLockPassword", Object.class)
.transform(Boolean.class, b -> Boolean.valueOf(Objects.toString(b)))
@@ -384,8 +381,6 @@ public final class RegistrarFormFields {
builder.setPhoneNumber(CONTACT_PHONE_NUMBER_FIELD.extractUntyped(args).orElse(null));
builder.setFaxNumber(CONTACT_FAX_NUMBER_FIELD.extractUntyped(args).orElse(null));
builder.setTypes(CONTACT_TYPES.extractUntyped(args).orElse(ImmutableSet.of()));
builder.setLoginEmailAddress(
CONTACT_LOGIN_EMAIL_ADDRESS_FIELD.extractUntyped(args).orElse(null));
// The parser is inconsistent with whether it retrieves boolean values as strings or booleans.
// As a result, use a potentially-redundant converter that can deal with both.
builder.setAllowedToSetRegistryLockPassword(
@@ -16,6 +16,9 @@ package google.registry.ui.server.console;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.model.common.FeatureFlag.FeatureName.NEW_CONSOLE;
import static google.registry.model.common.FeatureFlag.isActiveNow;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.GET;
import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
@@ -29,10 +32,12 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
import google.registry.batch.CloudTasksUtils;
import google.registry.config.RegistryConfig;
import google.registry.export.sheet.SyncRegistrarsSheetAction;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.registrar.RegistrarPocBase;
@@ -62,6 +67,10 @@ public abstract class ConsoleApiAction implements Runnable {
@Inject CloudTasksUtils cloudTasksUtils;
@Inject
@RegistryConfig.Config("registryAdminClientId")
String registryAdminClientId;
public ConsoleApiAction(ConsoleApiParams consoleApiParams) {
this.consoleApiParams = consoleApiParams;
}
@@ -77,8 +86,15 @@ public abstract class ConsoleApiAction implements Runnable {
// This allows us to enable console to a selected cohort of users with release
// We can ignore it in tests
if (RegistryEnvironment.get() != RegistryEnvironment.UNITTEST
&& GlobalRole.NONE.equals(user.getUserRoles().getGlobalRole())) {
UserRoles userRoles = user.getUserRoles();
boolean hasGlobalOrTestingRole =
!GlobalRole.NONE.equals(userRoles.getGlobalRole())
|| userRoles.hasPermission(
registryAdminClientId, ConsolePermission.VIEW_REGISTRAR_DETAILS);
if (!hasGlobalOrTestingRole
&& RegistryEnvironment.get() != RegistryEnvironment.UNITTEST
&& tm().transact(() -> !isActiveNow(NEW_CONSOLE))) {
try {
consoleApiParams.response().sendRedirect(ConsoleUiAction.PATH);
return;
@@ -115,6 +115,7 @@ public class ConsoleDomainListAction extends ConsoleApiAction {
.setFirstResult(numResultsToSkip)
.setMaxResults(resultsPerPage)
.getResultList();
consoleApiParams
.response()
.setPayload(gson.toJson(new DomainListResult(domains, checkpoint, actualTotalResults)));
@@ -19,16 +19,13 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.POST;
import static google.registry.request.RequestParameters.extractBooleanParameter;
import static google.registry.request.RequestParameters.extractOptionalLongParameter;
import static google.registry.request.RequestParameters.extractOptionalParameter;
import static google.registry.request.RequestParameters.extractRequiredParameter;
import static google.registry.ui.server.registrar.RegistryLockPostAction.VERIFICATION_EMAIL_TEMPLATE;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import com.google.common.collect.ImmutableList;
import com.google.gson.Gson;
import com.google.gson.annotations.Expose;
import google.registry.flows.EppException;
import google.registry.flows.domain.DomainFlowUtils;
import google.registry.groups.GmailClient;
@@ -46,8 +43,8 @@ import google.registry.ui.server.registrar.ConsoleApiParams;
import google.registry.util.EmailMessage;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.inject.Inject;
import org.joda.time.Duration;
@@ -69,6 +66,7 @@ public class ConsoleRegistryLockAction extends ConsoleApiAction {
private final DomainLockUtils domainLockUtils;
private final GmailClient gmailClient;
private final Gson gson;
private final Optional<ConsoleRegistryLockPostInput> optionalPostInput;
private final String registrarId;
@Inject
@@ -77,11 +75,14 @@ public class ConsoleRegistryLockAction extends ConsoleApiAction {
DomainLockUtils domainLockUtils,
GmailClient gmailClient,
Gson gson,
@Parameter("consoleRegistryLockPostInput")
Optional<ConsoleRegistryLockPostInput> optionalPostInput,
@Parameter("registrarId") String registrarId) {
super(consoleApiParams);
this.domainLockUtils = domainLockUtils;
this.gmailClient = gmailClient;
this.gson = gson;
this.optionalPostInput = optionalPostInput;
this.registrarId = registrarId;
}
@@ -94,7 +95,6 @@ public class ConsoleRegistryLockAction extends ConsoleApiAction {
@Override
protected void postHandler(User user) {
HttpServletRequest req = consoleApiParams.request();
Response response = consoleApiParams.response();
// User must have the proper permission on the registrar
checkPermission(user, registrarId, ConsolePermission.REGISTRY_LOCK);
@@ -107,10 +107,12 @@ public class ConsoleRegistryLockAction extends ConsoleApiAction {
registrarId);
// Retrieve and validate the necessary params
String domainName = extractRequiredParameter(req, "domainName");
boolean isLock = extractBooleanParameter(req, "isLock");
Optional<String> maybePassword = extractOptionalParameter(req, "password");
Optional<Long> relockDurationMillis = extractOptionalLongParameter(req, "relockDurationMillis");
ConsoleRegistryLockPostInput postInput =
optionalPostInput.orElseThrow(() -> new IllegalArgumentException("No POST input provided"));
String domainName = postInput.domainName();
boolean isLock = postInput.isLock();
Optional<String> maybePassword = Optional.ofNullable(postInput.password());
Optional<Long> relockDurationMillis = Optional.ofNullable(postInput.relockDurationMillis());
try {
DomainFlowUtils.validateDomainName(domainName);
@@ -176,4 +178,10 @@ public class ConsoleRegistryLockAction extends ConsoleApiAction {
.filter(lock -> !lock.isLockRequestExpired(tm().getTransactionTime()))
.collect(toImmutableList()));
}
public record ConsoleRegistryLockPostInput(
@Expose String domainName,
@Expose boolean isLock,
@Expose @Nullable String password,
@Expose @Nullable Long relockDurationMillis) {}
}
@@ -60,7 +60,7 @@ public class ConsoleRegistryLockVerifyAction extends ConsoleApiAction {
RegistryLock lock =
domainLockUtils.verifyVerificationCode(lockVerificationCode, user.getUserRoles().isAdmin());
RegistryLockAction action =
lock.getLockCompletionTime().isPresent()
lock.getUnlockCompletionTime().isPresent()
? RegistryLockAction.UNLOCKED
: RegistryLockAction.LOCKED;
RegistryLockVerificationResponse lockResponse =
@@ -16,6 +16,7 @@ package google.registry.ui.server.console;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.POST;
@@ -23,6 +24,7 @@ import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
import com.google.gson.Gson;
import google.registry.model.console.ConsolePermission;
@@ -36,6 +38,8 @@ import google.registry.request.Parameter;
import google.registry.request.auth.Auth;
import google.registry.ui.server.registrar.ConsoleApiParams;
import google.registry.util.StringGenerator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Named;
@@ -50,6 +54,11 @@ public class RegistrarsAction extends ConsoleApiAction {
private static final int PASSCODE_LENGTH = 5;
private static final ImmutableList<RegistrarBase.Type> allowedRegistrarTypes =
ImmutableList.of(Registrar.Type.REAL, RegistrarBase.Type.OTE);
private static final String SQL_TEMPLATE =
"""
SELECT * FROM "Registrar"
WHERE registrar_id in :registrarIds
""";
static final String PATH = "/console-api/registrars";
private final Gson gson;
private final Optional<Registrar> registrar;
@@ -72,18 +81,34 @@ public class RegistrarsAction extends ConsoleApiAction {
@Override
protected void getHandler(User user) {
if (!user.getUserRoles().hasGlobalPermission(ConsolePermission.VIEW_REGISTRARS)) {
if (user.getUserRoles().hasGlobalPermission(ConsolePermission.VIEW_REGISTRARS)) {
ImmutableList<Registrar> registrars =
Streams.stream(Registrar.loadAll())
.filter(r -> allowedRegistrarTypes.contains(r.getType()))
.collect(ImmutableList.toImmutableList());
consoleApiParams.response().setPayload(gson.toJson(registrars));
consoleApiParams.response().setStatus(SC_OK);
} else if (user.getUserRoles().getRegistrarRoles().values().stream()
.anyMatch(role -> role.hasPermission(ConsolePermission.VIEW_REGISTRAR_DETAILS))) {
ImmutableSet<String> accessibleRegistrarIds =
user.getUserRoles().getRegistrarRoles().entrySet().stream()
.filter(e -> e.getValue().hasPermission(ConsolePermission.VIEW_REGISTRAR_DETAILS))
.map(Map.Entry::getKey)
.collect(toImmutableSet());
List<Registrar> registrars =
tm().transact(
() ->
tm().getEntityManager()
.createNativeQuery(SQL_TEMPLATE, Registrar.class)
.setParameter("registrarIds", accessibleRegistrarIds)
.getResultList());
consoleApiParams.response().setPayload(gson.toJson(registrars));
consoleApiParams.response().setStatus(SC_OK);
} else {
consoleApiParams.response().setStatus(SC_FORBIDDEN);
return;
}
ImmutableList<Registrar> registrars =
Streams.stream(Registrar.loadAll())
.filter(r -> allowedRegistrarTypes.contains(r.getType()))
.collect(ImmutableList.toImmutableList());
consoleApiParams.response().setPayload(gson.toJson(registrars));
consoleApiParams.response().setStatus(SC_OK);
}
@Override
@@ -139,7 +164,6 @@ public class RegistrarsAction extends ConsoleApiAction {
.setRegistrar(registrar)
.setName(registrarParam.getEmailAddress())
.setEmailAddress(registrarParam.getEmailAddress())
.setLoginEmailAddress(registrarParam.getEmailAddress())
.build();
tm().transact(
@@ -151,4 +175,5 @@ public class RegistrarsAction extends ConsoleApiAction {
tm().putAll(registrar, contact);
});
}
}
@@ -17,6 +17,7 @@ package google.registry.ui.server.console.settings;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.POST;
import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
@@ -30,6 +31,7 @@ import google.registry.request.auth.AuthenticatedRegistrarAccessor;
import google.registry.request.auth.AuthenticatedRegistrarAccessor.RegistrarAccessDeniedException;
import google.registry.ui.server.console.ConsoleApiAction;
import google.registry.ui.server.registrar.ConsoleApiParams;
import java.util.Objects;
import java.util.Optional;
import javax.inject.Inject;
@@ -65,10 +67,10 @@ public class WhoisRegistrarFieldsAction extends ConsoleApiAction {
checkArgument(registrar.isPresent(), "'registrar' parameter is not present");
checkPermission(
user, registrar.get().getRegistrarId(), ConsolePermission.EDIT_REGISTRAR_DETAILS);
tm().transact(() -> loadAndModifyRegistrar(registrar.get()));
tm().transact(() -> loadAndModifyRegistrar(registrar.get(), user));
}
private void loadAndModifyRegistrar(Registrar providedRegistrar) {
private void loadAndModifyRegistrar(Registrar providedRegistrar, User user) {
Registrar savedRegistrar;
try {
// reload to make sure the object has all the correct fields
@@ -78,6 +80,15 @@ public class WhoisRegistrarFieldsAction extends ConsoleApiAction {
return;
}
// icannReferralEmail can't be updated by partners, only by global users with
// EDIT_REGISTRAR_DETAILS permission
if (!Objects.equals(
savedRegistrar.getIcannReferralEmail(), providedRegistrar.getIcannReferralEmail())
&& !user.getUserRoles().hasGlobalPermission(ConsolePermission.EDIT_REGISTRAR_DETAILS)) {
setFailedResponse(
"Icann Referral Email update is not permitted for this user", SC_BAD_REQUEST);
}
Registrar newRegistrar =
savedRegistrar
.asBuilder()
@@ -86,6 +97,8 @@ public class WhoisRegistrarFieldsAction extends ConsoleApiAction {
.setLocalizedAddress(providedRegistrar.getLocalizedAddress())
.setPhoneNumber(providedRegistrar.getPhoneNumber())
.setFaxNumber(providedRegistrar.getFaxNumber())
.setIcannReferralEmail(providedRegistrar.getIcannReferralEmail())
.setEmailAddress(providedRegistrar.getEmailAddress())
.build();
tm().put(newRegistrar);
sendExternalUpdatesIfNecessary(
@@ -30,7 +30,6 @@ import google.registry.batch.CloudTasksUtils;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.console.RegistrarRole;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.model.console.UserRoles;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarAddress;
@@ -260,10 +259,10 @@ public final class ConsoleRegistrarCreatorAction extends HtmlAction {
"Registrar with client ID %s already exists",
registrar.getRegistrarId());
tm().put(registrar);
tm().put(user);
});
UserDao.saveUser(user);
User.grantIapPermission(
user.getEmailAddress(), maybeGroupEmailAddress, cloudTasksUtils, iamClient);
user.getEmailAddress(), maybeGroupEmailAddress, cloudTasksUtils, null, iamClient);
data.put("password", password);
data.put("passcode", phonePasscode);
@@ -14,10 +14,13 @@
package google.registry.ui.server.registrar;
import static com.google.common.base.Strings.isNullOrEmpty;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.auth.AuthenticatedRegistrarAccessor.Role.ADMIN;
import static google.registry.request.auth.AuthenticatedRegistrarAccessor.Role.OWNER;
import static google.registry.ui.server.SoyTemplateUtils.CSS_RENAMING_MAP_SUPPLIER;
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static jakarta.servlet.http.HttpServletResponse.SC_PERMANENT_REDIRECT;
import static jakarta.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE;
import com.google.common.base.Supplier;
@@ -26,6 +29,8 @@ import com.google.common.flogger.FluentLogger;
import com.google.template.soy.data.SoyMapData;
import com.google.template.soy.tofu.SoyTofu;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.common.FeatureFlag;
import google.registry.model.console.GlobalRole;
import google.registry.request.Action;
import google.registry.request.Parameter;
import google.registry.request.auth.Auth;
@@ -35,8 +40,10 @@ import google.registry.request.auth.AuthenticatedRegistrarAccessor.Role;
import google.registry.ui.server.SoyTemplateUtils;
import google.registry.ui.soy.registrar.ConsoleSoyInfo;
import google.registry.util.RegistryEnvironment;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import javax.inject.Inject;
/** Action that serves Registrar Console single HTML page (SPA). */
@@ -100,6 +107,7 @@ public final class ConsoleUiAction extends HtmlAction {
soyMapData.put("announcementsEmail", announcementsEmail);
soyMapData.put("supportPhoneNumber", supportPhoneNumber);
soyMapData.put("technicalDocsUrl", technicalDocsUrl);
if (!enabled) {
response.setStatus(SC_SERVICE_UNAVAILABLE);
response.setPayload(
@@ -111,9 +119,35 @@ public final class ConsoleUiAction extends HtmlAction {
.render());
return;
}
// Set permanent redirect to the new console for tech support
if (isNullOrEmpty(req.getParameter("redirect"))
&& Stream.of(GlobalRole.SUPPORT_LEAD, GlobalRole.SUPPORT_AGENT)
.anyMatch(
globalRole ->
globalRole.equals(authResult.user().get().getUserRoles().getGlobalRole()))) {
response.setStatus(SC_PERMANENT_REDIRECT);
try {
response.sendRedirect("/console");
return;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
ImmutableSetMultimap<String, Role> roleMap = registrarAccessor.getAllRegistrarIdsWithRoles();
soyMapData.put("allClientIds", roleMap.keySet());
soyMapData.put("environment", RegistryEnvironment.get().toString());
boolean newConsole =
tm().transact(
() ->
FeatureFlag.getUncached(FeatureFlag.FeatureName.NEW_CONSOLE)
.map(
flag ->
flag.getStatus(tm().getTransactionTime())
.equals(FeatureFlag.FeatureStatus.ACTIVE))
.orElse(false));
soyMapData.put("includeDeprecationWarning", newConsole);
// We set the initial value to the value that will show if guessClientId throws.
String clientId = "<null>";
try {
@@ -34,6 +34,7 @@ import google.registry.request.auth.AuthResult;
import google.registry.security.XsrfTokenManager;
import google.registry.ui.server.SendEmailUtils;
import google.registry.ui.server.console.ConsoleEppPasswordAction.EppPasswordData;
import google.registry.ui.server.console.ConsoleRegistryLockAction.ConsoleRegistryLockPostInput;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Optional;
import org.joda.time.DateTime;
@@ -242,4 +243,11 @@ public final class RegistrarConsoleModule {
Gson gson, @OptionalJsonPayload Optional<JsonElement> payload) {
return payload.map(s -> gson.fromJson(s, EppPasswordData.class));
}
@Provides
@Parameter("consoleRegistryLockPostInput")
public static Optional<ConsoleRegistryLockPostInput> provideRegistryLockPostInput(
Gson gson, @OptionalJsonPayload Optional<JsonElement> payload) {
return payload.map(e -> gson.fromJson(e, ConsoleRegistryLockPostInput.class));
}
}
@@ -261,7 +261,7 @@ li.kd-menulistitem {
top: 136px;
left: 173px;
bottom: 0;
width: 100%;
width: calc(100% - 173px);
margin: 0;
padding: 25px 0 1em 0;
overflow-y: scroll !important;
@@ -296,3 +296,31 @@ li.kd-menulistitem {
.reg-select {
margin-left: 23px;
}
div#reg-deprecation-warning {
position: fixed;
top: 64px;
width: 100%;
left: 0;
text-align: center;
background-color: #ba2a1a;
font-size: 0.6svw;
}
div#reg-deprecation-warning h1 {
color: #ffffff;
}
div#reg-deprecation-warning a {
color: #add8e6;
}
div#reg-accessing-as-role {
margin-top: 40px;
}
@media screen and (max-width: 1600px) {
div#reg-deprecation-warning {
font-size: 0.85vw;
}
}
@@ -36,6 +36,7 @@
{@param technicalDocsUrl: string}
{@param environment: string}
{@param analyticsConfig: [googleAnalyticsId: string|null]}
{@param includeDeprecationWarning: bool}
{call registry.soy.console.header}
{param app: 'registrar' /}
@@ -45,7 +46,7 @@
{call registry.soy.console.googlebar data="all" /}
<div id="reg-app" lang="en-US">
<div id="reg-appbar" class="{css('kd-appbar')}">
<div class="{css('kd-description')}">
<div id="reg-accessing-as-role" class="{css('kd-description')}">
Accessing <span class="{css('kd-value')}">{$clientId}</span> as{sp}
{if $isOwner}<span class="{css('kd-value')}">Owner</span>{/if}
{if $isAdmin}<span class="{css('kd-value')}">Admin</span>{/if}
@@ -53,6 +54,13 @@
{sp}(Switch registrar: {call .clientIdSelect_ data="all" /})
{/if}
</div>
{if $includeDeprecationWarning}
<div id="reg-deprecation-warning">
<h1>Note: this console is deprecated and will be deactivated in October 2024.
Please use the <a href="/console">new console</a> instead.
</h1>
</div>
{/if}
<div id="reg-app-buttons" class="{css('kd-buttonbar')} {css('left')}"></div>
</div>
{call .navbar_ data="all" /}
@@ -172,7 +172,6 @@ public class SyncRegistrarsSheetTest {
// distinction to make sure we're not relying on it. Sigh.
.setVisibleInWhoisAsAdmin(false)
.setVisibleInWhoisAsTech(true)
.setLoginEmailAddress("john.doe@example.tld")
.build(),
new RegistrarPoc.Builder()
.setRegistrar(registrar)
@@ -191,7 +190,7 @@ public class SyncRegistrarsSheetTest {
ImmutableList<ImmutableMap<String, String>> rows = getOnlyElement(rowsCaptor.getAllValues());
assertThat(rows).hasSize(2);
ImmutableMap<String, String> row = rows.get(0);
ImmutableMap<String, String> row = rows.getFirst();
assertThat(row).containsEntry("registrarId", "aaaregistrar");
assertThat(row).containsEntry("registrarName", "AAA Registrar Inc.");
assertThat(row).containsEntry("state", "SUSPENDED");
@@ -208,7 +207,6 @@ public class SyncRegistrarsSheetTest {
Visible in registrar WHOIS query as Technical contact: No
Phone number and email visible in domain WHOIS query as Registrar Abuse contact\
info: No
Registrar-Console access: No
John Doe
john.doe@example.tld
@@ -219,8 +217,6 @@ public class SyncRegistrarsSheetTest {
Visible in registrar WHOIS query as Technical contact: Yes
Phone number and email visible in domain WHOIS query as Registrar Abuse contact\
info: No
Registrar-Console access: Yes
Login Email Address: john.doe@example.tld
""");
assertThat(row)
.containsEntry(
@@ -233,7 +229,6 @@ public class SyncRegistrarsSheetTest {
Visible in registrar WHOIS query as Technical contact: No
Phone number and email visible in domain WHOIS query as Registrar Abuse contact\
info: No
Registrar-Console access: No
""");
assertThat(row).containsEntry("marketingContacts", "");
assertThat(row).containsEntry("abuseContacts", "");
@@ -251,7 +246,6 @@ public class SyncRegistrarsSheetTest {
Visible in registrar WHOIS query as Technical contact: No
Phone number and email visible in domain WHOIS query as Registrar Abuse contact\
info: No
Registrar-Console access: No
""");
assertThat(row).containsEntry("contactsMarkedAsWhoisAdmin", "");
assertThat(row)
@@ -267,8 +261,6 @@ public class SyncRegistrarsSheetTest {
Visible in registrar WHOIS query as Technical contact: Yes
Phone number and email visible in domain WHOIS query as Registrar Abuse contact\
info: No
Registrar-Console access: Yes
Login Email Address: john.doe@example.tld
""");
assertThat(row).containsEntry("emailAddress", "nowhere@example.org");
assertThat(row).containsEntry(
@@ -104,8 +104,10 @@ import google.registry.model.contact.Contact;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.DesignatedContact.Type;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.secdns.DomainDsData;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.model.eppcommon.Trid;
import google.registry.model.host.Host;
import google.registry.model.poll.PendingActionNotificationResponse.DomainPendingActionNotificationResponse;
@@ -264,6 +266,38 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
doSuccessfulTest("generic_success_response_no_cltrid.xml");
}
@Test
void testSuccess_noExistingAuthData() throws Exception {
setEppInput("domain_update_no_auth_change.xml");
persistReferencedEntities();
persistDomain();
persistResource(
reloadResourceByForeignKey()
.asBuilder()
.setAuthInfo(DomainAuthInfo.create(PasswordAuth.create(null)))
.build());
assertMutatingFlow(true);
runFlowAssertResponse(loadFile("generic_success_response.xml"));
Domain domain = reloadResourceByForeignKey();
// Check that the domain was updated. These values came from the xml.
assertAboutDomains()
.that(domain)
.hasStatusValue(CLIENT_HOLD)
.and()
.hasAuthInfoPwd(null)
.and()
.hasOneHistoryEntryEachOfTypes(DOMAIN_CREATE, DOMAIN_UPDATE)
.and()
.hasLastEppUpdateTime(clock.nowUtc())
.and()
.hasLastEppUpdateRegistrarId("TheRegistrar")
.and()
.hasNoAutorenewEndTime();
assertNoBillingEvents();
assertDomainDnsRequests("example.tld");
assertLastHistoryContainsResource(reloadResourceByForeignKey());
}
@Test
void testSuccess_cachingDisabled() throws Exception {
boolean origIsCachingEnabled = RegistryConfig.isEppResourceCachingEnabled();
@@ -22,6 +22,7 @@ import static google.registry.persistence.transaction.JpaTransactionManagerExten
import static google.registry.testing.CertificateSamples.SAMPLE_CERT;
import static google.registry.testing.CertificateSamples.SAMPLE_CERT_HASH;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.loadByKeyIfPresent;
import static google.registry.testing.DatabaseHelper.persistPremiumList;
import static google.registry.testing.DatabaseHelper.persistSimpleResource;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
@@ -36,10 +37,10 @@ import com.google.common.collect.ImmutableList;
import google.registry.batch.CloudTasksUtils;
import google.registry.model.console.RegistrarRole;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.model.registrar.Registrar;
import google.registry.model.tld.Tld;
import google.registry.model.tld.Tld.TldState;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.CloudTasksHelper;
@@ -106,7 +107,7 @@ public final class OteAccountBuilderTest {
}
public static void verifyUser(String registrarId, String email) {
Optional<User> maybeUser = UserDao.loadUser(email);
Optional<User> maybeUser = loadByKeyIfPresent(VKey.create(User.class, email));
assertThat(maybeUser).isPresent();
assertThat(maybeUser.get().getUserRoles().getRegistrarRoles().get(registrarId))
.isEqualTo(RegistrarRole.ACCOUNT_MANAGER);
@@ -1,84 +0,0 @@
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.console;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static org.junit.jupiter.api.Assertions.assertThrows;
import google.registry.model.EntityTestCase;
import org.junit.jupiter.api.Test;
/** Tests for {@link UserDao}. */
public class UserDaoTest extends EntityTestCase {
@Test
void testSuccess_saveAndRetrieve() {
User user1 =
new User.Builder()
.setEmailAddress("email@email.com")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.SUPPORT_AGENT).build())
.build();
User user2 =
new User.Builder()
.setEmailAddress("foo@bar.com")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.SUPPORT_AGENT).build())
.build();
UserDao.saveUser(user1);
UserDao.saveUser(user2);
assertAboutImmutableObjects()
.that(user1)
.isEqualExceptFields(UserDao.loadUser("email@email.com").get(), "id", "updateTimestamp");
assertAboutImmutableObjects()
.that(user2)
.isEqualExceptFields(UserDao.loadUser("foo@bar.com").get(), "id", "updateTimestamp");
}
@Test
void testSuccess_absentUser() {
User user =
new User.Builder()
.setEmailAddress("email@email.com")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.SUPPORT_AGENT).build())
.build();
UserDao.saveUser(user);
User fromDb = UserDao.loadUser("email@email.com").get();
// nonexistent one should never exist
assertThat(UserDao.loadUser("nonexistent@email.com")).isEmpty();
// now try deleting the one that does exist
tm().transact(() -> tm().delete(fromDb));
assertThat(UserDao.loadUser("email@email.com")).isEmpty();
}
@Test
void testFailure_sameEmail() {
User user1 =
new User.Builder()
.setEmailAddress("email@email.com")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.SUPPORT_AGENT).build())
.build();
User user2 =
new User.Builder()
.setEmailAddress("email@email.com")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.SUPPORT_AGENT).build())
.build();
UserDao.saveUser(user1);
assertThrows(IllegalArgumentException.class, () -> UserDao.saveUser(user2));
assertAboutImmutableObjects()
.that(user1)
.isEqualExceptFields(UserDao.loadUser("email@email.com").get(), "id", "updateTimestamp");
}
}
@@ -21,15 +21,20 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
import static org.junit.jupiter.api.Assertions.assertThrows;
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 com.google.cloud.tasks.v2.HttpMethod;
import com.google.common.collect.ImmutableMap;
import com.google.common.net.MediaType;
import google.registry.batch.CloudTasksUtils;
import google.registry.model.EntityTestCase;
import google.registry.testing.CloudTasksHelper;
import google.registry.testing.CloudTasksHelper.TaskMatcher;
import google.registry.testing.DatabaseHelper;
import google.registry.tools.IamClient;
import google.registry.tools.ServiceConnection;
import google.registry.tools.server.UpdateUserGroupAction;
import java.util.Optional;
import org.junit.jupiter.api.Test;
@@ -57,6 +62,34 @@ public class UserTest extends EntityTestCase {
});
}
@Test
void testFailure_asyncAndSyncModeConflict() {
assertThrows(
IllegalArgumentException.class,
() -> User.grantIapPermission("email@example.com", Optional.empty(), null, null, null));
assertThrows(
IllegalArgumentException.class,
() ->
User.grantIapPermission(
"email@example.com",
Optional.empty(),
mock(CloudTasksUtils.class),
mock(ServiceConnection.class),
null));
assertThrows(
IllegalArgumentException.class,
() -> User.revokeIapPermission("email@example.com", Optional.empty(), null, null, null));
assertThrows(
IllegalArgumentException.class,
() ->
User.revokeIapPermission(
"email@example.com",
Optional.empty(),
mock(CloudTasksUtils.class),
mock(ServiceConnection.class),
null));
}
@Test
void testFailure_badInputs() {
User.Builder builder = new User.Builder();
@@ -116,19 +149,20 @@ public class UserTest extends EntityTestCase {
}
@Test
void testGrantIapPermission() {
void testGrantIapPermissionAsync() {
CloudTasksHelper cloudTasksHelper = new CloudTasksHelper();
IamClient iamClient = mock(IamClient.class);
CloudTasksUtils cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
// Individual permission.
User.grantIapPermission("email@example.com", Optional.empty(), cloudTasksUtils, iamClient);
User.grantIapPermission(
"email@example.com", Optional.empty(), cloudTasksUtils, null, iamClient);
cloudTasksHelper.assertNoTasksEnqueued();
verify(iamClient).addBinding("email@example.com", IAP_SECURED_WEB_APP_USER_ROLE);
// Group membership.
User.grantIapPermission(
"email@example.com", Optional.of("console@example.com"), cloudTasksUtils, iamClient);
"email@example.com", Optional.of("console@example.com"), cloudTasksUtils, null, iamClient);
cloudTasksHelper.assertTasksEnqueued(
"console-user-group-update",
new TaskMatcher()
@@ -142,19 +176,49 @@ public class UserTest extends EntityTestCase {
}
@Test
void testRevokeIapPermission() {
void testGrantIapPermissionSync() throws Exception {
ServiceConnection connection = mock(ServiceConnection.class);
IamClient iamClient = mock(IamClient.class);
// Individual permission.
User.grantIapPermission("email@example.com", Optional.empty(), null, connection, iamClient);
verifyNoInteractions(connection);
verify(iamClient).addBinding("email@example.com", IAP_SECURED_WEB_APP_USER_ROLE);
// Group membership.
User.grantIapPermission(
"email@example.com", Optional.of("console@example.com"), null, connection, iamClient);
verify(connection)
.sendPostRequest(
UpdateUserGroupAction.PATH,
ImmutableMap.of(
"userEmailAddress",
"email@example.com",
"groupEmailAddress",
"console@example.com",
"groupUpdateMode",
"ADD"),
MediaType.PLAIN_TEXT_UTF_8,
new byte[0]);
verifyNoMoreInteractions(iamClient);
verifyNoMoreInteractions(connection);
}
@Test
void testRevokeIapPermissionAsync() {
CloudTasksHelper cloudTasksHelper = new CloudTasksHelper();
IamClient iamClient = mock(IamClient.class);
CloudTasksUtils cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
// Individual permission.
User.revokeIapPermission("email@example.com", Optional.empty(), cloudTasksUtils, iamClient);
User.revokeIapPermission(
"email@example.com", Optional.empty(), cloudTasksUtils, null, iamClient);
cloudTasksHelper.assertNoTasksEnqueued();
verify(iamClient).removeBinding("email@example.com", IAP_SECURED_WEB_APP_USER_ROLE);
// Group membership.
User.revokeIapPermission(
"email@example.com", Optional.of("console@example.com"), cloudTasksUtils, iamClient);
"email@example.com", Optional.of("console@example.com"), cloudTasksUtils, null, iamClient);
cloudTasksHelper.assertTasksEnqueued(
"console-user-group-update",
new TaskMatcher()
@@ -166,4 +230,33 @@ public class UserTest extends EntityTestCase {
.param("groupUpdateMode", "REMOVE"));
verifyNoMoreInteractions(iamClient);
}
@Test
void testRevokeIapPermissionSync() throws Exception {
ServiceConnection connection = mock(ServiceConnection.class);
IamClient iamClient = mock(IamClient.class);
// Individual permission.
User.revokeIapPermission("email@example.com", Optional.empty(), null, connection, iamClient);
verifyNoInteractions(connection);
verify(iamClient).removeBinding("email@example.com", IAP_SECURED_WEB_APP_USER_ROLE);
// Group membership.
User.revokeIapPermission(
"email@example.com", Optional.of("console@example.com"), null, connection, iamClient);
verify(connection)
.sendPostRequest(
UpdateUserGroupAction.PATH,
ImmutableMap.of(
"userEmailAddress",
"email@example.com",
"groupEmailAddress",
"console@example.com",
"groupUpdateMode",
"REMOVE"),
MediaType.PLAIN_TEXT_UTF_8,
new byte[0]);
verifyNoMoreInteractions(iamClient);
verifyNoMoreInteractions(connection);
}
}
@@ -426,7 +426,6 @@ public abstract class JpaTransactionManagerExtension
.setEmailAddress("johndoe@theregistrar.com")
.setPhoneNumber("+1.1234567890")
.setTypes(ImmutableSet.of(RegistrarPocBase.Type.ADMIN))
.setLoginEmailAddress("johndoe@theregistrar.com")
.build();
}
@@ -438,7 +437,6 @@ public abstract class JpaTransactionManagerExtension
.setRegistryLockEmailAddress("Marla.Singer.RegistryLock@crr.com")
.setPhoneNumber("+1.2128675309")
.setTypes(ImmutableSet.of(RegistrarPocBase.Type.TECH))
.setLoginEmailAddress("Marla.Singer@crr.com")
.setAllowedToSetRegistryLockPassword(true)
.setRegistryLockPassword("hi")
.build();
@@ -73,7 +73,6 @@ import google.registry.model.billing.BillingRecurrence;
import google.registry.model.common.DnsRefreshRequest;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.model.console.UserRoles;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactAuthInfo;
@@ -1014,20 +1013,33 @@ public final class DatabaseHelper {
return tm().transact(() -> tm().loadByEntity(resource));
}
public static User loadExistingUser(String emailAddress) {
return loadByKey(VKey.create(User.class, emailAddress));
}
/** Persists an admin {@link User} with the given email address if it doesn't already exist. */
public static User createAdminUser(String emailAddress) {
Optional<User> existingUser = UserDao.loadUser(emailAddress);
if (existingUser.isPresent()) {
return existingUser.get();
}
User user =
new User.Builder()
.setEmailAddress(emailAddress)
.setUserRoles(
new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).setIsAdmin(true).build())
.build();
tm().transact(() -> tm().put(user));
return UserDao.loadUser(emailAddress).get();
// Reload the user to pick up the update time
return loadByEntity(
tm().transact(
() -> {
VKey<User> key = VKey.create(User.class, emailAddress);
Optional<User> existingUser = tm().loadByKeyIfPresent(key);
if (existingUser.isPresent()) {
return existingUser.get();
}
User user =
new User.Builder()
.setEmailAddress(emailAddress)
.setUserRoles(
new UserRoles.Builder()
.setGlobalRole(GlobalRole.FTE)
.setIsAdmin(true)
.build())
.build();
tm().put(user);
return user;
}));
}
/** Returns all the history entries that are parented off the given EppResource. */
@@ -29,6 +29,7 @@ import google.registry.model.eppcommon.AuthInfo;
import google.registry.testing.TruthChainer.And;
import google.registry.tmch.LordnTaskUtils.LordnPhase;
import java.util.Set;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
/** Truth subject for asserting things about {@link Domain} instances. */
@@ -65,7 +66,7 @@ public final class DomainSubject extends AbstractEppResourceSubject<Domain, Doma
return hasValue(lordnPhase, actual.getLordnPhase(), "lordnPhase");
}
public And<DomainSubject> hasAuthInfoPwd(String pw) {
public And<DomainSubject> hasAuthInfoPwd(@Nullable String pw) {
AuthInfo authInfo = actual.getAuthInfo();
return hasValue(pw, authInfo == null ? null : authInfo.getPw().getValue(), "has auth info pw");
}
@@ -16,22 +16,21 @@ package google.registry.tools;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.console.User.IAP_SECURED_WEB_APP_USER_ROLE;
import static google.registry.testing.DatabaseHelper.loadExistingUser;
import static org.junit.Assert.assertThrows;
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 com.google.cloud.tasks.v2.HttpMethod;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.net.MediaType;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.RegistrarRole;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.testing.CloudTasksHelper;
import google.registry.testing.CloudTasksHelper.TaskMatcher;
import google.registry.testing.DatabaseHelper;
import google.registry.tools.server.UpdateUserGroupAction;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -40,13 +39,13 @@ import org.junit.jupiter.api.Test;
public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
private final IamClient iamClient = mock(IamClient.class);
private final CloudTasksHelper cloudTasksHelper = new CloudTasksHelper();
private final ServiceConnection connection = mock(ServiceConnection.class);
@BeforeEach
void beforeEach() {
command.iamClient = iamClient;
command.maybeGroupEmailAddress = Optional.empty();
command.cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
command.setConnection(connection);
}
@Test
@@ -59,7 +58,7 @@ public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
assertThat(onlyUser.getUserRoles().getRegistrarRoles()).isEmpty();
verify(iamClient).addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE);
verifyNoMoreInteractions(iamClient);
cloudTasksHelper.assertNoTasksEnqueued("console-user-group-update");
verifyNoInteractions(connection);
}
@Test
@@ -71,16 +70,20 @@ public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
assertThat(onlyUser.getUserRoles().isAdmin()).isFalse();
assertThat(onlyUser.getUserRoles().getGlobalRole()).isEqualTo(GlobalRole.NONE);
assertThat(onlyUser.getUserRoles().getRegistrarRoles()).isEmpty();
cloudTasksHelper.assertTasksEnqueued(
"console-user-group-update",
new TaskMatcher()
.method(HttpMethod.POST)
.service("TOOLS")
.path("/_dr/admin/updateUserGroup")
.param("userEmailAddress", "user@example.test")
.param("groupEmailAddress", "group@example.test")
.param("groupUpdateMode", "ADD"));
verify(connection)
.sendPostRequest(
UpdateUserGroupAction.PATH,
ImmutableMap.of(
"userEmailAddress",
"user@example.test",
"groupEmailAddress",
"group@example.test",
"groupUpdateMode",
"ADD"),
MediaType.PLAIN_TEXT_UTF_8,
new byte[0]);
verifyNoInteractions(iamClient);
verifyNoMoreInteractions(connection);
}
@Test
@@ -90,27 +93,27 @@ public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
"user@example.test",
"--registry_lock_email_address",
"registrylockemail@otherexample.test");
assertThat(UserDao.loadUser("user@example.test").get().getRegistryLockEmailAddress())
assertThat(loadExistingUser("user@example.test").getRegistryLockEmailAddress())
.hasValue("registrylockemail@otherexample.test");
}
@Test
void testSuccess_admin() throws Exception {
runCommandForced("--email", "user@example.test", "--admin", "true");
assertThat(UserDao.loadUser("user@example.test").get().getUserRoles().isAdmin()).isTrue();
assertThat(loadExistingUser("user@example.test").getUserRoles().isAdmin()).isTrue();
verify(iamClient).addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE);
verifyNoMoreInteractions(iamClient);
cloudTasksHelper.assertNoTasksEnqueued("console-user-group-update");
verifyNoInteractions(connection);
}
@Test
void testSuccess_globalRole() throws Exception {
runCommandForced("--email", "user@example.test", "--global_role", "FTE");
assertThat(UserDao.loadUser("user@example.test").get().getUserRoles().getGlobalRole())
assertThat(loadExistingUser("user@example.test").getUserRoles().getGlobalRole())
.isEqualTo(GlobalRole.FTE);
verify(iamClient).addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE);
verifyNoMoreInteractions(iamClient);
cloudTasksHelper.assertNoTasksEnqueued("console-user-group-update");
verifyNoInteractions(connection);
}
@Test
@@ -120,7 +123,7 @@ public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
"user@example.test",
"--registrar_roles",
"TheRegistrar=ACCOUNT_MANAGER,NewRegistrar=PRIMARY_CONTACT");
assertThat(UserDao.loadUser("user@example.test").get().getUserRoles().getRegistrarRoles())
assertThat(loadExistingUser("user@example.test").getUserRoles().getRegistrarRoles())
.isEqualTo(
ImmutableMap.of(
"TheRegistrar",
@@ -129,7 +132,7 @@ public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
RegistrarRole.PRIMARY_CONTACT));
verify(iamClient).addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE);
verifyNoMoreInteractions(iamClient);
cloudTasksHelper.assertNoTasksEnqueued("console-user-group-update");
verifyNoInteractions(connection);
}
@Test
@@ -144,7 +147,7 @@ public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
.hasMessageThat()
.isEqualTo("A user with email user@example.test already exists");
verifyNoMoreInteractions(iamClient);
cloudTasksHelper.assertNoTasksEnqueued("console-user-group-update");
verifyNoInteractions(connection);
}
@Test
@@ -22,11 +22,12 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import com.google.cloud.tasks.v2.HttpMethod;
import google.registry.model.console.UserDao;
import google.registry.testing.CloudTasksHelper;
import google.registry.testing.CloudTasksHelper.TaskMatcher;
import com.google.common.collect.ImmutableMap;
import com.google.common.net.MediaType;
import google.registry.model.console.User;
import google.registry.persistence.VKey;
import google.registry.testing.DatabaseHelper;
import google.registry.tools.server.UpdateUserGroupAction;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -35,43 +36,49 @@ import org.junit.jupiter.api.Test;
public class DeleteUserCommandTest extends CommandTestCase<DeleteUserCommand> {
private final IamClient iamClient = mock(IamClient.class);
private final CloudTasksHelper cloudTasksHelper = new CloudTasksHelper();
private final ServiceConnection connection = mock(ServiceConnection.class);
@BeforeEach
void beforeEach() {
command.iamClient = iamClient;
command.maybeGroupEmailAddress = Optional.empty();
command.cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
command.setConnection(connection);
}
@Test
void testSuccess_deletesUser() throws Exception {
DatabaseHelper.createAdminUser("email@example.test");
assertThat(UserDao.loadUser("email@example.test")).isPresent();
VKey<User> key = VKey.create(User.class, "email@example.test");
assertThat(DatabaseHelper.loadByKeyIfPresent(key)).isPresent();
runCommandForced("--email", "email@example.test");
assertThat(UserDao.loadUser("email@example.test")).isEmpty();
assertThat(DatabaseHelper.loadByKeyIfPresent(key)).isEmpty();
verify(iamClient).removeBinding("email@example.test", IAP_SECURED_WEB_APP_USER_ROLE);
verifyNoMoreInteractions(iamClient);
cloudTasksHelper.assertNoTasksEnqueued("console-user-group-update");
verifyNoInteractions(connection);
}
@Test
void testSuccess_deletesUser_removeFromGroup() throws Exception {
command.maybeGroupEmailAddress = Optional.of("group@example.test");
DatabaseHelper.createAdminUser("email@example.test");
assertThat(UserDao.loadUser("email@example.test")).isPresent();
VKey<User> key = VKey.create(User.class, "email@example.test");
assertThat(DatabaseHelper.loadByKeyIfPresent(key)).isPresent();
runCommandForced("--email", "email@example.test");
assertThat(UserDao.loadUser("email@example.test")).isEmpty();
cloudTasksHelper.assertTasksEnqueued(
"console-user-group-update",
new TaskMatcher()
.method(HttpMethod.POST)
.service("TOOLS")
.path("/_dr/admin/updateUserGroup")
.param("userEmailAddress", "email@example.test")
.param("groupEmailAddress", "group@example.test")
.param("groupUpdateMode", "REMOVE"));
assertThat(DatabaseHelper.loadByKeyIfPresent(key)).isEmpty();
verify(connection)
.sendPostRequest(
UpdateUserGroupAction.PATH,
ImmutableMap.of(
"userEmailAddress",
"email@example.test",
"groupEmailAddress",
"group@example.test",
"groupUpdateMode",
"REMOVE"),
MediaType.PLAIN_TEXT_UTF_8,
new byte[0]);
verifyNoInteractions(iamClient);
verifyNoMoreInteractions(connection);
}
@Test
@@ -83,6 +90,6 @@ public class DeleteUserCommandTest extends CommandTestCase<DeleteUserCommand> {
.hasMessageThat()
.isEqualTo("Email does not correspond to a valid user");
verifyNoInteractions(iamClient);
cloudTasksHelper.assertNoTasksEnqueued("console-user-group-update");
verifyNoInteractions(connection);
}
}
@@ -23,7 +23,8 @@ import com.google.common.io.Resources;
import google.registry.persistence.NomulusPostgreSql;
import java.io.File;
import java.nio.file.Path;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
@@ -33,26 +34,31 @@ import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
class GenerateSqlSchemaCommandTest extends CommandTestCase<GenerateSqlSchemaCommand> {
private String containerHostName;
private int containerPort;
private static String containerHostName;
private static int containerPort;
@Container
private static PostgreSQLContainer postgres =
new PostgreSQLContainer(NomulusPostgreSql.getDockerTag())
private static final PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>(NomulusPostgreSql.getDockerTag())
.withDatabaseName("postgres")
.withUsername("postgres")
.withPassword("domain-registry");
@BeforeEach
void beforeEach() {
containerHostName = postgres.getContainerIpAddress();
@BeforeAll
static void beforeAll() {
containerHostName = postgres.getHost();
containerPort = postgres.getMappedPort(GenerateSqlSchemaCommand.POSTGRESQL_PORT);
}
@AfterAll
static void afterAll() {
postgres.close();
}
@Test
void testSchemaGeneration() throws Exception {
runCommand(
"--out_file=" + tmpDir.resolve("schema.sql").toString(),
"--out_file=" + tmpDir.resolve("schema.sql"),
"--db_host=" + containerHostName,
"--db_port=" + containerPort);
@@ -69,7 +75,7 @@ class GenerateSqlSchemaCommandTest extends CommandTestCase<GenerateSqlSchemaComm
@Test
void testIncompatibleFlags() throws Exception {
runCommand(
"--out_file=" + tmpDir.resolve("schema.sql").toString(),
"--out_file=" + tmpDir.resolve("schema.sql"),
"--db_host=" + containerHostName,
"--db_port=" + containerPort,
"--start_postgresql");
@@ -79,14 +85,14 @@ class GenerateSqlSchemaCommandTest extends CommandTestCase<GenerateSqlSchemaComm
@Test
void testDockerPostgresql() throws Exception {
Path schemaFile = tmpDir.resolve("schema.sql");
runCommand("--start_postgresql", "--out_file=" + schemaFile.toString());
runCommand("--start_postgresql", "--out_file=" + schemaFile);
assertThat(schemaFile.toFile().exists()).isTrue();
}
@Test
void validateGeneratedSchemaIsSameAsSchemaInFile() throws Exception {
Path schemaFile = tmpDir.resolve("schema.sql");
runCommand("--start_postgresql", "--out_file=" + schemaFile.toString());
runCommand("--start_postgresql", "--out_file=" + schemaFile);
assertThat(schemaFile.toFile().toURI().toURL())
.hasSameContentAs(Resources.getResource("sql/schema/db-schema.sql.generated"));
}
@@ -14,12 +14,13 @@
package google.registry.tools;
import com.google.common.collect.ImmutableMap;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.RegistrarRole;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.model.console.UserRoles;
import google.registry.testing.DatabaseHelper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -28,7 +29,7 @@ public class GetUserCommandTest extends CommandTestCase<GetUserCommand> {
@BeforeEach
void beforeEach() {
UserDao.saveUser(
DatabaseHelper.putInDb(
new User.Builder()
.setEmailAddress("johndoe@theregistrar.com")
.setUserRoles(
@@ -36,8 +37,7 @@ public class GetUserCommandTest extends CommandTestCase<GetUserCommand> {
.setRegistrarRoles(
ImmutableMap.of("TheRegistrar", RegistrarRole.PRIMARY_CONTACT))
.build())
.build());
UserDao.saveUser(
.build(),
new User.Builder()
.setEmailAddress("fte@google.com")
.setUserRoles(
@@ -52,7 +52,6 @@ public class GetUserCommandTest extends CommandTestCase<GetUserCommand> {
"""
User: {
emailAddress=fte@google.com
id=2
registryLockEmailAddress=null
registryLockPasswordHash=null
registryLockPasswordSalt=null
@@ -75,7 +74,6 @@ public class GetUserCommandTest extends CommandTestCase<GetUserCommand> {
"""
User: {
emailAddress=johndoe@theregistrar.com
id=1
registryLockEmailAddress=null
registryLockPasswordHash=null
registryLockPasswordSalt=null
@@ -90,7 +88,6 @@ public class GetUserCommandTest extends CommandTestCase<GetUserCommand> {
}
User: {
emailAddress=fte@google.com
id=2
registryLockEmailAddress=null
registryLockPasswordHash=null
registryLockPasswordSalt=null
@@ -113,7 +110,6 @@ public class GetUserCommandTest extends CommandTestCase<GetUserCommand> {
"""
User: {
emailAddress=johndoe@theregistrar.com
id=1
registryLockEmailAddress=null
registryLockPasswordHash=null
registryLockPasswordSalt=null
@@ -68,8 +68,7 @@ class RegistrarPocCommandTest extends CommandTestCase<RegistrarPocCommand> {
"Visible in registrar WHOIS query as Admin contact: Yes",
"Visible in registrar WHOIS query as Technical contact: No",
"Phone number and email visible in domain WHOIS query as "
+ "Registrar Abuse contact info: No",
"Registrar-Console access: No");
+ "Registrar Abuse contact info: No");
}
@Test
@@ -116,69 +115,6 @@ class RegistrarPocCommandTest extends CommandTestCase<RegistrarPocCommand> {
.build());
}
@Test
void testUpdate_enableConsoleAccess() throws Exception {
Registrar registrar = loadRegistrar("NewRegistrar");
persistSimpleResource(
new RegistrarPoc.Builder()
.setRegistrar(registrar)
.setName("Jane Doe")
.setEmailAddress("jane.doe@example.com")
.build());
runCommandForced(
"--mode=UPDATE",
"--email=jane.doe@example.com",
"--allow_console_access=true",
"NewRegistrar");
RegistrarPoc registrarPoc =
loadRegistrar("NewRegistrar").getContacts().stream()
.filter(rc -> "jane.doe@example.com".equals(rc.getEmailAddress()))
.findFirst()
.get();
assertThat(registrarPoc.getLoginEmailAddress()).isEqualTo("jane.doe@example.com");
}
@Test
void testUpdate_enableConsoleAccess_specifyLoginEmail() throws Exception {
Registrar registrar = loadRegistrar("NewRegistrar");
persistSimpleResource(
new RegistrarPoc.Builder()
.setRegistrar(registrar)
.setName("Jane Doe")
.setEmailAddress("jane.doe@example.com")
.build());
runCommandForced(
"--mode=UPDATE",
"--email=jane.doe@example.com",
"--login_email=jim.doe@example.com",
"--allow_console_access=true",
"NewRegistrar");
RegistrarPoc registrarPoc =
loadRegistrar("NewRegistrar").getContacts().stream()
.filter(rc -> "jane.doe@example.com".equals(rc.getEmailAddress()))
.findFirst()
.get();
assertThat(registrarPoc.getLoginEmailAddress()).isEqualTo("jim.doe@example.com");
}
@Test
void testUpdate_disableConsoleAccess() throws Exception {
Registrar registrar = loadRegistrar("NewRegistrar");
persistSimpleResource(
new RegistrarPoc.Builder()
.setRegistrar(registrar)
.setName("Judith Doe")
.setEmailAddress("judith.doe@example.com")
.build());
runCommandForced(
"--mode=UPDATE",
"--email=judith.doe@example.com",
"--allow_console_access=false",
"NewRegistrar");
RegistrarPoc registrarPoc = loadRegistrar("NewRegistrar").getContacts().asList().get(1);
assertThat(registrarPoc.getLoginEmailAddress()).isNull();
}
@Test
void testUpdate_unsetOtherWhoisAbuseFlags() throws Exception {
Registrar registrar = loadRegistrar("NewRegistrar");
@@ -334,7 +270,6 @@ class RegistrarPocCommandTest extends CommandTestCase<RegistrarPocCommand> {
.setVisibleInWhoisAsTech(false)
.setVisibleInDomainWhoisAsAbuse(true)
.build());
assertThat(registrarPoc.getLoginEmailAddress()).isNull();
}
@Test
@@ -346,7 +281,7 @@ class RegistrarPocCommandTest extends CommandTestCase<RegistrarPocCommand> {
@Test
void testDelete_failsOnDomainWhoisAbuseContact() {
RegistrarPoc registrarPoc = loadRegistrar("NewRegistrar").getContacts().asList().get(0);
RegistrarPoc registrarPoc = loadRegistrar("NewRegistrar").getContacts().asList().getFirst();
putInDb(registrarPoc.asBuilder().setVisibleInDomainWhoisAsAbuse(true).build());
IllegalArgumentException thrown =
assertThrows(
@@ -358,33 +293,6 @@ class RegistrarPocCommandTest extends CommandTestCase<RegistrarPocCommand> {
assertThat(loadRegistrar("NewRegistrar").getContacts()).isNotEmpty();
}
@Test
void testCreate_withConsoleAccessEnabled() throws Exception {
runCommandForced(
"--mode=CREATE",
"--name=Jim Doe",
"--email=jim.doe@example.com",
"--allow_console_access=true",
"--contact_type=ADMIN,ABUSE",
"NewRegistrar");
RegistrarPoc registrarPoc = loadRegistrar("NewRegistrar").getContacts().asList().get(1);
assertThat(registrarPoc.getEmailAddress()).isEqualTo("jim.doe@example.com");
}
@Test
void testCreate_withConsoleAccessEnabled_specifyLoginEmail() throws Exception {
runCommandForced(
"--mode=CREATE",
"--name=Jim Doe",
"--email=jim.doe@example.com",
"--login_email=jane.doe@example.com",
"--allow_console_access=true",
"--contact_type=ADMIN,ABUSE",
"NewRegistrar");
RegistrarPoc registrarPoc = loadRegistrar("NewRegistrar").getContacts().asList().get(1);
assertThat(registrarPoc.getLoginEmailAddress()).isEqualTo("jane.doe@example.com");
}
@Test
void testCreate_withNoContactTypes() throws Exception {
runCommandForced(
@@ -22,9 +22,11 @@ import static google.registry.model.tld.Tld.TldState.GENERAL_AVAILABILITY;
import static google.registry.model.tld.Tld.TldState.START_DATE_SUNRISE;
import static google.registry.testing.CertificateSamples.SAMPLE_CERT_HASH;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.loadExistingUser;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.persistPremiumList;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.DatabaseHelper.putInDb;
import static org.joda.money.CurrencyUnit.USD;
import static org.joda.time.DateTimeZone.UTC;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -39,7 +41,6 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.model.console.UserRoles;
import google.registry.model.registrar.Registrar;
import google.registry.model.tld.Tld;
@@ -447,12 +448,11 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
@Test
void testFailure_userExists() {
User user =
putInDb(
new User.Builder()
.setEmailAddress("contact@email.com")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build())
.build();
UserDao.saveUser(user);
.build());
IllegalStateException thrown =
assertThrows(
IllegalStateException.class,
@@ -494,12 +494,11 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
@Test
void testSuccess_userExists_replaceExisting() throws Exception {
User user =
putInDb(
new User.Builder()
.setEmailAddress("contact@email.com")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build())
.build();
UserDao.saveUser(user);
.build());
runCommandForced(
"--overwrite",
@@ -520,7 +519,7 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
verifyUser("blobio-5", "contact@email.com");
// verify that the role is completely replaced, e.g., the global role is gone.
assertThat(UserDao.loadUser("contact@email.com").get().getUserRoles().getGlobalRole())
assertThat(loadExistingUser("contact@email.com").getUserRoles().getGlobalRole())
.isEqualTo(GlobalRole.NONE);
verifyIapPermission("contact@email.com");
@@ -15,13 +15,14 @@
package google.registry.tools;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.DatabaseHelper.loadExistingUser;
import static google.registry.testing.DatabaseHelper.putInDb;
import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableMap;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.RegistrarRole;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.model.console.UserRoles;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -31,7 +32,7 @@ public class UpdateUserCommandTest extends CommandTestCase<UpdateUserCommand> {
@BeforeEach
void beforeEach() throws Exception {
UserDao.saveUser(
putInDb(
new User.Builder()
.setEmailAddress("user@example.test")
.setUserRoles(new UserRoles.Builder().build())
@@ -45,41 +46,39 @@ public class UpdateUserCommandTest extends CommandTestCase<UpdateUserCommand> {
"user@example.test",
"--registry_lock_email_address",
"registrylockemail@otherexample.test");
assertThat(UserDao.loadUser("user@example.test").get().getRegistryLockEmailAddress())
assertThat(loadExistingUser("user@example.test").getRegistryLockEmailAddress())
.hasValue("registrylockemail@otherexample.test");
}
@Test
void testSuccess_removeRegistryLockEmail() throws Exception {
UserDao.saveUser(
UserDao.loadUser("user@example.test")
.get()
putInDb(
loadExistingUser("user@example.test")
.asBuilder()
.setRegistryLockEmailAddress("registrylock@otherexample.test")
.build());
runCommandForced("--email", "user@example.test", "--registry_lock_email_address", "");
assertThat(UserDao.loadUser("user@example.test").get().getRegistryLockEmailAddress()).isEmpty();
assertThat(loadExistingUser("user@example.test").getRegistryLockEmailAddress()).isEmpty();
}
@Test
void testSuccess_admin() throws Exception {
assertThat(UserDao.loadUser("user@example.test").get().getUserRoles().isAdmin()).isFalse();
assertThat(loadExistingUser("user@example.test").getUserRoles().isAdmin()).isFalse();
runCommandForced("--email", "user@example.test", "--admin", "true");
assertThat(UserDao.loadUser("user@example.test").get().getUserRoles().isAdmin()).isTrue();
assertThat(loadExistingUser("user@example.test").getUserRoles().isAdmin()).isTrue();
runCommandForced("--email", "user@example.test", "--admin", "false");
assertThat(UserDao.loadUser("user@example.test").get().getUserRoles().isAdmin()).isFalse();
assertThat(loadExistingUser("user@example.test").getUserRoles().isAdmin()).isFalse();
}
@Test
void testSuccess_registrarRoles() throws Exception {
assertThat(UserDao.loadUser("user@example.test").get().getUserRoles().getRegistrarRoles())
.isEmpty();
assertThat(loadExistingUser("user@example.test").getUserRoles().getRegistrarRoles()).isEmpty();
runCommandForced(
"--email",
"user@example.test",
"--registrar_roles",
"TheRegistrar=ACCOUNT_MANAGER,NewRegistrar=PRIMARY_CONTACT");
assertThat(UserDao.loadUser("user@example.test").get().getUserRoles().getRegistrarRoles())
assertThat(loadExistingUser("user@example.test").getUserRoles().getRegistrarRoles())
.isEqualTo(
ImmutableMap.of(
"TheRegistrar",
@@ -87,16 +86,15 @@ public class UpdateUserCommandTest extends CommandTestCase<UpdateUserCommand> {
"NewRegistrar",
RegistrarRole.PRIMARY_CONTACT));
runCommandForced("--email", "user@example.test", "--registrar_roles", "");
assertThat(UserDao.loadUser("user@example.test").get().getUserRoles().getRegistrarRoles())
.isEmpty();
assertThat(loadExistingUser("user@example.test").getUserRoles().getRegistrarRoles()).isEmpty();
}
@Test
void testSuccess_globalRole() throws Exception {
assertThat(UserDao.loadUser("user@example.test").get().getUserRoles().getGlobalRole())
assertThat(loadExistingUser("user@example.test").getUserRoles().getGlobalRole())
.isEqualTo(GlobalRole.NONE);
runCommandForced("--email", "user@example.test", "--global_role", "FTE");
assertThat(UserDao.loadUser("user@example.test").get().getUserRoles().getGlobalRole())
assertThat(loadExistingUser("user@example.test").getUserRoles().getGlobalRole())
.isEqualTo(GlobalRole.FTE);
}
@@ -24,6 +24,7 @@ import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.SqlHelper.getMostRecentRegistryLockByRepoId;
import static google.registry.testing.SqlHelper.saveRegistryLock;
import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STATUSES;
import static google.registry.ui.server.console.ConsoleRegistryLockAction.ConsoleRegistryLockPostInput;
import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
@@ -309,10 +310,7 @@ public class ConsoleRegistryLockActionTest {
persistResource(defaultDomain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build());
action =
createPostAction(
"example.test",
false,
"registryLockPassword",
Optional.of(Duration.standardDays(1).getMillis()));
"example.test", false, "registryLockPassword", Duration.standardDays(1).getMillis());
action.run();
assertThat(response.getStatus()).isEqualTo(SC_OK);
verifyEmail();
@@ -348,7 +346,7 @@ public class ConsoleRegistryLockActionTest {
.setUserRoles(
new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).setIsAdmin(true).build())
.build();
action = createPostAction("example.test", true, "", Optional.empty());
action = createPostAction("example.test", true, "", null);
action.run();
assertThat(response.getStatus()).isEqualTo(SC_OK);
verifyEmail();
@@ -414,8 +412,7 @@ public class ConsoleRegistryLockActionTest {
.setCreationRegistrarId("NewRegistrar")
.setPersistedCurrentSponsorRegistrarId("NewRegistrar")
.build());
action =
createPostAction("otherregistrar.test", true, "registryLockPassword", Optional.empty());
action = createPostAction("otherregistrar.test", true, "registryLockPassword", null);
action.run();
assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST);
assertThat(response.getPayload())
@@ -444,7 +441,7 @@ public class ConsoleRegistryLockActionTest {
@Test
void testPost_failure_badPassword() throws Exception {
action = createPostAction("example.test", true, "badPassword", Optional.empty());
action = createPostAction("example.test", true, "badPassword", null);
action.run();
assertThat(response.getStatus()).isEqualTo(SC_UNAUTHORIZED);
}
@@ -483,29 +480,25 @@ public class ConsoleRegistryLockActionTest {
}
private ConsoleRegistryLockAction createDefaultPostAction(boolean isLock) {
return createPostAction("example.test", isLock, "registryLockPassword", Optional.empty());
return createPostAction("example.test", isLock, "registryLockPassword", null);
}
private ConsoleRegistryLockAction createPostAction(
String domainName, boolean isLock, String password, Optional<Long> relockDurationMillis) {
String domainName, boolean isLock, String password, Long relockDurationMillis) {
ConsoleApiParams params = createParams();
when(params.request().getParameter("domainName")).thenReturn(domainName);
when(params.request().getParameterMap())
.thenReturn(ImmutableMap.of("isLock", new String[] {String.valueOf(isLock)}));
when(params.request().getParameter("isLock")).thenReturn(String.valueOf(isLock));
when(params.request().getParameter("password")).thenReturn(password);
relockDurationMillis.ifPresent(
duration ->
when(params.request().getParameter("relockDurationMillis"))
.thenReturn(String.valueOf(duration)));
return createGenericAction(params, "POST");
ConsoleRegistryLockPostInput postInput =
new ConsoleRegistryLockPostInput(domainName, isLock, password, relockDurationMillis);
return createGenericAction(params, "POST", Optional.of(postInput));
}
private ConsoleRegistryLockAction createGetAction() throws IOException {
return createGenericAction(createParams(), "GET");
return createGenericAction(createParams(), "GET", Optional.empty());
}
private ConsoleRegistryLockAction createGenericAction(ConsoleApiParams params, String method) {
private ConsoleRegistryLockAction createGenericAction(
ConsoleApiParams params,
String method,
Optional<ConsoleRegistryLockPostInput> optionalPostInput) {
when(params.request().getMethod()).thenReturn(method);
when(params.request().getServerName()).thenReturn("registrarconsole.tld");
when(params.request().getParameter("registrarId")).thenReturn("TheRegistrar");
@@ -516,7 +509,7 @@ public class ConsoleRegistryLockActionTest {
new CloudTasksHelper(fakeClock).getTestCloudTasksUtils());
response = (FakeResponse) params.response();
return new ConsoleRegistryLockAction(
params, domainLockUtils, gmailClient, GSON, "TheRegistrar");
params, domainLockUtils, gmailClient, GSON, optionalPostInput, "TheRegistrar");
}
private ConsoleApiParams createParams() {
@@ -90,7 +90,7 @@ public class ConsoleRegistryLockVerifyActionTest {
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
assertThat(response.getPayload())
.isEqualTo(
"{\"action\":\"unlocked\",\"domainName\":\"example.test\",\"registrarId\":\"TheRegistrar\"}");
"{\"action\":\"locked\",\"domainName\":\"example.test\",\"registrarId\":\"TheRegistrar\"}");
assertThat(loadByEntity(defaultDomain).getStatusValues())
.containsAtLeastElementsIn(REGISTRY_LOCK_STATUSES);
}
@@ -123,7 +123,7 @@ public class ConsoleRegistryLockVerifyActionTest {
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
assertThat(response.getPayload())
.isEqualTo(
"{\"action\":\"unlocked\",\"domainName\":\"example.test\",\"registrarId\":\"TheRegistrar\"}");
"{\"action\":\"locked\",\"domainName\":\"example.test\",\"registrarId\":\"TheRegistrar\"}");
assertThat(loadByEntity(defaultDomain).getStatusValues())
.containsAtLeastElementsIn(REGISTRY_LOCK_STATUSES);
}
@@ -21,7 +21,6 @@ import static google.registry.testing.DatabaseHelper.persistNewRegistrar;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.SqlHelper.saveRegistrar;
import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
@@ -148,6 +147,28 @@ class RegistrarsActionTest {
.isTrue();
}
@Test
void testSuccess_getOnlyAllowedRegistrars() {
saveRegistrar("registrarId");
RegistrarsAction action =
createAction(
Action.Method.GET,
AuthResult.createUser(
createUser(
new UserRoles.Builder()
.setRegistrarRoles(
ImmutableMap.of("registrarId", RegistrarRole.ACCOUNT_MANAGER))
.build())));
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK);
String payload = ((FakeResponse) consoleApiParams.response()).getPayload();
Registrar[] registrars = GSON.fromJson(payload, Registrar[].class);
assertThat(registrars).hasLength(1);
assertThat(registrars[0].getRegistrarId()).isEqualTo("registrarId");
}
@Test
void testSuccess_createRegistrar() {
RegistrarsAction action =
@@ -205,23 +226,6 @@ class RegistrarsActionTest {
.isEqualTo("Registrar with registrarId regIdTest already exists");
}
@Test
void testFailure_getRegistrarIds() {
saveRegistrar("registrarId");
RegistrarsAction action =
createAction(
Action.Method.GET,
AuthResult.createUser(
createUser(
new UserRoles.Builder()
.setRegistrarRoles(
ImmutableMap.of(
"registrarId", RegistrarRole.ACCOUNT_MANAGER_WITH_REGISTRY_LOCK))
.build())));
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_FORBIDDEN);
}
private User createUser(UserRoles userRoles) {
return new User.Builder()
.setEmailAddress("email@email.com")
@@ -17,7 +17,6 @@ package google.registry.ui.server.console.settings;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.registrar.RegistrarPocBase.Type.ADMIN;
import static google.registry.model.registrar.RegistrarPocBase.Type.WHOIS;
import static google.registry.testing.DatabaseHelper.createAdminUser;
import static google.registry.testing.DatabaseHelper.insertInDb;
import static google.registry.testing.DatabaseHelper.loadAllOf;
@@ -67,7 +66,7 @@ class ContactActionTest {
+ "\"emailAddress\":\"test.registrar1@example.com\","
+ "\"registrarId\":\"registrarId\","
+ "\"phoneNumber\":\"+1.9999999999\",\"faxNumber\":\"+1.9999999991\","
+ "\"types\":[\"WHOIS\",\"ADMIN\"],\"visibleInWhoisAsAdmin\":true,"
+ "\"types\":[\"ADMIN\"],\"visibleInWhoisAsAdmin\":true,"
+ "\"visibleInWhoisAsTech\":false,\"visibleInDomainWhoisAsAbuse\":false}";
private static String jsonRegistrar2 =
@@ -75,7 +74,7 @@ class ContactActionTest {
+ "\"emailAddress\":\"test.registrar2@example.com\","
+ "\"registrarId\":\"registrarId\","
+ "\"phoneNumber\":\"+1.1234567890\",\"faxNumber\":\"+1.1234567891\","
+ "\"types\":[\"WHOIS\",\"ADMIN\"],\"visibleInWhoisAsAdmin\":true,"
+ "\"types\":[\"ADMIN\"],\"visibleInWhoisAsAdmin\":true,"
+ "\"visibleInWhoisAsTech\":false,\"visibleInDomainWhoisAsAbuse\":false}";
private Registrar testRegistrar;
@@ -97,7 +96,7 @@ class ContactActionTest {
.setEmailAddress("test.registrar1@example.com")
.setPhoneNumber("+1.9999999999")
.setFaxNumber("+1.9999999991")
.setTypes(ImmutableSet.of(WHOIS, ADMIN))
.setTypes(ImmutableSet.of(ADMIN))
.setVisibleInWhoisAsAdmin(true)
.setVisibleInWhoisAsTech(false)
.setVisibleInDomainWhoisAsAbuse(false)
@@ -218,22 +217,22 @@ class ContactActionTest {
+ " {name=Test Registrar 1,"
+ " emailAddress=test.registrar1@example.com, registrarId=registrarId,"
+ " registryLockEmailAddress=null, phoneNumber=+1.9999999999,"
+ " faxNumber=+1.9999999991, types=[ADMIN, WHOIS], loginEmailAddress=null,"
+ " faxNumber=+1.9999999991, types=[ADMIN],"
+ " visibleInWhoisAsAdmin=true, visibleInWhoisAsTech=false,"
+ " visibleInDomainWhoisAsAbuse=false,"
+ " allowedToSetRegistryLockPassword=false}\n"
+ " REMOVED:\n"
+ " {name=Test Registrar 1, emailAddress=incorrect@email.com,"
+ " registrarId=registrarId, registryLockEmailAddress=null,"
+ " phoneNumber=+1.9999999999, faxNumber=+1.9999999991, types=[WHOIS,"
+ " ADMIN], loginEmailAddress=null, visibleInWhoisAsAdmin=true,"
+ " phoneNumber=+1.9999999999, faxNumber=+1.9999999991, types=[ADMIN],"
+ " visibleInWhoisAsAdmin=true,"
+ " visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false,"
+ " allowedToSetRegistryLockPassword=false}\n"
+ " FINAL CONTENTS:\n"
+ " {name=Test Registrar 1,"
+ " emailAddress=test.registrar1@example.com, registrarId=registrarId,"
+ " registryLockEmailAddress=null, phoneNumber=+1.9999999999,"
+ " faxNumber=+1.9999999991, types=[ADMIN, WHOIS], loginEmailAddress=null,"
+ " faxNumber=+1.9999999991, types=[ADMIN],"
+ " visibleInWhoisAsAdmin=true, visibleInWhoisAsTech=false,"
+ " visibleInDomainWhoisAsAbuse=false,"
+ " allowedToSetRegistryLockPassword=false}\n")
@@ -104,6 +104,8 @@ public class WhoisRegistrarFieldsActionTest {
ImmutableMap.of(
"whoisServer",
"whois.nic.google",
"icannReferralEmail",
"lol@sloth.test",
"phoneNumber",
"+1.4155552671",
"faxNumber",
@@ -29,6 +29,8 @@ import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STAT
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.model.common.FeatureFlag;
import google.registry.model.console.RegistrarRole;
import google.registry.model.domain.Domain;
import google.registry.model.domain.RegistryLock;
@@ -110,15 +112,6 @@ class RegistrarConsoleScreenshotTest extends WebDriverTestCase {
driver.diffPage("page");
}
/** Admins shouldn't have the "add" button */
@RetryingTest(3)
void settingsContact_asAdmin() throws Throwable {
server.setIsAdmin(true);
driver.get(server.getUrl("/registrar?clientId=NewRegistrar#contact-settings"));
driver.waitForDisplayedElement(By.tagName("h1"));
driver.diffPage("page");
}
@RetryingTest(3)
void settingsContactItem() throws Throwable {
driver.get(server.getUrl("/registrar#contact-settings/johndoe@theregistrar.com"));
@@ -518,6 +511,18 @@ class RegistrarConsoleScreenshotTest extends WebDriverTestCase {
driver.diffPage("page");
}
@RetryingTest(3)
void deprecationWarning_active() throws Throwable {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(FeatureFlag.FeatureName.NEW_CONSOLE)
.setStatusMap(ImmutableSortedMap.of(START_OF_TIME, FeatureFlag.FeatureStatus.ACTIVE))
.build());
driver.get(server.getUrl("/registrar"));
driver.waitForDisplayedElement(By.tagName("h1"));
driver.diffPage("page");
}
private static void createDomainAndSaveLock() {
createTld("tld");
Domain domain = persistActiveDomain("example.tld");
@@ -0,0 +1,26 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<update>
<domain:update
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>example.tld</domain:name>
<domain:add>
<domain:ns>
<domain:hostObj>ns2.example.foo</domain:hostObj>
</domain:ns>
<domain:contact type="tech">mak21</domain:contact>
<domain:status s="clientHold"
lang="en">Payment overdue.</domain:status>
</domain:add>
<domain:rem>
<domain:ns>
<domain:hostObj>ns1.example.foo</domain:hostObj>
</domain:ns>
<domain:contact type="tech">sh8013</domain:contact>
<domain:status s="clientUpdateProhibited"/>
</domain:rem>
</domain:update>
</update>
<clTRID>ABC-12345</clTRID>
</command>
</epp>
@@ -11,9 +11,9 @@ emailAddress: the.registrar@example.com -> thase@the.registrar
url: http://my.fake.url -> http://my.new.url
contacts:
ADDED:
{name=Extra Terrestrial, emailAddress=etphonehome@example.com, registrarId=TheRegistrar, registryLockEmailAddress=null, phoneNumber=+1.2345678901, faxNumber=null, types=[ADMIN, BILLING, TECH, WHOIS], loginEmailAddress=null, visibleInWhoisAsAdmin=true, visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false, allowedToSetRegistryLockPassword=false}
{name=Extra Terrestrial, emailAddress=etphonehome@example.com, registrarId=TheRegistrar, registryLockEmailAddress=null, phoneNumber=+1.2345678901, faxNumber=null, types=[ADMIN, BILLING, TECH, WHOIS], visibleInWhoisAsAdmin=true, visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false, allowedToSetRegistryLockPassword=false}
REMOVED:
{name=John Doe, emailAddress=johndoe@theregistrar.com, registrarId=TheRegistrar, registryLockEmailAddress=null, phoneNumber=+1.1234567890, faxNumber=null, types=[ADMIN], loginEmailAddress=johndoe@theregistrar.com, visibleInWhoisAsAdmin=false, visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false, allowedToSetRegistryLockPassword=false}
{name=John Doe, emailAddress=johndoe@theregistrar.com, registrarId=TheRegistrar, registryLockEmailAddress=null, phoneNumber=+1.1234567890, faxNumber=null, types=[ADMIN], visibleInWhoisAsAdmin=false, visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false, allowedToSetRegistryLockPassword=false}
FINAL CONTENTS:
{name=Extra Terrestrial, emailAddress=etphonehome@example.com, registrarId=TheRegistrar, registryLockEmailAddress=null, phoneNumber=+1.2345678901, faxNumber=null, types=[ADMIN, BILLING, TECH, WHOIS], loginEmailAddress=null, visibleInWhoisAsAdmin=true, visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false, allowedToSetRegistryLockPassword=false},
{name=Marla Singer, emailAddress=Marla.Singer@crr.com, registrarId=TheRegistrar, registryLockEmailAddress=Marla.Singer.RegistryLock@crr.com, phoneNumber=+1.2128675309, faxNumber=null, types=[TECH], loginEmailAddress=Marla.Singer@crr.com, visibleInWhoisAsAdmin=false, visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false, allowedToSetRegistryLockPassword=false}
{name=Extra Terrestrial, emailAddress=etphonehome@example.com, registrarId=TheRegistrar, registryLockEmailAddress=null, phoneNumber=+1.2345678901, faxNumber=null, types=[ADMIN, BILLING, TECH, WHOIS], visibleInWhoisAsAdmin=true, visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false, allowedToSetRegistryLockPassword=false},
{name=Marla Singer, emailAddress=Marla.Singer@crr.com, registrarId=TheRegistrar, registryLockEmailAddress=Marla.Singer.RegistryLock@crr.com, phoneNumber=+1.2128675309, faxNumber=null, types=[TECH], visibleInWhoisAsAdmin=false, visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false, allowedToSetRegistryLockPassword=false}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

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