mirror of
https://github.com/google/nomulus
synced 2025-12-23 06:15:42 +00:00
Skip user loading for proxy service account (#2825)
* Skip user loading for proxy service account Reduces database load by skipping the User entity lookup for the proxy service account during OIDC authentication. The high volume of EPP "hello" and "login" commands from the proxy service account results in a constant database load. These lookups are unnecessary as the proxy service account is not expected to have a corresponding User object. This change optimizes the authentication flow by checking for the proxy service account email *before* attempting to load a User from the database. This bypasses the database transaction entirely for these high-volume requests. This approach is more efficient than caching, as it eliminates the database lookup for the proxy service account altogether, rather than just caching the result. * comment added and service account llokup time improved * comment updated for more clarity
This commit is contained in:
@@ -1591,26 +1591,6 @@ 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,9 +161,6 @@ 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,20 +326,6 @@ 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:
|
||||
|
||||
@@ -15,20 +15,14 @@
|
||||
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;
|
||||
@@ -77,40 +71,6 @@ 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)
|
||||
@@ -152,24 +112,23 @@ 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;
|
||||
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);
|
||||
}
|
||||
|
||||
// Short-circuit the user lookup for known service accounts.
|
||||
// This check bypasses the database lookup for high-volume
|
||||
// traffic from trusted system accounts to reduce database load.
|
||||
|
||||
if (serviceAccountEmails.contains(email)) {
|
||||
return AuthResult.createApp(email);
|
||||
}
|
||||
logger.atInfo().log("No service account found for email address %s, loading the User", email);
|
||||
Optional<User> maybeUser =
|
||||
tm().transact(() -> tm().loadByKeyIfPresent(VKey.create(User.class, email)));
|
||||
stopwatch.tick("OidcTokenAuthenticationMechanism maybeUser loaded");
|
||||
if (maybeUser.isPresent()) {
|
||||
return AuthResult.createUser(maybeUser.get());
|
||||
}
|
||||
logger.atInfo().log("No end user found for email address %s", email);
|
||||
if (serviceAccountEmails.stream().anyMatch(e -> e.equals(email))) {
|
||||
return AuthResult.createApp(email);
|
||||
}
|
||||
logger.atInfo().log("No service account found for email address %s", email);
|
||||
|
||||
logger.atWarning().log(
|
||||
"The email address %s is not tied to a principal with access to Nomulus", email);
|
||||
return AuthResult.NOT_AUTHENTICATED;
|
||||
|
||||
@@ -16,20 +16,13 @@ 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;
|
||||
@@ -40,9 +33,7 @@ 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;
|
||||
@@ -53,7 +44,6 @@ 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;
|
||||
@@ -88,12 +78,6 @@ 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);
|
||||
@@ -167,8 +151,7 @@ public class OidcTokenAuthenticationMechanismTest {
|
||||
payload.setEmail("service@email.test");
|
||||
authResult = authenticationMechanism.authenticate(request);
|
||||
assertThat(authResult.isAuthenticated()).isTrue();
|
||||
assertThat(authResult.authLevel()).isEqualTo(AuthLevel.USER);
|
||||
assertThat(authResult.user().get()).isEqualTo(serviceUser);
|
||||
assertThat(authResult.authLevel()).isEqualTo(AuthLevel.APP);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -208,62 +191,6 @@ 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 {
|
||||
@@ -309,12 +236,4 @@ public class OidcTokenAuthenticationMechanismTest {
|
||||
return GoogleCredentialsBundle.create(GoogleCredentials.newBuilder().build());
|
||||
}
|
||||
}
|
||||
|
||||
private void reinitializeCache() {
|
||||
OidcTokenAuthenticationMechanism.userCache =
|
||||
CacheUtils.newCacheBuilder(getUserAuthCachingDuration())
|
||||
.maximumSize(getUserAuthMaxCachedEntries())
|
||||
.recordStats()
|
||||
.build(OidcTokenAuthenticationMechanism::loadUser);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user