From 9f191e9392f8b6355f1475b41484c3db182c6261 Mon Sep 17 00:00:00 2001 From: gbrodman Date: Mon, 28 Jul 2025 16:23:39 -0400 Subject: [PATCH] Add Registry Lock password reset on front end (#2785) 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 1998881b7..2d17eb981 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 d670a546d..acd88fa7f 100644 --- a/console-webapp/src/app/users/userDetails.component.html +++ b/console-webapp/src/app/users/userDetails.component.html @@ -80,7 +80,15 @@ 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 5ce1026a9..4993faf0b 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 4a77fce55..0b1aab4b3 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 e69de29bb..e35847646 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 04448f8db..d22940b92 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 ed5c9ad48..c65f194cc 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 4aba7ef2d..67467a421 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 class User extends UpdateAutoTimestampEntity implements Buildable { } @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 class User extends UpdateAutoTimestampEntity implements Buildable { 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 dcfd8904d..6821365a6 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 static google.registry.util.CollectionUtils.forceEmptyToNull; 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 class FeeCheckResponseExtensionItemCommandV12 extends ImmutableObject { } /** 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 f61156885..520b3cf3e 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 class Registrar extends UpdateAutoTimestampEntity implements Buildable, J } @Override - public Builder asBuilder() { - return new Builder<>(clone(this)); + public Builder asBuilder() { + return new Builder(clone(this)); } @Override @@ -706,59 +706,58 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J } /** 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 class Registrar extends UpdateAutoTimestampEntity implements Buildable, J * {@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 class Registrar extends UpdateAutoTimestampEntity implements Buildable, J 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 class Registrar extends UpdateAutoTimestampEntity implements Buildable, J 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 class Registrar extends UpdateAutoTimestampEntity implements Buildable, J 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 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J 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 class Registrar extends UpdateAutoTimestampEntity implements Buildable, J byte[] salt = SALT_SUPPLIER.get(); getInstance().salt = base64().encode(salt); getInstance().passwordHash = hashPassword(password, salt); - return thisCastToDerived(); + return this; } /** @@ -939,18 +938,18 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J * * @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 class Registrar extends UpdateAutoTimestampEntity implements Buildable, J * 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 e942c82a3..e5f8eea25 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.ImmutableList; 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 class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe 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 class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe } /** 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 class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe 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 class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe 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 8ae98c09d..6246e87fc 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 @@ public class ConsoleUsersAction extends ConsoleApiAction { u -> new UserData( u.getEmailAddress(), + u.getRegistryLockEmailAddress().orElse(null), u.getUserRoles().getRegistrarRoles().get(registrarId).toString(), null)) .collect(Collectors.toList()); @@ -237,7 +238,9 @@ public class ConsoleUsersAction extends ConsoleApiAction { .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 @@ public class ConsoleUsersAction extends ConsoleApiAction { } 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 517d8bf59..ff2a40719 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 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase { 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 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase { 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 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase { 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 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase { 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 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase { 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 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase { 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 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase { 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 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase { 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 @@ class ConsoleUsersActionTest extends ConsoleActionBaseTestCase { 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())