Compare commits

...
Author SHA1 Message Date
Nilay ShahandGitHub 06299ccb86 Add cache for User entities in OIDC auth flow (#2822)
* Add cache for User entities in OIDC auth flow

* refactor: Address review feedback

- Refactor database call into a single, reusable method
- Increase the default cache size to 200
- Remove .recordStats() and using spy for testing
- Split unit tests into separate implementation test that use Mockito spies instead of checking internal cache stats
2025-09-12 07:43:32 +00:00
gbrodmanandGitHub 732c30b359 Remove registry-lock-related fields from RegistrarPoc (#2818)
We've moved these over to the User class, so we should remove these for
clarity. In addition, we should make it clear (in Java at least) that
the field in the RegistryLock object refers to the email address used
for the lock in question.
2025-09-11 15:29:06 +00:00
gbrodmanandGitHub ee5a2d3916 Include internal registrars in the console (#2821)
This allows us to also check / modify the CharlestonRoad registrar in
the console, and also allows us to test actions (like password reset)
using that registrar in the prod environment.
2025-09-05 20:37:23 +00:00
gbrodmanandGitHub 2b5643df4c Sort registrars list in console (#2820)
This was bugging me slightly
2025-09-05 18:44:17 +00:00
Pavlo TkachandGitHub 6bbd7a2290 Update proxy resources, increase ssl handshake timeout (#2819) 2025-09-05 18:09:55 +00:00
Weimin YuandGitHub 77ab80f3dc Fix OOM in UploadBsaUnavailableDomains action (#2817)
* Fix OOM in UploadBsaUnavailableDomains action

The action was using string concatenation to generate the upload content.
This causes an OOM when string length exceeds 25MB on our current VM.

This PR witches to streaming upload.

Also added an HTTP upload test.

* Fix OOM in UploadBsaUnavailableDomains action

The action was using string concatenation to generate the upload content.
This causes an OOM when string length exceeds 25MB on our current VM.

This PR witches to streaming upload.

Also added an HTTP upload test.
2025-09-03 18:25:56 +00:00
Pavlo TkachandGitHub 5e1cd0120f Adjust proxy resource allocation and update nomulus compute class (#2814) 2025-08-28 18:49:16 +00:00
Weimin YuandGitHub 0167dad85f Fix OOM error in BsaValidation (#2813)
Error happened in the case that an unblockable name reported with
'Registered' as reason has been deregistered. We tried to check the
deletion time of the domain to decide if this is a transient error
that is no worth reporting. However, we forgot that we do not have
the domain key in this case.

As best-effort action, and with a case that rarely happens, we decide
not to make the optimization (staleness check) in thise case.
2025-08-27 15:47:13 +00:00
45 changed files with 1339 additions and 5937 deletions
@@ -25,7 +25,10 @@ export class RegistrarSelectorComponent {
registrarInput = signal<string>(this.registrarService.registrarId());
filteredOptions?: string[];
allRegistrarIds = computed(() =>
this.registrarService.registrars().map((r) => r.registrarId)
this.registrarService
.registrars()
.map((r) => r.registrarId)
.sort()
);
constructor(protected registrarService: RegistrarService) {
+2
View File
@@ -58,6 +58,8 @@ def fragileTestPatterns = [
// Changes cache timeouts and for some reason appears to have contention
// with other tests.
"google/registry/whois/WhoisCommandFactoryTest.*",
// Breaks random other tests when running with standardTests.
"google/registry/bsa/UploadBsaUnavailableDomainsActionTest.*",
// Currently changes a global configuration parameter that for some reason
// results in timestamp inversions for other tests. TODO(mmuller): fix.
"google/registry/flows/host/HostInfoFlowTest.*",
@@ -15,7 +15,6 @@
package google.registry.batch;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.POST;
import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STATUSES;
@@ -30,8 +29,6 @@ import google.registry.groups.GmailClient;
import google.registry.model.domain.Domain;
import google.registry.model.domain.RegistryLock;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.tld.RegistryLockDao;
import google.registry.persistence.VKey;
import google.registry.request.Action;
@@ -70,12 +67,14 @@ public class RelockDomainAction implements Runnable {
"""
The domain %s was successfully re-locked.
Please contact support at %s if you have any questions.""";
Please contact support at %s if you have any questions.\
""";
private static final String RELOCK_NON_RETRYABLE_FAILURE_EMAIL_TEMPLATE =
"""
There was an error when automatically re-locking %s. Error message: %s
Please contact support at %s if you have any questions.""";
Please contact support at %s if you have any questions.\
""";
private static final String RELOCK_TRANSIENT_FAILURE_EMAIL_TEMPLATE =
"There was an unexpected error when automatically re-locking %s. We will continue retrying "
+ "the lock for five hours. Please contact support at %s if you have any questions";
@@ -171,7 +170,7 @@ public class RelockDomainAction implements Runnable {
domainLockUtils.administrativelyApplyLock(
oldLock.getDomainName(),
oldLock.getRegistrarId(),
oldLock.getRegistrarPocId(),
oldLock.getRegistryLockEmail(),
oldLock.isSuperuser());
logger.atInfo().log("Re-locked domain %s.", oldLock.getDomainName());
response.setStatus(SC_OK);
@@ -221,7 +220,7 @@ public class RelockDomainAction implements Runnable {
EmailMessage.newBuilder()
.setBody(body)
.setSubject(String.format("Error re-locking domain %s", oldLock.getDomainName()))
.setRecipients(getEmailRecipients(oldLock.getRegistrarId()))
.setRecipients(ImmutableSet.of(getEmailRecipient(oldLock)))
.build());
}
@@ -250,7 +249,7 @@ public class RelockDomainAction implements Runnable {
EmailMessage.newBuilder()
.setBody(body)
.setSubject(String.format("Successful re-lock of domain %s", oldLock.getDomainName()))
.setRecipients(getEmailRecipients(oldLock.getRegistrarId()))
.setRecipients(ImmutableSet.of(getEmailRecipient(oldLock)))
.build());
}
@@ -261,7 +260,7 @@ public class RelockDomainAction implements Runnable {
// For an unexpected failure, notify both the lock-enabled contacts and our alerting email
ImmutableSet<InternetAddress> allRecipients =
new ImmutableSet.Builder<InternetAddress>()
.addAll(getEmailRecipients(oldLock.getRegistrarId()))
.add(getEmailRecipient(oldLock))
.add(alertRecipientAddress)
.build();
gmailClient.sendEmail(
@@ -281,31 +280,12 @@ public class RelockDomainAction implements Runnable {
.build());
}
private ImmutableSet<InternetAddress> getEmailRecipients(String registrarId) {
Registrar registrar =
Registrar.loadByRegistrarIdCached(registrarId)
.orElseThrow(
() ->
new IllegalStateException(String.format("Unknown registrar %s", registrarId)));
ImmutableSet<String> registryLockEmailAddresses =
registrar.getContacts().stream()
.filter(RegistrarPoc::isRegistryLockAllowed)
.map(RegistrarPoc::getRegistryLockEmailAddress)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(toImmutableSet());
ImmutableSet.Builder<InternetAddress> builder = new ImmutableSet.Builder<>();
// can't use streams due to the 'throws' in the InternetAddress constructor
for (String registryLockEmailAddress : registryLockEmailAddresses) {
try {
builder.add(new InternetAddress(registryLockEmailAddress));
} catch (AddressException e) {
// This shouldn't stop any other emails going out, so swallow it
logger.atWarning().log("Invalid email address '%s'.", registryLockEmailAddress);
}
private InternetAddress getEmailRecipient(RegistryLock lock) {
try {
return new InternetAddress(lock.getRegistryLockEmail());
} catch (AddressException e) {
// this really shouldn't happen
throw new RuntimeException(e);
}
return builder.build();
}
}
@@ -215,10 +215,12 @@ public class BsaValidateAction implements Runnable {
if (Objects.equals(expectedReason, domain.reason())) {
return Optional.empty();
}
if (isRegistered || domain.reason().equals(Reason.REGISTERED)) {
if (isStalenessAllowed(isRegistered, activeDomains.get(domain.domainName()))) {
// Registered name still reported with other reasons: Don't report if registration is recent.
// Note that staleness is not tolerated if deregistered name is still reported as registered:
// in this case we do not have the VKey on hand, and it is not worth the effort to find it
// out.
if (isRegistered && isStalenessAllowed(activeDomains.get(domain.domainName()))) {
return Optional.empty();
}
}
return Optional.of(
String.format(
@@ -228,15 +230,10 @@ public class BsaValidateAction implements Runnable {
domain.reason()));
}
boolean isStalenessAllowed(boolean isNewDomain, VKey<Domain> domainVKey) {
boolean isStalenessAllowed(VKey<Domain> domainVKey) {
Domain domain = bsaQuery(() -> replicaTm().loadByKey(domainVKey));
var now = clock.nowUtc();
if (isNewDomain) {
return domain.getCreationTime().plus(maxStaleness).isAfter(now);
} else {
return domain.getDeletionTime().isBefore(now)
&& domain.getDeletionTime().plus(maxStaleness).isAfter(now);
}
return domain.getCreationTime().plus(maxStaleness).isAfter(now);
}
/** Returns unique labels across all block lists in the download specified by {@code jobName}. */
@@ -25,16 +25,16 @@ import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.POST;
import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.cloud.storage.BlobId;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Ordering;
import com.google.common.flogger.FluentLogger;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import com.google.common.io.ByteSource;
import google.registry.bsa.api.BsaCredential;
import google.registry.config.RegistryConfig.Config;
import google.registry.gcs.GcsUtils;
@@ -47,10 +47,13 @@ import google.registry.request.auth.Auth;
import google.registry.util.Clock;
import jakarta.inject.Inject;
import jakarta.persistence.TypedQuery;
import java.io.ByteArrayOutputStream;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.Writer;
import java.util.Optional;
import java.util.zip.GZIPOutputStream;
@@ -60,14 +63,17 @@ import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okio.BufferedSink;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.joda.time.DateTime;
/**
* Daily action that uploads unavailable domain names on applicable TLDs to BSA.
*
* <p>The upload is a single zipped text file containing combined details for all BSA-enrolled TLDs.
* The text is a newline-delimited list of punycoded fully qualified domain names, and contains all
* domains on each TLD that are registered and/or reserved.
* The text is a newline-delimited list of punycoded fully qualified domain names with a trailing
* newline at the end, and contains all domains on each TLD that are registered and/or reserved.
*
* <p>The file is also uploaded to GCS to preserve it as a record for ourselves.
*/
@@ -118,7 +124,7 @@ public class UploadBsaUnavailableDomainsAction implements Runnable {
// TODO(mcilwain): Implement a date Cursor, have the cronjob run frequently, and short-circuit
// the run if the daily upload is already completed.
DateTime runTime = clock.nowUtc();
String unavailableDomains = Joiner.on("\n").join(getUnavailableDomains(runTime));
ImmutableSortedSet<String> unavailableDomains = getUnavailableDomains(runTime);
if (unavailableDomains.isEmpty()) {
logger.atWarning().log("No unavailable domains found; terminating.");
emailSender.sendNotification(
@@ -136,12 +142,16 @@ public class UploadBsaUnavailableDomainsAction implements Runnable {
}
/** Uploads the unavailable domains list to GCS in the unavailable domains bucket. */
boolean uploadToGcs(String unavailableDomains, DateTime runTime) {
boolean uploadToGcs(ImmutableSortedSet<String> unavailableDomains, DateTime runTime) {
logger.atInfo().log("Uploading unavailable names file to GCS in bucket %s", gcsBucket);
BlobId blobId = BlobId.of(gcsBucket, createFilename(runTime));
// `gcsUtils.openOutputStream` returns a buffered stream
try (OutputStream gcsOutput = gcsUtils.openOutputStream(blobId);
Writer osWriter = new OutputStreamWriter(gcsOutput, US_ASCII)) {
osWriter.write(unavailableDomains);
for (var domainName : unavailableDomains) {
osWriter.write(domainName);
osWriter.write("\n");
}
return true;
} catch (Exception e) {
logger.atSevere().withCause(e).log(
@@ -150,10 +160,14 @@ public class UploadBsaUnavailableDomainsAction implements Runnable {
}
}
boolean uploadToBsa(String unavailableDomains, DateTime runTime) {
boolean uploadToBsa(ImmutableSortedSet<String> unavailableDomains, DateTime runTime) {
try {
byte[] gzippedContents = gzipUnavailableDomains(unavailableDomains);
String sha512Hash = ByteSource.wrap(gzippedContents).hash(Hashing.sha512()).toString();
Hasher sha512Hasher = Hashing.sha512().newHasher();
unavailableDomains.stream()
.map(name -> name + "\n")
.forEachOrdered(line -> sha512Hasher.putString(line, UTF_8));
String sha512Hash = sha512Hasher.hash().toString();
String filename = createFilename(runTime);
OkHttpClient client = new OkHttpClient().newBuilder().build();
@@ -169,7 +183,9 @@ public class UploadBsaUnavailableDomainsAction implements Runnable {
.addFormDataPart(
"file",
String.format("%s.gz", filename),
RequestBody.create(gzippedContents, MediaType.parse("application/octet-stream")))
new StreamingRequestBody(
gzippedStream(unavailableDomains),
MediaType.parse("application/octet-stream")))
.build();
Request request =
@@ -196,15 +212,6 @@ public class UploadBsaUnavailableDomainsAction implements Runnable {
}
}
private byte[] gzipUnavailableDomains(String unavailableDomains) throws IOException {
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
try (GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream)) {
gzipOutputStream.write(unavailableDomains.getBytes(US_ASCII));
}
return byteArrayOutputStream.toByteArray();
}
}
private static String createFilename(DateTime runTime) {
return String.format("unavailable_domains_%s.txt", runTime.toString());
}
@@ -280,4 +287,65 @@ public class UploadBsaUnavailableDomainsAction implements Runnable {
private static String toDomain(String domainLabel, Tld tld) {
return String.format("%s.%s", domainLabel, tld.getTldStr());
}
private InputStream gzippedStream(ImmutableSortedSet<String> unavailableDomains)
throws IOException {
PipedInputStream inputStream = new PipedInputStream();
PipedOutputStream outputStream = new PipedOutputStream(inputStream);
new Thread(
() -> {
try {
gzipUnavailableDomains(outputStream, unavailableDomains);
} catch (Throwable e) {
logger.atSevere().withCause(e).log("Failed to gzip unavailable domains.");
try {
// This will cause the next read to throw an IOException.
inputStream.close();
} catch (IOException ignore) {
// Won't happen for `PipedInputStream.close()`
}
}
})
.start();
return inputStream;
}
private void gzipUnavailableDomains(
PipedOutputStream outputStream, ImmutableSortedSet<String> unavailableDomains)
throws IOException {
// `GZIPOutputStream` is buffered.
try (GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream)) {
for (String name : unavailableDomains) {
var line = name + "\n";
gzipOutputStream.write(line.getBytes(US_ASCII));
}
}
}
private static class StreamingRequestBody extends RequestBody {
private final BufferedInputStream inputStream;
private final MediaType mediaType;
StreamingRequestBody(InputStream inputStream, MediaType mediaType) {
this.inputStream = new BufferedInputStream(inputStream);
this.mediaType = mediaType;
}
@Nullable
@Override
public MediaType contentType() {
return mediaType;
}
@Override
public void writeTo(@NotNull BufferedSink bufferedSink) throws IOException {
byte[] buffer = new byte[2048];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
bufferedSink.write(buffer, 0, bytesRead);
}
}
}
}
@@ -1591,6 +1591,26 @@ public final class RegistryConfig {
return CONFIG_SETTINGS.get().caching.eppResourceMaxCachedEntries;
}
/** Returns if we have enabled caching for User Authentication */
public static boolean getUserAuthCachingEnabled() {
return CONFIG_SETTINGS.get().caching.userAuthCachingEnabled;
}
@VisibleForTesting
public static void overrideIsUserAuthCachingEnabledForTesting(boolean enabled) {
CONFIG_SETTINGS.get().caching.userAuthCachingEnabled = enabled;
}
/** Returns the expiry duration for the user authentication cache. */
public static java.time.Duration getUserAuthCachingDuration() {
return java.time.Duration.ofSeconds(CONFIG_SETTINGS.get().caching.userAuthCachingSeconds);
}
/** Returns the maximum number of entries in user authentication cache. */
public static int getUserAuthMaxCachedEntries() {
return CONFIG_SETTINGS.get().caching.userAuthMaxCachedEntries;
}
/** Returns the amount of time that a particular claims list should be cached. */
public static java.time.Duration getClaimsListCacheDuration() {
return java.time.Duration.ofSeconds(CONFIG_SETTINGS.get().caching.claimsListCachingSeconds);
@@ -161,6 +161,9 @@ public class RegistryConfigSettings {
public int eppResourceCachingSeconds;
public int eppResourceMaxCachedEntries;
public int claimsListCachingSeconds;
public boolean userAuthCachingEnabled;
public int userAuthCachingSeconds;
public int userAuthMaxCachedEntries;
}
/** Configuration for ICANN monthly reporting. */
@@ -326,6 +326,20 @@ caching:
# long duration is acceptable because claims lists don't change frequently.
claimsListCachingSeconds: 21600 # six hours
#-- User Authentication Cache Settings --#
# Whether to cache User objects during OIDC token authentication to reduce database load.
# This helps mitigate high QPS from frequent hello commands and session-less requests.
userAuthCachingEnabled: true
# The duration in seconds for which a User object is cached after being loaded.
# A short duration is recommended to avoid stale data.
userAuthCachingSeconds: 60
# The maximum number of User objects to store in the cache per pod.
# This helps limit the memory footprint of the cache.
userAuthMaxCachedEntries: 200
# Note: Only allowedServiceAccountEmails and oauthClientId should be configured.
# Other fields are related to OAuth-based authentication and will be removed.
auth:
@@ -101,8 +101,15 @@ public final class RegistryLock extends UpdateAutoTimestampEntity implements Bui
@Column(nullable = false)
private String registrarId;
/** The POC that performed the action, or null if it was a superuser. */
@Expose private String registrarPocId;
/**
* The email address of the user that performed the action, or null if it was a superuser.
*
* <p>Note: this is misnamed in the database due to historical reasons, where we used the
* registrar POC ID as the email address rather than a separate specialized field.
*/
@Column(name = "registrarPocId")
@Expose
private String registryLockEmail;
/** When the lock is first requested. */
@AttributeOverrides({
@@ -161,8 +168,8 @@ public final class RegistryLock extends UpdateAutoTimestampEntity implements Bui
return registrarId;
}
public String getRegistrarPocId() {
return registrarPocId;
public String getRegistryLockEmail() {
return registryLockEmail;
}
public DateTime getLockRequestTime() {
@@ -255,7 +262,7 @@ public final class RegistryLock extends UpdateAutoTimestampEntity implements Bui
checkArgumentNotNull(getInstance().registrarId, "Registrar ID cannot be null");
checkArgumentNotNull(getInstance().verificationCode, "Verification code cannot be null");
checkArgument(
getInstance().registrarPocId != null || getInstance().isSuperuser,
getInstance().registryLockEmail != null || getInstance().isSuperuser,
"Registrar POC ID must be provided if superuser is false");
return super.build();
}
@@ -275,8 +282,8 @@ public final class RegistryLock extends UpdateAutoTimestampEntity implements Bui
return this;
}
public Builder setRegistrarPocId(String registrarPocId) {
getInstance().registrarPocId = registrarPocId;
public Builder setRegistryLockEmail(String registryLockEmail) {
getInstance().registryLockEmail = registryLockEmail;
return this;
}
@@ -14,16 +14,11 @@
package google.registry.model.registrar;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.io.BaseEncoding.base64;
import static google.registry.model.registrar.Registrar.checkValidEmail;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableSortedCopy;
import static google.registry.util.PasswordUtils.SALT_SUPPLIER;
import static google.registry.util.PasswordUtils.hashPassword;
import static java.util.stream.Collectors.joining;
import com.google.common.annotations.VisibleForTesting;
@@ -38,7 +33,6 @@ import google.registry.model.Jsonifiable;
import google.registry.model.UnsafeSerializable;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.QueryComposer;
import google.registry.util.PasswordUtils;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
@@ -47,9 +41,7 @@ import jakarta.persistence.Id;
import jakarta.persistence.IdClass;
import java.io.Serializable;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nullable;
/**
* A contact for a Registrar. Note, equality, hashCode and comparable have been overridden to only
@@ -107,9 +99,6 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
@Id @Expose public String registrarId;
/** External email address of this contact used for registry lock confirmations. */
String registryLockEmailAddress;
/** The voice number of the contact. */
@Expose String phoneNumber;
@@ -147,22 +136,10 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
@Expose
boolean visibleInDomainWhoisAsAbuse = false;
/**
* Whether the contact is allowed to set their registry lock password through the registrar
* console. This will be set to false on contact creation and when the user sets a password.
*/
/** Legacy field, around until we can remove the non-null constraint and the column from SQL. */
@Column(nullable = false)
boolean allowedToSetRegistryLockPassword = false;
/**
* A hashed password that exists iff this contact is registry-lock-enabled. The hash is a base64
* encoded SHA256 string.
*/
String registryLockPasswordHash;
/** Randomly generated hash salt. */
String registryLockPasswordSalt;
/**
* Helper to update the contacts associated with a Registrar. This requires querying for the
* existing contacts, deleting existing contacts that are not part of the given {@code contacts}
@@ -197,10 +174,6 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
return emailAddress;
}
public Optional<String> getRegistryLockEmailAddress() {
return Optional.ofNullable(registryLockEmailAddress);
}
public String getPhoneNumber() {
return phoneNumber;
}
@@ -229,24 +202,6 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
return new Builder(clone(this));
}
public boolean isAllowedToSetRegistryLockPassword() {
return allowedToSetRegistryLockPassword;
}
public boolean isRegistryLockAllowed() {
return !isNullOrEmpty(registryLockPasswordHash) && !isNullOrEmpty(registryLockPasswordSalt);
}
public boolean verifyRegistryLockPassword(String registryLockPassword) {
if (isNullOrEmpty(registryLockPassword)
|| isNullOrEmpty(registryLockPasswordSalt)
|| isNullOrEmpty(registryLockPasswordHash)) {
return false;
}
return PasswordUtils.verifyPassword(
registryLockPassword, registryLockPasswordHash, registryLockPasswordSalt);
}
/**
* Returns a string representation that's human friendly.
*
@@ -296,15 +251,12 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
return new JsonMapBuilder()
.put("name", name)
.put("emailAddress", emailAddress)
.put("registryLockEmailAddress", registryLockEmailAddress)
.put("phoneNumber", phoneNumber)
.put("faxNumber", faxNumber)
.put("types", getTypes().stream().map(Object::toString).collect(joining(",")))
.put("visibleInWhoisAsAdmin", visibleInWhoisAsAdmin)
.put("visibleInWhoisAsTech", visibleInWhoisAsTech)
.put("visibleInDomainWhoisAsAbuse", visibleInDomainWhoisAsAbuse)
.put("allowedToSetRegistryLockPassword", allowedToSetRegistryLockPassword)
.put("registryLockAllowed", isRegistryLockAllowed())
.put("id", getId())
.build();
}
@@ -344,14 +296,6 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
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
// a registry lock password, we must also set up the correct registry lock email concurrently
// or beforehand.
if (getInstance().allowedToSetRegistryLockPassword) {
checkArgument(
!isNullOrEmpty(getInstance().registryLockEmailAddress),
"Registry lock email must not be null if allowing registry lock access");
}
return cloneEmptyToNull(super.build());
}
@@ -365,11 +309,6 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
return this;
}
public Builder setRegistryLockEmailAddress(@Nullable String registryLockEmailAddress) {
getInstance().registryLockEmailAddress = registryLockEmailAddress;
return this;
}
public Builder setPhoneNumber(String phoneNumber) {
getInstance().phoneNumber = phoneNumber;
return this;
@@ -409,28 +348,6 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
getInstance().visibleInDomainWhoisAsAbuse = visible;
return this;
}
public Builder setAllowedToSetRegistryLockPassword(boolean allowedToSetRegistryLockPassword) {
if (allowedToSetRegistryLockPassword) {
getInstance().registryLockPasswordSalt = null;
getInstance().registryLockPasswordHash = null;
}
getInstance().allowedToSetRegistryLockPassword = allowedToSetRegistryLockPassword;
return this;
}
public Builder setRegistryLockPassword(String registryLockPassword) {
checkArgument(
getInstance().allowedToSetRegistryLockPassword,
"Not allowed to set registry lock password for this contact");
checkArgument(
!isNullOrEmpty(registryLockPassword), "Registry lock password was null or empty");
byte[] salt = SALT_SUPPLIER.get();
getInstance().registryLockPasswordSalt = base64().encode(salt);
getInstance().registryLockPasswordHash = hashPassword(registryLockPassword, salt);
getInstance().allowedToSetRegistryLockPassword = false;
return this;
}
}
public static ImmutableList<RegistrarPoc> loadForRegistrar(String registrarId) {
@@ -15,14 +15,20 @@
package google.registry.request.auth;
import static com.google.common.base.Preconditions.checkState;
import static google.registry.config.RegistryConfig.getUserAuthCachingDuration;
import static google.registry.config.RegistryConfig.getUserAuthCachingEnabled;
import static google.registry.config.RegistryConfig.getUserAuthMaxCachedEntries;
import static google.registry.model.CacheUtils.newCacheBuilder;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.api.client.json.webtoken.JsonWebSignature;
import com.google.auth.oauth2.TokenVerifier.VerificationException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.console.User;
import google.registry.persistence.VKey;
@@ -71,6 +77,40 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
this.tokenVerifier = tokenVerifier;
}
/**
* An in-memory cache for User entities, built using the project's standard utility.
*
* <p>This cache reduces database load by temporarily storing User objects after they are fetched.
* It is configured to cache negative results (i.e., when a user is not found) to prevent repeated
* lookups for invalid users. The cache's behavior (enabled, expiry, size) is controlled by
* settings in {@link RegistryConfig}.
*/
@VisibleForTesting
static LoadingCache<String, Optional<User>> userCache =
newCacheBuilder(getUserAuthCachingDuration())
.maximumSize(getUserAuthMaxCachedEntries())
.build(OidcTokenAuthenticationMechanism::loadUser);
/**
* A loader function that defines how to fetch a User from the database on a cache miss.
*
* <p>This is the single point of entry to the database for this authentication flow. It will only
* be invoked by the cache when a requested user is not already in memory.
*/
@VisibleForTesting
static Optional<User> loadUser(String email) {
VKey<User> userVKey = VKey.create(User.class, email);
return tm().transact(() -> tm().loadByKeyIfPresent(userVKey));
}
@VisibleForTesting
public static void setCacheForTesting(LoadingCache<String, Optional<User>> cache) {
checkState(
RegistryEnvironment.get() == RegistryEnvironment.UNITTEST,
"Cannot set cache outside of a test environment");
OidcTokenAuthenticationMechanism.userCache = cache;
}
@Override
public AuthResult authenticate(HttpServletRequest request) {
if (RegistryEnvironment.get().equals(RegistryEnvironment.UNITTEST)
@@ -112,8 +152,15 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
logger.atInfo().log("No email address from the OIDC token:\n%s", token.getPayload());
return AuthResult.NOT_AUTHENTICATED;
}
Optional<User> maybeUser =
tm().transact(() -> tm().loadByKeyIfPresent(VKey.create(User.class, email)));
Optional<User> maybeUser;
if (getUserAuthCachingEnabled()) {
// If caching is ON, use the cache.
maybeUser = userCache.get(email);
} else {
// If caching is OFF, fall back to the original direct database call.
maybeUser = loadUser(email);
}
stopwatch.tick("OidcTokenAuthenticationMechanism maybeUser loaded");
if (maybeUser.isPresent()) {
return AuthResult.createUser(maybeUser.get());
@@ -74,11 +74,12 @@ public final class DomainLockUtils {
* <p>The lock will not be applied until {@link #verifyVerificationCode} is called.
*/
public RegistryLock saveNewRegistryLockRequest(
String domainName, String registrarId, @Nullable String registrarPocId, boolean isAdmin) {
String domainName, String registrarId, @Nullable String registryLockEmail, boolean isAdmin) {
return tm().transact(
() ->
RegistryLockDao.save(
createLockBuilder(domainName, registrarId, registrarPocId, isAdmin).build()));
createLockBuilder(domainName, registrarId, registryLockEmail, isAdmin)
.build()));
}
/**
@@ -129,13 +130,13 @@ public final class DomainLockUtils {
* the case of relocks, isAdmin is determined by the previous lock.
*/
public RegistryLock administrativelyApplyLock(
String domainName, String registrarId, @Nullable String registrarPocId, boolean isAdmin) {
String domainName, String registrarId, @Nullable String registryLockEmail, boolean isAdmin) {
return tm().transact(
() -> {
DateTime now = tm().getTransactionTime();
RegistryLock newLock =
RegistryLockDao.save(
createLockBuilder(domainName, registrarId, registrarPocId, isAdmin)
createLockBuilder(domainName, registrarId, registryLockEmail, isAdmin)
.setLockCompletionTime(now)
.build());
applyLockStatuses(newLock, now, isAdmin);
@@ -235,7 +236,7 @@ public final class DomainLockUtils {
}
private RegistryLock.Builder createLockBuilder(
String domainName, String registrarId, @Nullable String registrarPocId, boolean isAdmin) {
String domainName, String registrarId, @Nullable String registryLockEmail, boolean isAdmin) {
DateTime now = tm().getTransactionTime();
Domain domain = getDomain(domainName, registrarId, now);
verifyDomainNotLocked(domain, isAdmin);
@@ -255,7 +256,7 @@ public final class DomainLockUtils {
.setDomainName(domainName)
.setRepoId(domain.getRepoId())
.setRegistrarId(registrarId)
.setRegistrarPocId(registrarPocId)
.setRegistryLockEmail(registryLockEmail)
.isSuperuser(isAdmin);
}
@@ -85,12 +85,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 = "--registry_lock_email",
description = "Email address used for registry lock confirmation emails")
String registryLockEmail;
@Nullable
@Parameter(
names = "--phone",
@@ -132,15 +126,6 @@ final class RegistrarPocCommand extends MutatingCommand {
arity = 1)
private Boolean visibleInDomainWhoisAsAbuse;
@Nullable
@Parameter(
names = "--allowed_to_set_registry_lock_password",
description =
"Allow this contact to set their registry lock password in the console,"
+ " enabling registry lock",
arity = 1)
private Boolean allowedToSetRegistryLockPassword;
@Parameter(
names = {"-o", "--output"},
description = "Output file when --mode=LIST",
@@ -235,9 +220,6 @@ final class RegistrarPocCommand extends MutatingCommand {
builder.setRegistrar(registrar);
builder.setName(name);
builder.setEmailAddress(email);
if (!isNullOrEmpty(registryLockEmail)) {
builder.setRegistryLockEmailAddress(registryLockEmail);
}
if (phone != null) {
builder.setPhoneNumber(phone.orElse(null));
}
@@ -255,9 +237,6 @@ final class RegistrarPocCommand extends MutatingCommand {
if (visibleInDomainWhoisAsAbuse != null) {
builder.setVisibleInDomainWhoisAsAbuse(visibleInDomainWhoisAsAbuse);
}
if (allowedToSetRegistryLockPassword != null) {
builder.setAllowedToSetRegistryLockPassword(allowedToSetRegistryLockPassword);
}
return builder.build();
}
@@ -269,9 +248,6 @@ final class RegistrarPocCommand extends MutatingCommand {
if (!isNullOrEmpty(name)) {
builder.setName(name);
}
if (!isNullOrEmpty(registryLockEmail)) {
builder.setRegistryLockEmailAddress(registryLockEmail);
}
if (phone != null) {
builder.setPhoneNumber(phone.orElse(null));
}
@@ -290,9 +266,6 @@ final class RegistrarPocCommand extends MutatingCommand {
if (visibleInDomainWhoisAsAbuse != null) {
builder.setVisibleInDomainWhoisAsAbuse(visibleInDomainWhoisAsAbuse);
}
if (allowedToSetRegistryLockPassword != null) {
builder.setAllowedToSetRegistryLockPassword(allowedToSetRegistryLockPassword);
}
return builder.build();
}
@@ -21,7 +21,6 @@ import static google.registry.util.DomainNameUtils.canonicalizeHostname;
import com.google.common.base.Ascii;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.net.InternetDomainName;
@@ -29,7 +28,6 @@ import com.google.re2j.Pattern;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarAddress;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.ui.forms.FormException;
import google.registry.ui.forms.FormField;
import google.registry.ui.forms.FormFieldException;
import google.registry.ui.forms.FormFields;
@@ -38,7 +36,6 @@ import google.registry.util.X509Utils;
import java.security.cert.CertificateParsingException;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nullable;
@@ -192,14 +189,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<Object, Boolean> CONTACT_ALLOWED_TO_SET_REGISTRY_LOCK_PASSWORD =
FormField.named("allowedToSetRegistryLockPassword", Object.class)
.transform(Boolean.class, b -> Boolean.valueOf(Objects.toString(b)))
.build();
public static final FormField<String, String> CONTACT_REGISTRY_LOCK_PASSWORD_FIELD =
FormFields.NAME.asBuilderNamed("registryLockPassword").build();
public static final FormField<String, Set<RegistrarPoc.Type>> CONTACT_TYPES =
FormField.named("types")
.uppercased()
@@ -369,8 +358,6 @@ public final class RegistrarFormFields {
private static void applyRegistrarContactArgs(RegistrarPoc.Builder builder, Map<String, ?> args) {
builder.setName(CONTACT_NAME_FIELD.extractUntyped(args).orElse(null));
builder.setEmailAddress(CONTACT_EMAIL_ADDRESS_FIELD.extractUntyped(args).orElse(null));
builder.setRegistryLockEmailAddress(
REGISTRY_LOCK_EMAIL_ADDRESS_FIELD.extractUntyped(args).orElse(null));
builder.setVisibleInWhoisAsAdmin(
CONTACT_VISIBLE_IN_WHOIS_AS_ADMIN_FIELD.extractUntyped(args).orElse(false));
builder.setVisibleInWhoisAsTech(
@@ -380,23 +367,5 @@ 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()));
// 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(
CONTACT_ALLOWED_TO_SET_REGISTRY_LOCK_PASSWORD.extractUntyped(args).orElse(false));
// Registry lock password does not need to be set every time
CONTACT_REGISTRY_LOCK_PASSWORD_FIELD
.extractUntyped(args)
.ifPresent(
password -> {
if (!Strings.isNullOrEmpty(password)) {
if (password.length() < 8) {
throw new FormException(
"Registry lock password must be at least 8 characters long");
}
builder.setRegistryLockPassword(password);
}
});
}
}
@@ -53,8 +53,10 @@ import java.util.Optional;
public class RegistrarsAction extends ConsoleApiAction {
private static final int PASSWORD_LENGTH = 16;
private static final int PASSCODE_LENGTH = 5;
private static final ImmutableList<Registrar.Type> allowedRegistrarTypes =
ImmutableList.of(Registrar.Type.REAL, Registrar.Type.OTE);
private static final ImmutableSet<Registrar.Type> TYPES_ALLOWED_FOR_USERS =
ImmutableSet.of(Registrar.Type.REAL, Registrar.Type.OTE);
private static final ImmutableSet<Registrar.Type> TYPES_ALLOWED_FOR_ADMINS =
ImmutableSet.of(Registrar.Type.INTERNAL, Registrar.Type.REAL, Registrar.Type.OTE);
private static final String SQL_TEMPLATE =
"""
SELECT * FROM "Registrar"
@@ -80,6 +82,8 @@ public class RegistrarsAction extends ConsoleApiAction {
@Override
protected void getHandler(User user) {
if (user.getUserRoles().hasGlobalPermission(ConsolePermission.VIEW_REGISTRARS)) {
ImmutableSet<Registrar.Type> allowedRegistrarTypes =
user.getUserRoles().isAdmin() ? TYPES_ALLOWED_FOR_ADMINS : TYPES_ALLOWED_FOR_USERS;
ImmutableList<Registrar> registrars =
Streams.stream(Registrar.loadAll())
.filter(r -> allowedRegistrarTypes.contains(r.getType()))
@@ -94,6 +98,7 @@ public class RegistrarsAction extends ConsoleApiAction {
.map(Map.Entry::getKey)
.collect(toImmutableSet());
@SuppressWarnings("unchecked")
List<Registrar> registrars =
tm().transact(
() ->
@@ -45,7 +45,6 @@ import google.registry.ui.server.console.ConsoleApiAction;
import google.registry.ui.server.console.ConsoleApiParams;
import jakarta.inject.Inject;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
@@ -246,7 +245,6 @@ public class ContactAction extends ConsoleApiAction {
throw new ContactRequirementException(
"An abuse contact visible in domain WHOIS query must be designated");
}
checkContactRegistryLockRequirements(existingContacts, updatedContacts);
}
private static void enforcePrimaryContactRestrictions(
@@ -266,69 +264,6 @@ public class ContactAction extends ConsoleApiAction {
}
}
private static void checkContactRegistryLockRequirements(
ImmutableSet<RegistrarPoc> existingContacts, ImmutableSet<RegistrarPoc> updatedContacts) {
// Any contact(s) with new passwords must be allowed to set them
for (RegistrarPoc updatedContact : updatedContacts) {
if (updatedContact.isRegistryLockAllowed()
|| updatedContact.isAllowedToSetRegistryLockPassword()) {
RegistrarPoc existingContact =
existingContacts.stream()
.filter(
contact -> contact.getEmailAddress().equals(updatedContact.getEmailAddress()))
.findFirst()
.orElseThrow(
() ->
new FormException(
"Cannot set registry lock password directly on new contact"));
// Can't modify registry lock email address
if (!Objects.equals(
updatedContact.getRegistryLockEmailAddress(),
existingContact.getRegistryLockEmailAddress())) {
throw new FormException("Cannot modify registryLockEmailAddress through the UI");
}
if (updatedContact.isRegistryLockAllowed()) {
// the password must have been set before or the user was allowed to set it now
if (!existingContact.isAllowedToSetRegistryLockPassword()
&& !existingContact.isRegistryLockAllowed()) {
throw new FormException("Registrar contact not allowed to set registry lock password");
}
}
if (updatedContact.isAllowedToSetRegistryLockPassword()) {
if (!existingContact.isAllowedToSetRegistryLockPassword()) {
throw new FormException(
"Cannot modify isAllowedToSetRegistryLockPassword through the UI");
}
}
}
}
// Any previously-existing contacts with registry lock enabled cannot be deleted
existingContacts.stream()
.filter(RegistrarPoc::isRegistryLockAllowed)
.forEach(
contact -> {
Optional<RegistrarPoc> updatedContactOptional =
updatedContacts.stream()
.filter(
updatedContact ->
updatedContact.getEmailAddress().equals(contact.getEmailAddress()))
.findFirst();
if (updatedContactOptional.isEmpty()) {
throw new FormException(
String.format(
"Cannot delete the contact %s that has registry lock enabled",
contact.getEmailAddress()));
}
if (!updatedContactOptional.get().isRegistryLockAllowed()) {
throw new FormException(
String.format(
"Cannot remove the ability to use registry lock on the contact %s",
contact.getEmailAddress()));
}
});
}
/**
* Retrieves the registrar contact whose phone number and email address is visible in domain WHOIS
* query as abuse contact (if any).
@@ -68,7 +68,7 @@ public class RelockDomainActionTest {
private static final String DOMAIN_NAME = "example.tld";
private static final String CLIENT_ID = "TheRegistrar";
private static final String POC_ID = "marla.singer@example.com";
private static final String LOCK_EMAIL_ADDRESS = "Marla.Singer.RegistryLock@crr.com";
private final FakeResponse response = new FakeResponse();
private final FakeClock clock = new FakeClock(DateTime.parse("2015-05-18T12:34:56Z"));
@@ -94,7 +94,9 @@ public class RelockDomainActionTest {
Host host = persistActiveHost("ns1.example.net");
domain = persistResource(DatabaseHelper.newDomain(DOMAIN_NAME, host));
oldLock = domainLockUtils.administrativelyApplyLock(DOMAIN_NAME, CLIENT_ID, POC_ID, false);
oldLock =
domainLockUtils.administrativelyApplyLock(
DOMAIN_NAME, CLIENT_ID, LOCK_EMAIL_ADDRESS, false);
assertThat(loadByEntity(domain).getStatusValues())
.containsAtLeastElementsIn(REGISTRY_LOCK_STATUSES);
oldLock =
@@ -255,9 +257,10 @@ public class RelockDomainActionTest {
.setSubject("Successful re-lock of domain example.tld")
.setBody(
"""
The domain example.tld was successfully re-locked.
The domain example.tld was successfully re-locked.
Please contact support at support@example.com if you have any questions.""")
Please contact support at support@example.com if you have any questions.\
""")
.setRecipients(
ImmutableSet.of(new InternetAddress("Marla.Singer.RegistryLock@crr.com")))
.build();
@@ -268,9 +271,10 @@ public class RelockDomainActionTest {
String expectedBody =
String.format(
"""
There was an error when automatically re-locking example.tld. Error message: %s
There was an error when automatically re-locking example.tld. Error message: %s
Please contact support at support@example.com if you have any questions.""",
Please contact support at support@example.com if you have any questions.\
""",
exceptionMessage);
assertFailureEmailWithBody(
expectedBody, ImmutableSet.of(new InternetAddress("Marla.Singer.RegistryLock@crr.com")));
@@ -24,7 +24,6 @@ import static google.registry.bsa.persistence.BsaTestingUtils.persistDownloadSch
import static google.registry.bsa.persistence.BsaTestingUtils.persistUnblockableDomain;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistDeletedDomain;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.mockito.ArgumentMatchers.any;
@@ -218,7 +217,8 @@ public class BsaValidateActionTest {
test1,1;2
test2,3
""";
String blockPlusContent = """
String blockPlusContent =
"""
domainLabel,orderIDs
test2,4
""";
@@ -239,7 +239,7 @@ public class BsaValidateActionTest {
persistBsaLabel("label");
Domain domain = persistActiveDomain("label.app", fakeClock.nowUtc());
fakeClock.advanceBy(MAX_STALENESS.minus(Duration.standardSeconds(1)));
assertThat(action.isStalenessAllowed(true, domain.createVKey())).isTrue();
assertThat(action.isStalenessAllowed(domain.createVKey())).isTrue();
}
@Test
@@ -247,23 +247,7 @@ public class BsaValidateActionTest {
persistBsaLabel("label");
Domain domain = persistActiveDomain("label.app", fakeClock.nowUtc());
fakeClock.advanceBy(MAX_STALENESS);
assertThat(action.isStalenessAllowed(true, domain.createVKey())).isFalse();
}
@Test
void isStalenessAllowed_deletedDomain_allowed() {
persistBsaLabel("label");
Domain domain = persistDeletedDomain("label.app", fakeClock.nowUtc());
fakeClock.advanceBy(MAX_STALENESS.minus(Duration.standardSeconds(1)));
assertThat(action.isStalenessAllowed(false, domain.createVKey())).isTrue();
}
@Test
void isStalenessAllowed_deletedDomain_notAllowed() {
persistBsaLabel("label");
Domain domain = persistDeletedDomain("label.app", fakeClock.nowUtc());
fakeClock.advanceBy(MAX_STALENESS);
assertThat(action.isStalenessAllowed(false, domain.createVKey())).isFalse();
assertThat(action.isStalenessAllowed(domain.createVKey())).isFalse();
}
@Test
@@ -405,10 +389,11 @@ public class BsaValidateActionTest {
assertThat(message.body())
.isEqualTo(
"""
Most recent download is 2023-11-09t020857.880z.
Most recent download is 2023-11-09t020857.880z.
Error line 1.
Error line 2""");
Error line 1.
Error line 2\
""");
}
@Test
@@ -20,13 +20,24 @@ import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistDeletedDomain;
import static google.registry.testing.DatabaseHelper.persistReservedList;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.LogsSubject.assertAboutLogs;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static google.registry.util.NetworkUtils.pickUnusedPort;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.Executors.newSingleThreadExecutor;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import com.google.cloud.storage.BlobId;
import com.google.cloud.storage.contrib.nio.testing.LocalStorageHelper;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.common.hash.Hashing;
import com.google.common.io.ByteStreams;
import com.google.common.net.HostAndPort;
import com.google.common.testing.TestLogHandler;
import com.google.gson.Gson;
import google.registry.bsa.api.BsaCredential;
import google.registry.gcs.GcsUtils;
import google.registry.model.tld.Tld;
@@ -35,9 +46,25 @@ import google.registry.model.tld.label.ReservedList;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.request.UrlConnectionService;
import google.registry.server.Route;
import google.registry.server.TestServer;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeResponse;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.MultipartConfig;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.Part;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.GZIPInputStream;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -102,13 +129,112 @@ public class UploadBsaUnavailableDomainsActionTest {
BlobId existingFile =
BlobId.of(BUCKET, String.format("unavailable_domains_%s.txt", clock.nowUtc()));
String blockList = new String(gcsUtils.readBytesFrom(existingFile), UTF_8);
assertThat(blockList).isEqualTo("ace.tld\nflagrant.tld\nfoobar.tld\njimmy.tld\ntine.tld");
assertThat(blockList).isEqualTo("ace.tld\nflagrant.tld\nfoobar.tld\njimmy.tld\ntine.tld\n");
assertThat(blockList).doesNotContain("not-blocked.tld");
// This test currently fails in the upload-to-bsa step.
verify(emailSender, times(1))
.sendNotification("BSA daily upload completed with errors", "Please see logs for details.");
}
// TODO(mcilwain): Add test of BSA API upload as well.
@Test
void uploadToBsaTest() throws Exception {
TestLogHandler logHandler = new TestLogHandler();
Logger loggerToIntercept =
Logger.getLogger(UploadBsaUnavailableDomainsAction.class.getCanonicalName());
loggerToIntercept.addHandler(logHandler);
persistActiveDomain("foobar.tld");
persistActiveDomain("ace.tld");
persistDeletedDomain("not-blocked.tld", clock.nowUtc().minusDays(1));
var testServer = startTestServer();
action.apiUrl = testServer.getUrl("/upload").toURI().toString();
try {
action.run();
} finally {
testServer.stop();
}
String dataSent = "ace.tld\nflagrant.tld\nfoobar.tld\njimmy.tld\ntine.tld\n";
String checkSum = Hashing.sha512().hashString(dataSent, UTF_8).toString();
String expectedResponse =
"Received response with code 200 from server: "
+ String.format("Checksum: [%s]\n%s\n", checkSum, dataSent);
assertAboutLogs().that(logHandler).hasLogAtLevelWithMessage(Level.INFO, expectedResponse);
verify(emailSender, times(1)).sendNotification("BSA daily upload completed successfully", "");
}
private TestServer startTestServer() throws Exception {
TestServer testServer =
new TestServer(
HostAndPort.fromParts(InetAddress.getLocalHost().getHostAddress(), pickUnusedPort()),
ImmutableMap.of(),
ImmutableList.of(Route.route("/upload", Servelet.class)));
testServer.start();
newSingleThreadExecutor()
.execute(
() -> {
try {
while (true) {
testServer.process();
}
} catch (InterruptedException e) {
// Expected
}
});
return testServer;
}
@MultipartConfig(
location = "", // Directory for storing uploaded files. Use default when blank
maxFileSize = 10485760L, // 10MB
maxRequestSize = 20971520L, // 20MB
fileSizeThreshold = 1048576 // Save in memory if file size < 1MB
)
public static class Servelet extends HttpServlet {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String checkSum = null;
String content = null;
try {
for (Part part : req.getParts()) {
switch (part.getName()) {
case "zone" -> checkSum = readChecksum(part);
case "file" -> content = readGzipped(part);
}
}
} catch (Exception e) {
logger.atInfo().withCause(e).log("");
}
int status = checkSum == null || content == null ? 400 : 200;
resp.setStatus(status);
resp.setContentType("text/plain");
try (PrintWriter writer = resp.getWriter()) {
writer.printf("Checksum: [%s]\n%s\n", checkSum, content);
}
}
private String readChecksum(Part part) {
try (InputStream is = part.getInputStream()) {
return new Gson()
.fromJson(new String(ByteStreams.toByteArray(is), UTF_8), Map.class)
.getOrDefault("checkSum", "Not found")
.toString();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private String readGzipped(Part part) {
try (InputStream is = part.getInputStream();
GZIPInputStream gis = new GZIPInputStream(is)) {
return new String(ByteStreams.toByteArray(gis), UTF_8);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
@@ -434,11 +434,8 @@ public abstract class JpaTransactionManagerExtension
.setRegistrar(makeRegistrar2())
.setName("Marla Singer")
.setEmailAddress("Marla.Singer@crr.com")
.setRegistryLockEmailAddress("Marla.Singer.RegistryLock@crr.com")
.setPhoneNumber("+1.2128675309")
.setTypes(ImmutableSet.of(RegistrarPoc.Type.TECH))
.setAllowedToSetRegistryLockPassword(true)
.setRegistryLockPassword("hi")
.build();
}
@@ -16,13 +16,20 @@ package google.registry.request.auth;
import static com.google.common.net.HttpHeaders.AUTHORIZATION;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.config.RegistryConfig.getUserAuthCachingDuration;
import static google.registry.config.RegistryConfig.getUserAuthMaxCachedEntries;
import static google.registry.request.auth.AuthModule.BEARER_PREFIX;
import static google.registry.request.auth.AuthModule.IAP_HEADER_NAME;
import static google.registry.testing.DatabaseHelper.createAdminUser;
import static google.registry.testing.DatabaseHelper.persistResource;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken.Payload;
import com.google.api.client.json.webtoken.JsonWebSignature;
import com.google.api.client.json.webtoken.JsonWebSignature.Header;
@@ -33,7 +40,9 @@ import dagger.Component;
import dagger.Module;
import dagger.Provides;
import google.registry.config.CredentialModule.ApplicationDefaultCredential;
import google.registry.config.RegistryConfig;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.CacheUtils;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
@@ -44,6 +53,7 @@ import google.registry.request.auth.OidcTokenAuthenticationMechanism.RegularOidc
import google.registry.util.GoogleCredentialsBundle;
import jakarta.inject.Singleton;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Optional;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -54,6 +64,9 @@ public class OidcTokenAuthenticationMechanismTest {
private static final String rawToken = "this-token";
private static final String email = "user@email.test";
private static final String unknownEmail = "bad-guy@evil.real";
private static final String gaiaId = "gaia-id";
private static final ImmutableSet<String> serviceAccounts =
ImmutableSet.of("service@email.test", "email@service.goog");
@@ -75,6 +88,12 @@ public class OidcTokenAuthenticationMechanismTest {
@BeforeEach
void beforeEach() throws Exception {
// 1. Create a brand new cache.
LoadingCache<String, Optional<User>> testCache =
CacheUtils.newCacheBuilder(getUserAuthCachingDuration())
.maximumSize(getUserAuthMaxCachedEntries())
.build(OidcTokenAuthenticationMechanism::loadUser);
OidcTokenAuthenticationMechanism.setCacheForTesting(testCache);
payload.setEmail(email);
payload.setSubject(gaiaId);
user = createAdminUser(email);
@@ -154,7 +173,7 @@ public class OidcTokenAuthenticationMechanismTest {
@Test
void testAuthenticate_unknownEmailAddress() throws Exception {
payload.setEmail("bad-guy@evil.real");
payload.setEmail(unknownEmail);
authResult = authenticationMechanism.authenticate(request);
assertThat(authResult).isEqualTo(AuthResult.NOT_AUTHENTICATED);
}
@@ -189,6 +208,62 @@ public class OidcTokenAuthenticationMechanismTest {
authenticationMechanism = component.regularOidcAuthenticationMechanism();
}
@Test
void testAuthenticate_ExistentUser_isCached() {
// Arrange: Create a spy of the actual cache object.
// A spy calls the real methods of the object while allowing us to verify interactions.
LoadingCache<String, Optional<User>> spiedCache =
spy(OidcTokenAuthenticationMechanism.userCache);
OidcTokenAuthenticationMechanism.setCacheForTesting(spiedCache);
// Act: Call the authenticate method.
authenticationMechanism.authenticate(request);
// Assert: Verify that the cache's "get" method was called exactly once.
// This confirms the cache is being used without checking its internal stats.
verify(spiedCache).get(email);
}
@Test
void testAuthenticate_nonExistentUser_isCached() {
// Arrange: Use an email that is not in the test database.
payload.setEmail(unknownEmail);
LoadingCache<String, Optional<User>> spiedCache =
spy(OidcTokenAuthenticationMechanism.userCache);
OidcTokenAuthenticationMechanism.setCacheForTesting(spiedCache);
// Act: Call the authenticate method.
authenticationMechanism.authenticate(request);
// Assert: Verify that the cache's "get" method was called for the unverified email.
// This confirms that we attempted to look up the unknown user in the cache.
verify(spiedCache).get(unknownEmail);
}
@Test
void testAuthenticate_whenCacheIsDisabled_cacheIsNotUsed() {
// Arrange: Explicitly disable the cache and create a spy.
RegistryConfig.overrideIsUserAuthCachingEnabledForTesting(false);
LoadingCache<String, Optional<User>> spiedCache =
spy(OidcTokenAuthenticationMechanism.userCache);
OidcTokenAuthenticationMechanism.setCacheForTesting(spiedCache);
// Act: Authenticate the user.
AuthResult authResult = authenticationMechanism.authenticate(request);
// Assert: The authentication should still succeed because the code falls back
// to the direct database call.
assertThat(authResult.isAuthenticated()).isTrue();
// Assert: Crucially, verify that the cache's "get" method was NEVER called.
// This proves the cache was correctly bypassed.
verify(spiedCache, never()).get(any(String.class));
// Teardown: Restore the default setting for other tests.
RegistryConfig.overrideIsUserAuthCachingEnabledForTesting(true);
}
@Singleton
@Component(modules = {AuthModule.class, TestModule.class})
interface TestComponent {
@@ -234,4 +309,12 @@ public class OidcTokenAuthenticationMechanismTest {
return GoogleCredentialsBundle.create(GoogleCredentials.newBuilder().build());
}
}
private void reinitializeCache() {
OidcTokenAuthenticationMechanism.userCache =
CacheUtils.newCacheBuilder(getUserAuthCachingDuration())
.maximumSize(getUserAuthMaxCachedEntries())
.recordStats()
.build(OidcTokenAuthenticationMechanism::loadUser);
}
}
@@ -50,7 +50,6 @@ class RegistrarPocTest {
.setRegistrar(testRegistrar)
.setName("Judith Registrar")
.setEmailAddress("judith.doe@example.com")
.setRegistryLockEmailAddress("judith.doe@external.com")
.setPhoneNumber("+1.2125650000")
.setFaxNumber("+1.2125650001")
.setTypes(ImmutableSet.of(WHOIS))
@@ -26,10 +26,16 @@ import com.google.common.net.HostAndPort;
import com.google.common.util.concurrent.SimpleTimeLimiter;
import google.registry.util.RegistryEnvironment;
import google.registry.util.UrlChecker;
import jakarta.servlet.MultipartConfigElement;
import jakarta.servlet.annotation.MultipartConfig;
import jakarta.servlet.http.HttpServlet;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.FutureTask;
@@ -49,6 +55,8 @@ import org.eclipse.jetty.server.ServerConnector;
* {@link #stop()} methods. However, a {@link #process()} method was added, which is used to process
* requests made to servlets (not static files) in the calling thread.
*
* <p>A servlet that expects multi-part requests should be annotated with {@link MultipartConfig}.
*
* <p><b>Note:</b> This server is intended for development purposes. For the love all that is good,
* do not make this public-facing.
*
@@ -70,6 +78,7 @@ public final class TestServer {
private final HostAndPort urlAddress;
private final Server server = new Server();
private final BlockingQueue<FutureTask<Void>> requestQueue = new LinkedBlockingDeque<>();
private List<Path> multiPartTmpDirs = new ArrayList<>();
/**
* Creates a new instance, but does not begin serving.
@@ -134,6 +143,13 @@ public final class TestServer {
},
SHUTDOWN_TIMEOUT_MS,
TimeUnit.MILLISECONDS);
for (var dir : multiPartTmpDirs) {
try {
Files.delete(dir);
} catch (Exception e) {
// Ignore
}
}
} catch (Exception e) {
throwIfUnchecked(e);
throw new RuntimeException(e);
@@ -161,7 +177,27 @@ public final class TestServer {
StaticResourceServlet.configureServletHolder(holder, runfile.getKey(), runfile.getValue());
}
for (Route route : routes) {
context.addServlet(wrapServlet(route.servletClass()), route.path());
holder = context.addServlet(wrapServlet(route.servletClass()), route.path());
MultipartConfig multipartConfig = route.servletClass().getAnnotation(MultipartConfig.class);
if (multipartConfig != null) {
try {
var location = multipartConfig.location();
if (location == null || location.isBlank()) {
Path tmpDir = Files.createTempDirectory("TestServer_");
multiPartTmpDirs.add(tmpDir);
location = tmpDir.toString();
}
MultipartConfigElement multipartConfigElement =
new MultipartConfigElement(
location,
multipartConfig.maxFileSize(),
multipartConfig.maxRequestSize(),
multipartConfig.fileSizeThreshold());
holder.getRegistration().setMultipartConfig(multipartConfigElement);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
holder = context.addServlet(DefaultServlet.class, "/*");
holder.setInitParameter("aliases", "1");
@@ -291,7 +291,7 @@ public final class DomainLockUtilsTest {
.setRegistrarId("TheRegistrar")
.setRepoId(domain.getRepoId())
.isSuperuser(false)
.setRegistrarPocId(POC_ID)
.setRegistryLockEmail(POC_ID)
.build());
clock.advanceOneMilli();
RegistryLock resultLock =
@@ -477,7 +477,7 @@ public final class DomainLockUtilsTest {
.setRepoId("repoId")
.setRelockDuration(standardHours(6))
.setRegistrarId("TheRegistrar")
.setRegistrarPocId("someone@example.com")
.setRegistryLockEmail("someone@example.com")
.setVerificationCode("hi")
.build());
domainLockUtils.enqueueDomainRelock(lock.getRelockDuration().get(), lock.getRevisionId(), 0);
@@ -504,7 +504,7 @@ public final class DomainLockUtilsTest {
.setDomainName("example.tld")
.setRepoId("repoId")
.setRegistrarId("TheRegistrar")
.setRegistrarPocId("someone@example.com")
.setRegistryLockEmail("someone@example.com")
.setVerificationCode("hi")
.build());
IllegalArgumentException thrown =
@@ -92,7 +92,6 @@ class RegistrarPocCommandTest extends CommandTestCase<RegistrarPocCommand> {
"--mode=UPDATE",
"--name=Judith Registrar",
"--email=judith.doe@example.com",
"--registry_lock_email=judith.doe@external.com",
"--phone=+1.2125650000",
"--fax=+1.2125650001",
"--contact_type=WHOIS",
@@ -108,7 +107,6 @@ class RegistrarPocCommandTest extends CommandTestCase<RegistrarPocCommand> {
.setRegistrar(registrar)
.setName("Judith Registrar")
.setEmailAddress("judith.doe@example.com")
.setRegistryLockEmailAddress("judith.doe@external.com")
.setPhoneNumber("+1.2125650000")
.setFaxNumber("+1.2125650001")
.setTypes(ImmutableSet.of(WHOIS))
@@ -255,7 +253,6 @@ class RegistrarPocCommandTest extends CommandTestCase<RegistrarPocCommand> {
"--mode=CREATE",
"--name=Jim Doe",
"--email=jim.doe@example.com",
"--registry_lock_email=jim.doe@external.com",
"--contact_type=ADMIN,ABUSE",
"--visible_in_whois_as_admin=true",
"--visible_in_whois_as_tech=false",
@@ -269,7 +266,6 @@ class RegistrarPocCommandTest extends CommandTestCase<RegistrarPocCommand> {
.setRegistrar(registrar)
.setName("Jim Doe")
.setEmailAddress("jim.doe@example.com")
.setRegistryLockEmailAddress("jim.doe@external.com")
.setTypes(ImmutableSet.of(ADMIN, ABUSE))
.setVisibleInWhoisAsAdmin(true)
.setVisibleInWhoisAsTech(false)
@@ -318,87 +314,6 @@ class RegistrarPocCommandTest extends CommandTestCase<RegistrarPocCommand> {
assertThat(loadRegistrar("NewRegistrar").getContactsRequireSyncing()).isTrue();
}
@Test
void testCreate_setAllowedToSetRegistryLockPassword() throws Exception {
runCommandForced(
"--mode=CREATE",
"--name=Jim Doe",
"--email=jim.doe@example.com",
"--registry_lock_email=jim.doe.registry.lock@example.com",
"--allowed_to_set_registry_lock_password=true",
"NewRegistrar");
RegistrarPoc registrarPoc = loadRegistrar("NewRegistrar").getContacts().asList().get(1);
assertThat(registrarPoc.isAllowedToSetRegistryLockPassword()).isTrue();
registrarPoc.asBuilder().setRegistryLockPassword("foo");
}
@Test
void testUpdate_setAllowedToSetRegistryLockPassword() throws Exception {
Registrar registrar = loadRegistrar("NewRegistrar");
RegistrarPoc registrarPoc =
persistResource(
new RegistrarPoc.Builder()
.setRegistrar(registrar)
.setName("Jim Doe")
.setEmailAddress("jim.doe@example.com")
.build());
assertThat(registrarPoc.isAllowedToSetRegistryLockPassword()).isFalse();
// First, try (and fail) to set the password directly
assertThrows(
IllegalArgumentException.class,
() -> registrarPoc.asBuilder().setRegistryLockPassword("foo"));
// Next, try (and fail) to allow registry lock without a registry lock email
assertThat(
assertThrows(
IllegalArgumentException.class,
() ->
runCommandForced(
"--mode=UPDATE",
"--email=jim.doe@example.com",
"--allowed_to_set_registry_lock_password=true",
"NewRegistrar")))
.hasMessageThat()
.isEqualTo("Registry lock email must not be null if allowing registry lock access");
// Next, include the email and it should succeed
runCommandForced(
"--mode=UPDATE",
"--email=jim.doe@example.com",
"--registry_lock_email=jim.doe.registry.lock@example.com",
"--allowed_to_set_registry_lock_password=true",
"NewRegistrar");
RegistrarPoc newContact = reloadResource(registrarPoc);
assertThat(newContact.isAllowedToSetRegistryLockPassword()).isTrue();
// should be allowed to set the password now
newContact.asBuilder().setRegistryLockPassword("foo");
}
@Test
void testUpdate_setAllowedToSetRegistryLockPassword_removesOldPassword() throws Exception {
Registrar registrar = loadRegistrar("NewRegistrar");
RegistrarPoc registrarPoc =
persistResource(
new RegistrarPoc.Builder()
.setRegistrar(registrar)
.setName("Jim Doe")
.setEmailAddress("jim.doe@example.com")
.setRegistryLockEmailAddress("jim.doe.registry.lock@example.com")
.setAllowedToSetRegistryLockPassword(true)
.setRegistryLockPassword("hi")
.build());
assertThat(registrarPoc.verifyRegistryLockPassword("hi")).isTrue();
assertThat(registrarPoc.verifyRegistryLockPassword("hello")).isFalse();
runCommandForced(
"--mode=UPDATE",
"--email=jim.doe@example.com",
"--allowed_to_set_registry_lock_password=true",
"NewRegistrar");
registrarPoc = reloadResource(registrarPoc);
assertThat(registrarPoc.verifyRegistryLockPassword("hi")).isFalse();
}
@Test
void testCreate_failure_badEmail() {
IllegalArgumentException thrown =
@@ -75,7 +75,8 @@ public class ConsoleRegistryLockActionTest extends ConsoleActionBaseTestCase {
Note: this code will expire in one hour.
https://registrarconsole.tld/console/#/registry-lock-verify?lockVerificationCode=\
123456789ABCDEFGHJKLMNPQRSTUVWXY""";
123456789ABCDEFGHJKLMNPQRSTUVWXY\
""";
@Mock GmailClient gmailClient;
private ConsoleRegistryLockAction action;
@@ -112,8 +113,8 @@ public class ConsoleRegistryLockActionTest extends ConsoleActionBaseTestCase {
assertThat(response.getStatus()).isEqualTo(SC_OK);
assertThat(response.getPayload())
.isEqualTo(
"""
[{"domainName":"example.test","registrarPocId":"johndoe@theregistrar.com","lockRequestTime":\
"""
[{"domainName":"example.test","registryLockEmail":"johndoe@theregistrar.com","lockRequestTime":\
{"creationTime":"2024-04-15T00:00:00.000Z"},"unlockRequestTime":"null","lockCompletionTime":\
"2024-04-15T00:00:00.000Z","unlockCompletionTime":"null","isSuperuser":false}]\
""");
@@ -127,7 +128,7 @@ public class ConsoleRegistryLockActionTest extends ConsoleActionBaseTestCase {
.setDomainName("expired.test")
.setRegistrarId("TheRegistrar")
.setVerificationCode("123456789ABCDEFGHJKLMNPQRSTUVWXY")
.setRegistrarPocId("johndoe@theregistrar.com")
.setRegistryLockEmail("johndoe@theregistrar.com")
.build();
saveRegistryLock(expiredLock);
RegistryLock expiredUnlock =
@@ -136,7 +137,7 @@ public class ConsoleRegistryLockActionTest extends ConsoleActionBaseTestCase {
.setDomainName("expiredunlock.test")
.setRegistrarId("TheRegistrar")
.setVerificationCode("123456789ABCDEFGHJKLMNPQRSTUVWXY")
.setRegistrarPocId("johndoe@theregistrar.com")
.setRegistryLockEmail("johndoe@theregistrar.com")
.setLockCompletionTime(clock.nowUtc())
.setUnlockRequestTime(clock.nowUtc())
.build();
@@ -149,7 +150,7 @@ public class ConsoleRegistryLockActionTest extends ConsoleActionBaseTestCase {
.setDomainName("example.test")
.setRegistrarId("TheRegistrar")
.setVerificationCode("123456789ABCDEFGHJKLMNPQRSTUVWXY")
.setRegistrarPocId("johndoe@theregistrar.com")
.setRegistryLockEmail("johndoe@theregistrar.com")
.setLockCompletionTime(clock.nowUtc())
.build();
clock.advanceOneMilli();
@@ -168,7 +169,7 @@ public class ConsoleRegistryLockActionTest extends ConsoleActionBaseTestCase {
.setDomainName("pending.test")
.setRegistrarId("TheRegistrar")
.setVerificationCode("111111111ABCDEFGHJKLMNPQRSTUVWXY")
.setRegistrarPocId("johndoe@theregistrar.com")
.setRegistryLockEmail("johndoe@theregistrar.com")
.build();
RegistryLock incompleteUnlock =
@@ -177,7 +178,7 @@ public class ConsoleRegistryLockActionTest extends ConsoleActionBaseTestCase {
.setDomainName("incompleteunlock.test")
.setRegistrarId("TheRegistrar")
.setVerificationCode("123456789ABCDEFGHJKLMNPQRSTUVWXY")
.setRegistrarPocId("johndoe@theregistrar.com")
.setRegistryLockEmail("johndoe@theregistrar.com")
.setLockCompletionTime(clock.nowUtc())
.setUnlockRequestTime(clock.nowUtc())
.build();
@@ -187,7 +188,7 @@ public class ConsoleRegistryLockActionTest extends ConsoleActionBaseTestCase {
.setRepoId("repoId")
.setDomainName("unlocked.test")
.setRegistrarId("TheRegistrar")
.setRegistrarPocId("johndoe@theregistrar.com")
.setRegistryLockEmail("johndoe@theregistrar.com")
.setVerificationCode("123456789ABCDEFGHJKLMNPQRSTUUUUU")
.setLockCompletionTime(clock.nowUtc())
.setUnlockRequestTime(clock.nowUtc())
@@ -206,26 +207,27 @@ public class ConsoleRegistryLockActionTest extends ConsoleActionBaseTestCase {
// locks or completed unlocks
assertThat(response.getPayload())
.isEqualTo(
"""
"""
[{"domainName":"adminexample.test","lockRequestTime":{"creationTime":"2024-04-16T00:00:00.001Z"},\
"unlockRequestTime":"null","lockCompletionTime":"2024-04-16T00:00:00.001Z","unlockCompletionTime":\
"null","isSuperuser":true},\
\
{"domainName":"example.test","registrarPocId":"johndoe@theregistrar.com","lockRequestTime":\
{"domainName":"example.test","registryLockEmail":"johndoe@theregistrar.com","lockRequestTime":\
{"creationTime":"2024-04-16T00:00:00.001Z"},"unlockRequestTime":"null","lockCompletionTime":\
"2024-04-16T00:00:00.000Z","unlockCompletionTime":"null","isSuperuser":false},\
\
{"domainName":"expiredunlock.test","registrarPocId":"johndoe@theregistrar.com","lockRequestTime":\
{"domainName":"expiredunlock.test","registryLockEmail":"johndoe@theregistrar.com","lockRequestTime":\
{"creationTime":"2024-04-15T00:00:00.000Z"},"unlockRequestTime":"2024-04-15T00:00:00.000Z",\
"lockCompletionTime":"2024-04-15T00:00:00.000Z","unlockCompletionTime":"null","isSuperuser":false},\
\
{"domainName":"incompleteunlock.test","registrarPocId":"johndoe@theregistrar.com","lockRequestTime":\
{"domainName":"incompleteunlock.test","registryLockEmail":"johndoe@theregistrar.com","lockRequestTime":\
{"creationTime":"2024-04-16T00:00:00.001Z"},"unlockRequestTime":"2024-04-16T00:00:00.001Z",\
"lockCompletionTime":"2024-04-16T00:00:00.001Z","unlockCompletionTime":"null","isSuperuser":false},\
\
{"domainName":"pending.test","registrarPocId":"johndoe@theregistrar.com","lockRequestTime":\
{"domainName":"pending.test","registryLockEmail":"johndoe@theregistrar.com","lockRequestTime":\
{"creationTime":"2024-04-16T00:00:00.001Z"},"unlockRequestTime":"null","lockCompletionTime":"null",\
"unlockCompletionTime":"null","isSuperuser":false}]""");
"unlockCompletionTime":"null","isSuperuser":false}]\
""");
}
@Test
@@ -500,7 +502,7 @@ public class ConsoleRegistryLockActionTest extends ConsoleActionBaseTestCase {
.setRepoId(defaultDomain.getRepoId())
.setDomainName(defaultDomain.getDomainName())
.setRegistrarId(defaultDomain.getCurrentSponsorRegistrarId())
.setRegistrarPocId("johndoe@theregistrar.com")
.setRegistryLockEmail("johndoe@theregistrar.com")
.setVerificationCode("123456789ABCDEFGHJKLMNPQRSTUUUUU");
}
@@ -187,7 +187,7 @@ public class ConsoleRegistryLockVerifyActionTest extends ConsoleActionBaseTestCa
.setRepoId(defaultDomain.getRepoId())
.setDomainName(defaultDomain.getDomainName())
.setRegistrarId(defaultDomain.getCurrentSponsorRegistrarId())
.setRegistrarPocId("johndoe@theregistrar.com")
.setRegistryLockEmail("johndoe@theregistrar.com")
.setVerificationCode(DEFAULT_CODE);
}
@@ -284,39 +284,27 @@ class ContactActionTest extends ConsoleActionBaseTestCase {
"Registrar New Registrar (registrarId) updated in registry unittest"
+ " environment")
.setBody(
"The following changes were made in registry unittest environment to the"
+ " registrar registrarId by admin fte@email.tld:\n"
+ "\n"
+ "contacts:\n"
+ " ADDED:\n"
+ " {id="
+ id
+ ", name=Test Registrar 2,"
+ " emailAddress=incorrect@example.com, registrarId=registrarId,"
+ " registryLockEmailAddress=null, phoneNumber=+1.1234567890,"
+ " faxNumber=+1.1234567891, types=[TECH],"
+ " visibleInWhoisAsAdmin=false, visibleInWhoisAsTech=true,"
+ " visibleInDomainWhoisAsAbuse=false,"
+ " allowedToSetRegistryLockPassword=false}\n"
+ " REMOVED:\n"
+ " {id="
+ id
+ ", name=Test Registrar 2, emailAddress=test.registrar2@example.com,"
+ " registrarId=registrarId, registryLockEmailAddress=null,"
+ " phoneNumber=+1.1234567890, faxNumber=+1.1234567891, types=[TECH],"
+ " visibleInWhoisAsAdmin=false,"
+ " visibleInWhoisAsTech=true, visibleInDomainWhoisAsAbuse=false,"
+ " allowedToSetRegistryLockPassword=false}\n"
+ " FINAL CONTENTS:\n"
+ " {id="
+ id
+ ", name=Test Registrar 2,"
+ " emailAddress=incorrect@example.com, registrarId=registrarId,"
+ " registryLockEmailAddress=null, phoneNumber=+1.1234567890,"
+ " faxNumber=+1.1234567891, types=[TECH],"
+ " visibleInWhoisAsAdmin=false, visibleInWhoisAsTech=true,"
+ " visibleInDomainWhoisAsAbuse=false,"
+ " allowedToSetRegistryLockPassword=false}\n")
"""
The following changes were made in registry unittest environment to the registrar registrarId \
by admin fte@email.tld:
contacts:
ADDED:
{id=5, name=Test Registrar 2, emailAddress=incorrect@example.com, registrarId=registrarId, \
phoneNumber=+1.1234567890, faxNumber=+1.1234567891, types=[TECH], visibleInWhoisAsAdmin=false, \
visibleInWhoisAsTech=true, visibleInDomainWhoisAsAbuse=false, \
allowedToSetRegistryLockPassword=false}
REMOVED:
{id=5, name=Test Registrar 2, emailAddress=test.registrar2@example.com, \
registrarId=registrarId, phoneNumber=+1.1234567890, faxNumber=+1.1234567891, types=[TECH], \
visibleInWhoisAsAdmin=false, visibleInWhoisAsTech=true, visibleInDomainWhoisAsAbuse=false, \
allowedToSetRegistryLockPassword=false}
FINAL CONTENTS:
{id=5, name=Test Registrar 2, emailAddress=incorrect@example.com, registrarId=registrarId, \
phoneNumber=+1.1234567890, faxNumber=+1.1234567891, types=[TECH], visibleInWhoisAsAdmin=false, \
visibleInWhoisAsTech=true, visibleInDomainWhoisAsAbuse=false, \
allowedToSetRegistryLockPassword=false}
""")
.setRecipients(ImmutableList.of(new InternetAddress("notification@test.example")))
.build());
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -194,3 +194,4 @@ V193__password_reset_request.sql
V194__password_reset_request_registrar.sql
V195__registrar_poc_id.sql
V196__tld_expiry_access_period_enabled.sql
V197__poc_rlock_drop_not_null.sql
@@ -0,0 +1,16 @@
-- 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.
-- In order to remove the field from the Java class it needs to be nullable
ALTER table "RegistrarPoc" ALTER column allowed_to_set_registry_lock_password DROP NOT NULL;
@@ -688,9 +688,6 @@
id bigint,
name text,
phone_number text,
registry_lock_email_address text,
registry_lock_password_hash text,
registry_lock_password_salt text,
types text[],
visible_in_domain_whois_as_abuse boolean not null,
visible_in_whois_as_admin boolean not null,
@@ -1009,7 +1009,7 @@ CREATE TABLE public."Registrar" (
CREATE TABLE public."RegistrarPoc" (
email_address text NOT NULL,
allowed_to_set_registry_lock_password boolean NOT NULL,
allowed_to_set_registry_lock_password boolean,
fax_number text,
name text,
phone_number text,
@@ -379,9 +379,9 @@
"type": "string",
"description": "Name of the domain."
},
"registrarPocId": {
"registryLockEmail": {
"type": "string",
"description": "Registrar point of contact ID."
"description": "Email address of the requester."
},
"lockRequestTime": {
"type": "object",
+6 -1
View File
@@ -15,7 +15,6 @@ spec:
spec:
serviceAccountName: nomulus
nodeSelector:
cloud.google.com/compute-class: "Performance"
cloud.google.com/machine-family: c4
containers:
- name: backend
@@ -25,9 +24,15 @@ spec:
name: http
resources:
requests:
# explicit pod-slots 0 is required in order to downgrade node
# class from performance, which has implicit pod-slots 1
cloud.google.com/pod-slots: 0
cpu: "500m"
memory: "1Gi"
limits:
# explicit pod-slots 0 is required in order to downgrade node
# class from performance, which has implicit pod-slots 1
cloud.google.com/pod-slots: 0
cpu: "1000m"
memory: "1.5Gi"
args: [ENVIRONMENT]
+13 -2
View File
@@ -15,7 +15,6 @@ spec:
spec:
serviceAccountName: nomulus
nodeSelector:
cloud.google.com/compute-class: "Performance"
cloud.google.com/machine-family: c4
containers:
- name: frontend
@@ -25,9 +24,15 @@ spec:
name: http
resources:
requests:
# explicit pod-slots 0 is required in order to downgrade node
# class from performance, which has implicit pod-slots 1
cloud.google.com/pod-slots: 0
cpu: "600m"
memory: "1Gi"
limits:
# explicit pod-slots 0 is required in order to downgrade node
# class from performance, which has implicit pod-slots 1
cloud.google.com/pod-slots: 0
cpu: "1000m"
memory: "2Gi"
args: [ENVIRONMENT]
@@ -53,9 +58,15 @@ spec:
name: epp
resources:
requests:
# explicit pod-slots 0 is required in order to downgrade node
# class from performance, which has implicit pod-slots 1
cloud.google.com/pod-slots: 0
cpu: "600m"
memory: "512Mi"
limits:
# explicit pod-slots 0 is required in order to downgrade node
# class from performance, which has implicit pod-slots 1
cloud.google.com/pod-slots: 0
cpu: "1000m"
memory: "1Gi"
args: [--env, PROXY_ENV, --log, --local]
@@ -88,7 +99,7 @@ spec:
apiVersion: apps/v1
kind: Deployment
name: frontend
minReplicas: 8
minReplicas: 12
maxReplicas: 16
metrics:
- type: Resource
@@ -119,6 +119,7 @@ public class SslClientInitializer<C extends Channel> extends ChannelInitializer<
sslContextBuilder
.build()
.newHandler(channel.alloc(), hostProvider.apply(channel), portProvider.apply(channel));
sslHandler.setHandshakeTimeoutMillis(20000);
// Enable hostname verification.
SSLEngine sslEngine = sslHandler.engine();
@@ -139,6 +139,8 @@ public class SslServerInitializer<C extends Channel> extends ChannelInitializer<
logger.atInfo().log("Available Cipher Suites: %s", sslContext.cipherSuites());
SslHandler sslHandler = sslContext.newHandler(channel.alloc());
sslHandler.setHandshakeTimeoutMillis(20000);
if (requireClientCert) {
Promise<X509Certificate> clientCertificatePromise = channel.eventLoop().newPromise();
Future<Channel> unusedFuture =
@@ -159,15 +161,15 @@ public class SslServerInitializer<C extends Channel> extends ChannelInitializer<
}
logger.atInfo().log(
"""
--SSL Information--
Client Certificate Hash: %s
SSL Protocol: %s
Cipher Suite: %s
Not Before: %s
Not After: %s
Client Certificate Type: %s
Client Certificate Length: %s
""",
--SSL Information--
Client Certificate Hash: %s
SSL Protocol: %s
Cipher Suite: %s
Not Before: %s
Not After: %s
Client Certificate Type: %s
Client Certificate Length: %s
""",
getCertificateHash(clientCertificate),
sslSession.getProtocol(),
sslSession.getCipherSuite(),
-1
View File
@@ -33,7 +33,6 @@ do
--project "${project}" --zone "${parts[1]}"
sed s/GCP_PROJECT/${project}/g "./kubernetes/proxy-deployment-${environment}.yaml" | \
kubectl apply -f -
kubectl apply -f "./kubernetes/proxy-limit-range.yaml" --force
kubectl apply -f "./kubernetes/proxy-service.yaml" --force
# Alpha does not have canary
if [[ ${environment} != "alpha" ]]; then
@@ -33,6 +33,13 @@ spec:
port: health-check
initialDelaySeconds: 15
periodSeconds: 20
resources:
requests:
cpu: "400m"
memory: "350Mi"
limits:
cpu: "600m"
memory: "512Mi"
imagePullPolicy: Always
args: ["--env", "production_canary"]
env:
@@ -33,6 +33,13 @@ spec:
port: health-check
initialDelaySeconds: 15
periodSeconds: 20
resources:
requests:
cpu: "400m"
memory: "350Mi"
limits:
cpu: "600m"
memory: "512Mi"
imagePullPolicy: Always
args: ["--env", "production"]
env:
@@ -33,6 +33,13 @@ spec:
port: health-check
initialDelaySeconds: 15
periodSeconds: 20
resources:
requests:
cpu: "400m"
memory: "350Mi"
limits:
cpu: "600m"
memory: "512Mi"
imagePullPolicy: Always
args: ["--env", "sandbox_canary", "--log"]
env:
@@ -33,6 +33,13 @@ spec:
port: health-check
initialDelaySeconds: 15
periodSeconds: 20
resources:
requests:
cpu: "400m"
memory: "350Mi"
limits:
cpu: "600m"
memory: "512Mi"
imagePullPolicy: Always
args: ["--env", "sandbox", "--log"]
env:
-14
View File
@@ -1,14 +0,0 @@
apiVersion: v1
kind: LimitRange
metadata:
name: resource-limits
namespace: default
spec:
limits:
- type: Container
default:
cpu: "300m"
memory: "512Mi"
defaultRequest:
cpu: "100m"
memory: "350Mi"