From ef409e8b87c0b1811d275e58af6a9ad203b43cd3 Mon Sep 17 00:00:00 2001 From: Gus Brodman Date: Tue, 22 Jul 2025 16:51:38 -0400 Subject: [PATCH] Add Registry Lock password reset on front end This is only enabled for admins, for now at least. It sends an email to the registry lock email address to reset it. --- .../app/shared/services/backend.service.ts | 11 ++ .../src/app/users/userDetails.component.html | 10 +- .../src/app/users/userDetails.component.scss | 3 + .../src/app/users/userEditForm.component.html | 92 ++++++------ .../src/app/users/userEditForm.component.scss | 20 +++ .../src/app/users/userEditForm.component.ts | 85 ++++++++++- console-webapp/src/app/users/users.service.ts | 1 + .../google/registry/model/console/User.java | 35 +++-- ...eCheckResponseExtensionItemCommandV12.java | 5 +- .../registry/model/registrar/Registrar.java | 135 +++++++++--------- .../model/registrar/RegistrarPoc.java | 65 +++++---- .../ui/server/console/ConsoleUsersAction.java | 10 +- .../console/ConsoleUsersActionTest.java | 25 ++-- 13 files changed, 322 insertions(+), 175 deletions(-) diff --git a/console-webapp/src/app/shared/services/backend.service.ts b/console-webapp/src/app/shared/services/backend.service.ts index 1998881b73e..2d17eb981c8 100644 --- a/console-webapp/src/app/shared/services/backend.service.ts +++ b/console-webapp/src/app/shared/services/backend.service.ts @@ -280,4 +280,15 @@ export class BackendService { `/console-api/registry-lock-verify?lockVerificationCode=${lockVerificationCode}` ); } + + requestRegistryLockPasswordReset( + registrarId: string, + registryLockEmail: string + ) { + return this.http.post('/console-api/password-reset-request', { + type: 'REGISTRY_LOCK', + registrarId, + registryLockEmail, + }); + } } diff --git a/console-webapp/src/app/users/userDetails.component.html b/console-webapp/src/app/users/userDetails.component.html index d670a546db2..acd88fa7f9e 100644 --- a/console-webapp/src/app/users/userDetails.component.html +++ b/console-webapp/src/app/users/userDetails.component.html @@ -80,7 +80,15 @@

User details

roleToDescription(userDetails().role) }} - @if (userDetails().password) { + @if (userDetails().registryLockEmailAddress) { + + + Registry Lock email + {{ + userDetails().registryLockEmailAddress + }} + + } @if (userDetails().password) { Password diff --git a/console-webapp/src/app/users/userDetails.component.scss b/console-webapp/src/app/users/userDetails.component.scss index 5ce1026a92b..4993faf0b01 100644 --- a/console-webapp/src/app/users/userDetails.component.scss +++ b/console-webapp/src/app/users/userDetails.component.scss @@ -35,5 +35,8 @@ border: 1px solid #ddd; border-radius: 10px; } + .console-app__list-key { + width: 160px; + } } } diff --git a/console-webapp/src/app/users/userEditForm.component.html b/console-webapp/src/app/users/userEditForm.component.html index 4a77fce55c5..0b1aab4b3d3 100644 --- a/console-webapp/src/app/users/userEditForm.component.html +++ b/console-webapp/src/app/users/userEditForm.component.html @@ -1,45 +1,57 @@ -
-

- - User name prefix: - help_outline - - -

-

- - User Role: - help_outline - - Editor - Viewer - - -

+
+ +

+ + User name prefix: + help_outline + + +

+

+ + User Role: + help_outline + + Editor + Viewer + + +

+ + + @if(userDataService.userData()?.isAdmin) { - + } +
diff --git a/console-webapp/src/app/users/userEditForm.component.scss b/console-webapp/src/app/users/userEditForm.component.scss index e69de29bb2d..e35847646cc 100644 --- a/console-webapp/src/app/users/userEditForm.component.scss +++ b/console-webapp/src/app/users/userEditForm.component.scss @@ -0,0 +1,20 @@ +// Copyright 2025 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +.console-app__user-edit { + button { + display: block; + margin-bottom: 5px; + } +} diff --git a/console-webapp/src/app/users/userEditForm.component.ts b/console-webapp/src/app/users/userEditForm.component.ts index 04448f8dbb8..d22940b9215 100644 --- a/console-webapp/src/app/users/userEditForm.component.ts +++ b/console-webapp/src/app/users/userEditForm.component.ts @@ -17,13 +17,56 @@ import { Component, ElementRef, EventEmitter, + Inject, input, Output, ViewChild, } from '@angular/core'; import { MaterialModule } from '../material.module'; import { FormsModule } from '@angular/forms'; -import { User } from './users.service'; +import { User, UsersService } from './users.service'; +import { UserDataService } from '../shared/services/userData.service'; +import { BackendService } from '../shared/services/backend.service'; +import { RegistrarService } from '../registrar/registrar.service'; +import { + MAT_DIALOG_DATA, + MatDialog, + MatDialogRef, +} from '@angular/material/dialog'; +import { filter, switchMap, take } from 'rxjs'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { HttpErrorResponse } from '@angular/common/http'; + +@Component({ + selector: 'app-reset-lock-password-dialog', + template: ` +

Please confirm the password reset:

+ + This will send a registry lock password reset email to + {{ data.registryLockEmailAddress }}. + + + + + + `, + imports: [CommonModule, MaterialModule], +}) +export class ResetRegistryLockPasswordComponent { + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) + public data: { registryLockEmailAddress: string } + ) {} + + onSave(): void { + this.dialogRef.close(true); + } + + onCancel(): void { + this.dialogRef.close(false); + } +} @Component({ selector: 'app-user-edit-form', @@ -39,12 +82,22 @@ export class UserEditFormComponent { { emailAddress: '', role: 'ACCOUNT_MANAGER', + registryLockEmailAddress: '', }, { transform: (user: User) => structuredClone(user) } ); @Output() onEditComplete = new EventEmitter(); + constructor( + protected userDataService: UserDataService, + private backendService: BackendService, + private resetRegistryLockPasswordDialog: MatDialog, + private registrarService: RegistrarService, + private usersService: UsersService, + private _snackBar: MatSnackBar + ) {} + saveEdit(e: SubmitEvent) { e.preventDefault(); if (this.form.nativeElement.checkValidity()) { @@ -53,4 +106,34 @@ export class UserEditFormComponent { this.form.nativeElement.reportValidity(); } } + + sendRegistryLockPasswordResetRequest() { + return this.backendService.requestRegistryLockPasswordReset( + this.registrarService.registrarId(), + this.user().registryLockEmailAddress! + ); + } + + requestRegistryLockPasswordReset() { + const dialogRef = this.resetRegistryLockPasswordDialog.open( + ResetRegistryLockPasswordComponent, + { + data: { + registryLockEmailAddress: this.user().registryLockEmailAddress, + }, + } + ); + dialogRef + .afterClosed() + .pipe( + take(1), + filter((result) => !!result) + ) + .pipe(switchMap((_) => this.sendRegistryLockPasswordResetRequest())) + .subscribe({ + next: (_) => this.usersService.currentlyOpenUserEmail.set(''), + error: (err: HttpErrorResponse) => + this._snackBar.open(err.error || err.message), + }); + } } diff --git a/console-webapp/src/app/users/users.service.ts b/console-webapp/src/app/users/users.service.ts index ed5c9ad482c..c65f194cc28 100644 --- a/console-webapp/src/app/users/users.service.ts +++ b/console-webapp/src/app/users/users.service.ts @@ -33,6 +33,7 @@ export interface User { emailAddress: string; role: string; password?: string; + registryLockEmailAddress?: string; } @Injectable() diff --git a/core/src/main/java/google/registry/model/console/User.java b/core/src/main/java/google/registry/model/console/User.java index 4aba7ef2d9d..67467a421b1 100644 --- a/core/src/main/java/google/registry/model/console/User.java +++ b/core/src/main/java/google/registry/model/console/User.java @@ -62,7 +62,7 @@ public class User extends UpdateAutoTimestampEntity implements Buildable { @Id @Expose String emailAddress; /** Optional external email address to use for registry lock confirmation emails. */ - @Column String registryLockEmailAddress; + @Column @Expose String registryLockEmailAddress; /** Roles (which grant permissions) associated with this user. */ @Expose @@ -250,51 +250,50 @@ public boolean hasAnyRegistryLockPermission() { } @Override - public Builder asBuilder() { - return new Builder<>(clone(this)); + public Builder asBuilder() { + return new Builder(clone(this)); } /** Builder for constructing immutable {@link User} objects. */ - public static class Builder> - extends GenericBuilder { + public static class Builder extends Buildable.Builder { public Builder() {} - public Builder(T abstractUser) { - super(abstractUser); + public Builder(User user) { + super(user); } @Override - public T build() { + public User build() { checkArgumentNotNull(getInstance().emailAddress, "Email address cannot be null"); checkArgumentNotNull(getInstance().userRoles, "User roles cannot be null"); return super.build(); } - public B setEmailAddress(String emailAddress) { + public Builder setEmailAddress(String emailAddress) { getInstance().emailAddress = checkValidEmail(emailAddress); - return thisCastToDerived(); + return this; } - public B setRegistryLockEmailAddress(@Nullable String registryLockEmailAddress) { + public Builder setRegistryLockEmailAddress(@Nullable String registryLockEmailAddress) { getInstance().registryLockEmailAddress = registryLockEmailAddress == null ? null : checkValidEmail(registryLockEmailAddress); - return thisCastToDerived(); + return this; } - public B setUserRoles(UserRoles userRoles) { + public Builder setUserRoles(UserRoles userRoles) { checkArgumentNotNull(userRoles, "User roles cannot be null"); getInstance().userRoles = userRoles; - return thisCastToDerived(); + return this; } - public B removeRegistryLockPassword() { + public Builder removeRegistryLockPassword() { getInstance().registryLockPasswordHash = null; getInstance().registryLockPasswordSalt = null; - return thisCastToDerived(); + return this; } - public B setRegistryLockPassword(String registryLockPassword) { + public Builder setRegistryLockPassword(String registryLockPassword) { checkArgument( getInstance().hasAnyRegistryLockPermission(), "User has no registry lock permission"); checkArgument( @@ -304,7 +303,7 @@ public B setRegistryLockPassword(String registryLockPassword) { byte[] salt = SALT_SUPPLIER.get(); getInstance().registryLockPasswordSalt = base64().encode(salt); getInstance().registryLockPasswordHash = hashPassword(registryLockPassword, salt); - return thisCastToDerived(); + return this; } } } diff --git a/core/src/main/java/google/registry/model/domain/fee12/FeeCheckResponseExtensionItemCommandV12.java b/core/src/main/java/google/registry/model/domain/fee12/FeeCheckResponseExtensionItemCommandV12.java index dcfd8904d4d..6821365a6d6 100644 --- a/core/src/main/java/google/registry/model/domain/fee12/FeeCheckResponseExtensionItemCommandV12.java +++ b/core/src/main/java/google/registry/model/domain/fee12/FeeCheckResponseExtensionItemCommandV12.java @@ -18,7 +18,7 @@ import com.google.common.base.Ascii; import com.google.common.collect.ImmutableList; -import google.registry.model.Buildable.GenericBuilder; +import google.registry.model.Buildable; import google.registry.model.ImmutableObject; import google.registry.model.domain.Period; import google.registry.model.domain.fee.Fee; @@ -77,8 +77,7 @@ public String getFeeClass() { } /** Builder for {@link FeeCheckResponseExtensionItemCommandV12}. */ - public static class Builder - extends GenericBuilder { + public static class Builder extends Buildable.Builder { public Builder setCommandName(CommandName commandName) { getInstance().commandName = Ascii.toLowerCase(commandName.name()); diff --git a/core/src/main/java/google/registry/model/registrar/Registrar.java b/core/src/main/java/google/registry/model/registrar/Registrar.java index f61156885b6..520b3cf3e5d 100644 --- a/core/src/main/java/google/registry/model/registrar/Registrar.java +++ b/core/src/main/java/google/registry/model/registrar/Registrar.java @@ -690,8 +690,8 @@ public void setRegistrarId(String registrarId) { } @Override - public Builder asBuilder() { - return new Builder<>(clone(this)); + public Builder asBuilder() { + return new Builder(clone(this)); } @Override @@ -706,59 +706,58 @@ public static VKey createVKey(String registrarId) { } /** A builder for constructing {@link Registrar}, since it is immutable. */ - public static class Builder> - extends GenericBuilder { + public static class Builder extends Buildable.Builder { public Builder() {} - public Builder(T instance) { + public Builder(Registrar instance) { super(instance); } - public B setRegistrarId(String registrarId) { + public Builder setRegistrarId(String registrarId) { // Registrar id must be [3,16] chars long. See "clIDType" in the base EPP schema of RFC 5730. // (Need to validate this here as there's no matching EPP XSD for validation.) checkArgument( Range.closed(3, 16).contains(registrarId.length()), "Registrar ID must be 3-16 characters long."); getInstance().registrarId = registrarId; - return thisCastToDerived(); + return this; } - public B setIanaIdentifier(@Nullable Long ianaIdentifier) { + public Builder setIanaIdentifier(@Nullable Long ianaIdentifier) { checkArgument( ianaIdentifier == null || ianaIdentifier > 0, "IANA ID must be a positive number"); getInstance().ianaIdentifier = ianaIdentifier; - return thisCastToDerived(); + return this; } - public B setPoNumber(Optional poNumber) { + public Builder setPoNumber(Optional poNumber) { getInstance().poNumber = poNumber.orElse(null); - return thisCastToDerived(); + return this; } - public B setBillingAccountMap(@Nullable Map billingAccountMap) { + public Builder setBillingAccountMap(@Nullable Map billingAccountMap) { getInstance().billingAccountMap = nullToEmptyImmutableCopy(billingAccountMap); - return thisCastToDerived(); + return this; } - public B setRegistrarName(String registrarName) { + public Builder setRegistrarName(String registrarName) { getInstance().registrarName = registrarName; - return thisCastToDerived(); + return this; } - public B setType(Type type) { + public Builder setType(Type type) { getInstance().type = type; - return thisCastToDerived(); + return this; } - public B setState(State state) { + public Builder setState(State state) { getInstance().state = state; - return thisCastToDerived(); + return this; } - public B setAllowedTlds(Set allowedTlds) { + public Builder setAllowedTlds(Set allowedTlds) { getInstance().allowedTlds = ImmutableSortedSet.copyOf(assertTldsExist(allowedTlds)); - return thisCastToDerived(); + return this; } /** @@ -771,7 +770,7 @@ public B setAllowedTlds(Set allowedTlds) { * {@code .now()} when saving the Registry entity to make sure it's actually saved before trying * to set the allowed TLDs. */ - public B setAllowedTldsUncached(Set allowedTlds) { + public Builder setAllowedTldsUncached(Set allowedTlds) { ImmutableSet> newTldKeys = Sets.difference(allowedTlds, getInstance().getAllowedTlds()).stream() .map(Tld::createVKey) @@ -780,10 +779,10 @@ public B setAllowedTldsUncached(Set allowedTlds) { Sets.difference(newTldKeys, tm().loadByKeysIfPresent(newTldKeys).keySet()); checkArgument(missingTldKeys.isEmpty(), "Trying to set nonexistent TLDs: %s", missingTldKeys); getInstance().allowedTlds = ImmutableSortedSet.copyOf(allowedTlds); - return thisCastToDerived(); + return this; } - public B setClientCertificate(String clientCertificate, DateTime now) { + public Builder setClientCertificate(String clientCertificate, DateTime now) { clientCertificate = emptyToNull(clientCertificate); String clientCertificateHash = calculateHash(clientCertificate); if (!Objects.equals(clientCertificate, getInstance().clientCertificate) @@ -792,23 +791,23 @@ public B setClientCertificate(String clientCertificate, DateTime now) { getInstance().clientCertificateHash = clientCertificateHash; getInstance().lastCertificateUpdateTime = now; } - return thisCastToDerived(); + return this; } - public B setLastExpiringCertNotificationSentDate(DateTime now) { + public Builder setLastExpiringCertNotificationSentDate(DateTime now) { checkArgumentNotNull(now, "Registrar lastExpiringCertNotificationSentDate cannot be null"); getInstance().lastExpiringCertNotificationSentDate = now; - return thisCastToDerived(); + return this; } - public B setLastExpiringFailoverCertNotificationSentDate(DateTime now) { + public Builder setLastExpiringFailoverCertNotificationSentDate(DateTime now) { checkArgumentNotNull( now, "Registrar lastExpiringFailoverCertNotificationSentDate cannot be null"); getInstance().lastExpiringFailoverCertNotificationSentDate = now; - return thisCastToDerived(); + return this; } - public B setFailoverClientCertificate(String clientCertificate, DateTime now) { + public Builder setFailoverClientCertificate(String clientCertificate, DateTime now) { clientCertificate = emptyToNull(clientCertificate); String clientCertificateHash = calculateHash(clientCertificate); if (!Objects.equals(clientCertificate, getInstance().failoverClientCertificate) @@ -817,13 +816,13 @@ public B setFailoverClientCertificate(String clientCertificate, DateTime now) { getInstance().failoverClientCertificateHash = clientCertificateHash; getInstance().lastCertificateUpdateTime = now; } - return thisCastToDerived(); + return this; } - public B setLastPocVerificationDate(DateTime now) { + public Builder setLastPocVerificationDate(DateTime now) { checkArgumentNotNull(now, "Registrar lastPocVerificationDate cannot be null"); getInstance().lastPocVerificationDate = now; - return thisCastToDerived(); + return this; } private static String calculateHash(String clientCertificate) { @@ -855,75 +854,75 @@ private static boolean isNotADuplicateIanaId( Objects.equals(newInstance.ianaIdentifier, registrar.getIanaIdentifier())); } - public B setContactsRequireSyncing(boolean contactsRequireSyncing) { + public Builder setContactsRequireSyncing(boolean contactsRequireSyncing) { getInstance().contactsRequireSyncing = contactsRequireSyncing; - return thisCastToDerived(); + return this; } - public B setIpAddressAllowList(Iterable ipAddressAllowList) { + public Builder setIpAddressAllowList(Iterable ipAddressAllowList) { getInstance().ipAddressAllowList = ImmutableList.copyOf(ipAddressAllowList); - return thisCastToDerived(); + return this; } - public B setLocalizedAddress(RegistrarAddress localizedAddress) { + public Builder setLocalizedAddress(RegistrarAddress localizedAddress) { getInstance().localizedAddress = localizedAddress; - return thisCastToDerived(); + return this; } - public B setInternationalizedAddress(RegistrarAddress internationalizedAddress) { + public Builder setInternationalizedAddress(RegistrarAddress internationalizedAddress) { getInstance().internationalizedAddress = internationalizedAddress; - return thisCastToDerived(); + return this; } - public B setPhoneNumber(String phoneNumber) { + public Builder setPhoneNumber(String phoneNumber) { getInstance().phoneNumber = (phoneNumber == null) ? null : checkValidPhoneNumber(phoneNumber); - return thisCastToDerived(); + return this; } - public B setFaxNumber(String faxNumber) { + public Builder setFaxNumber(String faxNumber) { getInstance().faxNumber = (faxNumber == null) ? null : checkValidPhoneNumber(faxNumber); - return thisCastToDerived(); + return this; } - public B setEmailAddress(String emailAddress) { + public Builder setEmailAddress(String emailAddress) { getInstance().emailAddress = checkValidEmail(emailAddress); - return thisCastToDerived(); + return this; } - public B setWhoisServer(String whoisServer) { + public Builder setWhoisServer(String whoisServer) { getInstance().whoisServer = whoisServer; - return thisCastToDerived(); + return this; } - public B setRdapBaseUrls(Set rdapBaseUrls) { + public Builder setRdapBaseUrls(Set rdapBaseUrls) { getInstance().rdapBaseUrls = ImmutableSet.copyOf(rdapBaseUrls); - return thisCastToDerived(); + return this; } - public B setBlockPremiumNames(boolean blockPremiumNames) { + public Builder setBlockPremiumNames(boolean blockPremiumNames) { getInstance().blockPremiumNames = blockPremiumNames; - return thisCastToDerived(); + return this; } - public B setUrl(String url) { + public Builder setUrl(String url) { getInstance().url = url; - return thisCastToDerived(); + return this; } - public B setIcannReferralEmail(String icannReferralEmail) { + public Builder setIcannReferralEmail(String icannReferralEmail) { getInstance().icannReferralEmail = checkValidEmail(icannReferralEmail); - return thisCastToDerived(); + return this; } - public B setDriveFolderId(@Nullable String driveFolderId) { + public Builder setDriveFolderId(@Nullable String driveFolderId) { checkArgument( driveFolderId == null || !driveFolderId.contains("/"), "Drive folder ID must not be a full URL"); getInstance().driveFolderId = driveFolderId; - return thisCastToDerived(); + return this; } - public B setPassword(String password) { + public Builder setPassword(String password) { // Passwords must be [6,16] chars long. See "pwType" in the base EPP schema of RFC 5730. checkArgument( Range.closed(6, 16).contains(nullToEmpty(password).length()), @@ -931,7 +930,7 @@ public B setPassword(String password) { byte[] salt = SALT_SUPPLIER.get(); getInstance().salt = base64().encode(salt); getInstance().passwordHash = hashPassword(password, salt); - return thisCastToDerived(); + return this; } /** @@ -939,18 +938,18 @@ public B setPassword(String password) { * * @throws IllegalArgumentException if provided passcode is not 5-digit numeric */ - public B setPhonePasscode(String phonePasscode) { + public Builder setPhonePasscode(String phonePasscode) { checkArgument( phonePasscode == null || PHONE_PASSCODE_PATTERN.matcher(phonePasscode).matches(), "Not a valid telephone passcode (must be 5 digits long): %s", phonePasscode); getInstance().phonePasscode = phonePasscode; - return thisCastToDerived(); + return this; } - public B setRegistryLockAllowed(boolean registryLockAllowed) { + public Builder setRegistryLockAllowed(boolean registryLockAllowed) { getInstance().registryLockAllowed = registryLockAllowed; - return thisCastToDerived(); + return this; } /** @@ -958,14 +957,14 @@ public B setRegistryLockAllowed(boolean registryLockAllowed) { * and breaks the verification that an object has not been updated since it was copied. */ @VisibleForTesting - public B setLastUpdateTime(DateTime timestamp) { + public Builder setLastUpdateTime(DateTime timestamp) { getInstance().setUpdateTimestamp(UpdateAutoTimestamp.create(timestamp)); - return thisCastToDerived(); + return this; } /** Build the registrar, nullifying empty fields. */ @Override - public T build() { + public Registrar build() { checkArgumentNotNull(getInstance().type, "Registrar type cannot be null"); checkArgumentNotNull(getInstance().registrarName, "Registrar name cannot be null"); checkArgument( diff --git a/core/src/main/java/google/registry/model/registrar/RegistrarPoc.java b/core/src/main/java/google/registry/model/registrar/RegistrarPoc.java index e942c82a3fe..e5f8eea25fe 100644 --- a/core/src/main/java/google/registry/model/registrar/RegistrarPoc.java +++ b/core/src/main/java/google/registry/model/registrar/RegistrarPoc.java @@ -31,7 +31,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedSet; import com.google.gson.annotations.Expose; -import google.registry.model.Buildable.GenericBuilder; +import google.registry.model.Buildable; import google.registry.model.ImmutableObject; import google.registry.model.JsonMapBuilder; import google.registry.model.Jsonifiable; @@ -225,8 +225,8 @@ public boolean getVisibleInDomainWhoisAsAbuse() { return visibleInDomainWhoisAsAbuse; } - public Builder asBuilder() { - return new Builder<>(clone(this)); + public Builder asBuilder() { + return new Builder(clone(this)); } public boolean isAllowedToSetRegistryLockPassword() { @@ -332,17 +332,16 @@ public void setRegistrarId(String registrarId) { } /** A builder for constructing a {@link RegistrarPoc}, since it is immutable. */ - public static class Builder> - extends GenericBuilder { + public static class Builder extends Buildable.Builder { public Builder() {} - protected Builder(T instance) { + protected Builder(RegistrarPoc instance) { super(instance); } /** Build the registrar, nullifying empty fields. */ @Override - public T build() { + public RegistrarPoc build() { checkNotNull(getInstance().registrarId, "Registrar ID cannot be null"); checkValidEmail(getInstance().emailAddress); // Check allowedToSetRegistryLockPassword here because if we want to allow the user to set @@ -356,71 +355,71 @@ public T build() { return cloneEmptyToNull(super.build()); } - public B setName(String name) { + public Builder setName(String name) { getInstance().name = name; - return thisCastToDerived(); + return this; } - public B setEmailAddress(String emailAddress) { + public Builder setEmailAddress(String emailAddress) { getInstance().emailAddress = emailAddress; - return thisCastToDerived(); + return this; } - public B setRegistryLockEmailAddress(@Nullable String registryLockEmailAddress) { + public Builder setRegistryLockEmailAddress(@Nullable String registryLockEmailAddress) { getInstance().registryLockEmailAddress = registryLockEmailAddress; - return thisCastToDerived(); + return this; } - public B setPhoneNumber(String phoneNumber) { + public Builder setPhoneNumber(String phoneNumber) { getInstance().phoneNumber = phoneNumber; - return thisCastToDerived(); + return this; } - public B setRegistrarId(String registrarId) { + public Builder setRegistrarId(String registrarId) { getInstance().registrarId = registrarId; - return thisCastToDerived(); + return this; } - public B setRegistrar(Registrar registrar) { + public Builder setRegistrar(Registrar registrar) { getInstance().registrarId = registrar.getRegistrarId(); - return thisCastToDerived(); + return this; } - public B setFaxNumber(String faxNumber) { + public Builder setFaxNumber(String faxNumber) { getInstance().faxNumber = faxNumber; - return thisCastToDerived(); + return this; } - public B setTypes(Iterable types) { + public Builder setTypes(Iterable types) { getInstance().types = ImmutableSet.copyOf(types); - return thisCastToDerived(); + return this; } - public B setVisibleInWhoisAsAdmin(boolean visible) { + public Builder setVisibleInWhoisAsAdmin(boolean visible) { getInstance().visibleInWhoisAsAdmin = visible; - return thisCastToDerived(); + return this; } - public B setVisibleInWhoisAsTech(boolean visible) { + public Builder setVisibleInWhoisAsTech(boolean visible) { getInstance().visibleInWhoisAsTech = visible; - return thisCastToDerived(); + return this; } - public B setVisibleInDomainWhoisAsAbuse(boolean visible) { + public Builder setVisibleInDomainWhoisAsAbuse(boolean visible) { getInstance().visibleInDomainWhoisAsAbuse = visible; - return thisCastToDerived(); + return this; } - public B setAllowedToSetRegistryLockPassword(boolean allowedToSetRegistryLockPassword) { + public Builder setAllowedToSetRegistryLockPassword(boolean allowedToSetRegistryLockPassword) { if (allowedToSetRegistryLockPassword) { getInstance().registryLockPasswordSalt = null; getInstance().registryLockPasswordHash = null; } getInstance().allowedToSetRegistryLockPassword = allowedToSetRegistryLockPassword; - return thisCastToDerived(); + return this; } - public B setRegistryLockPassword(String registryLockPassword) { + public Builder setRegistryLockPassword(String registryLockPassword) { checkArgument( getInstance().allowedToSetRegistryLockPassword, "Not allowed to set registry lock password for this contact"); @@ -430,7 +429,7 @@ public B setRegistryLockPassword(String registryLockPassword) { getInstance().registryLockPasswordSalt = base64().encode(salt); getInstance().registryLockPasswordHash = hashPassword(registryLockPassword, salt); getInstance().allowedToSetRegistryLockPassword = false; - return thisCastToDerived(); + return this; } } diff --git a/core/src/main/java/google/registry/ui/server/console/ConsoleUsersAction.java b/core/src/main/java/google/registry/ui/server/console/ConsoleUsersAction.java index 8ae98c09db4..6246e87fc85 100644 --- a/core/src/main/java/google/registry/ui/server/console/ConsoleUsersAction.java +++ b/core/src/main/java/google/registry/ui/server/console/ConsoleUsersAction.java @@ -119,6 +119,7 @@ protected void getHandler(User user) { u -> new UserData( u.getEmailAddress(), + u.getRegistryLockEmailAddress().orElse(null), u.getUserRoles().getRegistrarRoles().get(registrarId).toString(), null)) .collect(Collectors.toList()); @@ -237,7 +238,9 @@ private void runCreate() throws IOException { .setPayload( consoleApiParams .gson() - .toJson(new UserData(newEmail, ACCOUNT_MANAGER.toString(), newUser.getPassword()))); + .toJson( + new UserData( + newEmail, null, ACCOUNT_MANAGER.toString(), newUser.getPassword()))); finishAndPersistConsoleUpdateHistory( new ConsoleUpdateHistory.Builder() .setType(ConsoleUpdateHistory.Type.USER_CREATE) @@ -345,5 +348,8 @@ private boolean sendConfirmationEmail(String registrarId, String emailAddress, S } public record UserData( - @Expose String emailAddress, @Expose String role, @Expose @Nullable String password) {} + @Expose String emailAddress, + @Expose String registryLockEmailAddress, + @Expose String role, + @Expose @Nullable String password) {} } diff --git a/core/src/test/java/google/registry/ui/server/console/ConsoleUsersActionTest.java b/core/src/test/java/google/registry/ui/server/console/ConsoleUsersActionTest.java index 517d8bf59f1..ff2a407193a 100644 --- a/core/src/test/java/google/registry/ui/server/console/ConsoleUsersActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/ConsoleUsersActionTest.java @@ -153,7 +153,7 @@ void testFailure_invalidPrefix() throws IOException { createAction( Optional.of(ConsoleApiParamsUtils.createFake(authResult)), Optional.of("POST"), - Optional.of(new UserData("a@d", RegistrarRole.ACCOUNT_MANAGER.toString(), null))); + Optional.of(new UserData("a@d", null, RegistrarRole.ACCOUNT_MANAGER.toString(), null))); action.cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils(); when(directory.users()).thenReturn(users); when(users.insert(any(com.google.api.services.directory.model.User.class))).thenReturn(insert); @@ -170,7 +170,7 @@ void testSuccess_createsUser() throws IOException { createAction( Optional.of(ConsoleApiParamsUtils.createFake(authResult)), Optional.of("POST"), - Optional.of(new UserData("lol", RegistrarRole.ACCOUNT_MANAGER.toString(), null))); + Optional.of(new UserData("lol", null, RegistrarRole.ACCOUNT_MANAGER.toString(), null))); action.cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils(); when(directory.users()).thenReturn(users); when(users.insert(any(com.google.api.services.directory.model.User.class))).thenReturn(insert); @@ -195,7 +195,8 @@ void testFailure_noPermissionToDeleteUser() throws IOException { Optional.of(ConsoleApiParamsUtils.createFake(authResult)), Optional.of("DELETE"), Optional.of( - new UserData("test3@test.com", RegistrarRole.ACCOUNT_MANAGER.toString(), null))); + new UserData( + "test3@test.com", null, RegistrarRole.ACCOUNT_MANAGER.toString(), null))); when(directory.users()).thenReturn(users); when(users.delete(any(String.class))).thenReturn(delete); action.run(); @@ -213,7 +214,8 @@ void testFailure_userDoesntExist() throws IOException { Optional.of(ConsoleApiParamsUtils.createFake(authResult)), Optional.of("DELETE"), Optional.of( - new UserData("email-1@email.com", RegistrarRole.ACCOUNT_MANAGER.toString(), null))); + new UserData( + "email-1@email.com", null, RegistrarRole.ACCOUNT_MANAGER.toString(), null))); when(directory.users()).thenReturn(users); when(users.delete(any(String.class))).thenReturn(delete); action.run(); @@ -235,7 +237,8 @@ void testSuccess_deletesUser() throws IOException { Optional.of(ConsoleApiParamsUtils.createFake(authResult)), Optional.of("DELETE"), Optional.of( - new UserData("test2@test.com", RegistrarRole.ACCOUNT_MANAGER.toString(), null))); + new UserData( + "test2@test.com", null, RegistrarRole.ACCOUNT_MANAGER.toString(), null))); action.cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils(); when(directory.users()).thenReturn(users); when(users.delete(any(String.class))).thenReturn(delete); @@ -274,7 +277,8 @@ void testSuccess_removesRole() throws IOException { Optional.of(ConsoleApiParamsUtils.createFake(authResult)), Optional.of("DELETE"), Optional.of( - new UserData("test4@test.com", RegistrarRole.ACCOUNT_MANAGER.toString(), null))); + new UserData( + "test4@test.com", null, RegistrarRole.ACCOUNT_MANAGER.toString(), null))); action.cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils(); when(directory.users()).thenReturn(users); @@ -318,7 +322,8 @@ void testFailure_limitedTo4UsersPerRegistrar() throws IOException { Optional.of(ConsoleApiParamsUtils.createFake(authResult)), Optional.of("POST"), Optional.of( - new UserData("test3@test.com", RegistrarRole.ACCOUNT_MANAGER.toString(), null))); + new UserData( + "test3@test.com", null, RegistrarRole.ACCOUNT_MANAGER.toString(), null))); action.cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils(); when(directory.users()).thenReturn(users); when(users.insert(any(com.google.api.services.directory.model.User.class))).thenReturn(insert); @@ -348,7 +353,8 @@ void testSuccess_updatesUserRole() throws IOException { Optional.of(ConsoleApiParamsUtils.createFake(authResult)), Optional.of("PUT"), Optional.of( - new UserData("test2@test.com", RegistrarRole.ACCOUNT_MANAGER.toString(), null))); + new UserData( + "test2@test.com", null, RegistrarRole.ACCOUNT_MANAGER.toString(), null))); action.cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils(); action.run(); assertThat(response.getStatus()).isEqualTo(SC_OK); @@ -374,7 +380,8 @@ void testFailure_noPermissionToUpdateUser() throws IOException { Optional.of(ConsoleApiParamsUtils.createFake(authResult)), Optional.of("PUT"), Optional.of( - new UserData("test3@test.com", RegistrarRole.ACCOUNT_MANAGER.toString(), null))); + new UserData( + "test3@test.com", null, RegistrarRole.ACCOUNT_MANAGER.toString(), null))); action.run(); assertThat(response.getStatus()).isEqualTo(SC_FORBIDDEN); assertThat(response.getPayload())