This is an automated email from the ASF dual-hosted git repository.

xiangfu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new 0f93d52ef0b [audit] Add SPI-based token resolver for custom audit 
identity resolution (#17658)
0f93d52ef0b is described below

commit 0f93d52ef0bfa1c2dc367b31f137a63bd25f9d2f
Author: Suvodeep Pyne <[email protected]>
AuthorDate: Tue Feb 10 00:17:37 2026 -0800

    [audit] Add SPI-based token resolver for custom audit identity resolution 
(#17658)
    
    * [audit] Add support for custom token resolvers in audit identity 
resolution
    
    - Introduced `AuditTokenResolver` SPI for handling proprietary token 
formats.
    - Updated `AuditIdentityResolver` to integrate resolver-based identity 
resolution.
    - Enhanced `AuditConfig` to support token resolver class configuration.
    - Added unit tests and mock resolver for validation.
    
    * [audit] Refactor audit identity resolution to use structured 
`AuditUserIdentity`
    
    - Updated `AuditTokenResolver` to return `AuditUserIdentity` instead of 
plain principal strings.
    - Introduced `AuditUserIdentity` interface for extensible identity 
representations.
    - Refactored related classes and tests to use structured identity.
    - Enhanced `AuditIdentityResolver` to handle the updated resolver contract.
    
    * [audit] Simplify `MockAuditTokenResolver` implementation and enhance 
error handling in `AuditIdentityResolver`
    
    - Removed unnecessary `MockConfig` wrapper and replaced `AtomicReference` 
with direct static fields.
    - Streamlined logic in `MockAuditTokenResolver` for clearer behavior.
    - Improved resilience in `AuditIdentityResolver` to handle null resolver 
class gracefully.
    
    * [audit] Add no-arg constructor to MockAuditTokenResolver for PluginManager
    
    PluginManager.createInstance() requires a no-arg constructor to
    instantiate classes. The missing constructor caused
    testResolverLoadedViaPluginManager to fail in CI.
---
 .../org/apache/pinot/common/audit/AuditConfig.java |  12 ++
 .../org/apache/pinot/common/audit/AuditEvent.java  |   3 +-
 .../pinot/common/audit/AuditIdentityResolver.java  | 105 ++++++++++++++-
 .../common/audit/AuditIdentityResolverTest.java    | 145 ++++++++++-----------
 .../pinot/common/audit/MockAuditTokenResolver.java |  93 +++++++++++++
 .../apache/pinot/spi/audit/AuditTokenResolver.java |  51 ++++++++
 .../apache/pinot/spi/audit/AuditUserIdentity.java  |  41 ++++++
 7 files changed, 368 insertions(+), 82 deletions(-)

diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/audit/AuditConfig.java 
b/pinot-common/src/main/java/org/apache/pinot/common/audit/AuditConfig.java
index 9e542cf635c..e8ce1013202 100644
--- a/pinot-common/src/main/java/org/apache/pinot/common/audit/AuditConfig.java
+++ b/pinot-common/src/main/java/org/apache/pinot/common/audit/AuditConfig.java
@@ -60,6 +60,9 @@ public final class AuditConfig {
   @JsonProperty("capture.response.enabled")
   private boolean _captureResponseEnabled = false;
 
+  @JsonProperty("token.resolver.class")
+  private String _tokenResolverClass = "";
+
   public boolean isEnabled() {
     return _enabled;
   }
@@ -132,6 +135,14 @@ public final class AuditConfig {
     _captureResponseEnabled = captureResponseEnabled;
   }
 
+  public String getTokenResolverClass() {
+    return _tokenResolverClass;
+  }
+
+  public void setTokenResolverClass(String tokenResolverClass) {
+    _tokenResolverClass = tokenResolverClass;
+  }
+
   @Override
   public String toString() {
     return new StringJoiner(", ", AuditConfig.class.getSimpleName() + "[", 
"]").add("_enabled=" + _enabled)
@@ -143,6 +154,7 @@ public final class AuditConfig {
         .add("_useridHeader='" + _useridHeader + "'")
         .add("_useridJwtClaimName='" + _useridJwtClaimName + "'")
         .add("_captureResponseEnabled=" + _captureResponseEnabled)
+        .add("_tokenResolverClass='" + _tokenResolverClass + "'")
         .toString();
   }
 }
diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/audit/AuditEvent.java 
b/pinot-common/src/main/java/org/apache/pinot/common/audit/AuditEvent.java
index 8f370eeb3db..7a2c2bd760d 100644
--- a/pinot-common/src/main/java/org/apache/pinot/common/audit/AuditEvent.java
+++ b/pinot-common/src/main/java/org/apache/pinot/common/audit/AuditEvent.java
@@ -22,6 +22,7 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import java.util.Map;
+import org.apache.pinot.spi.audit.AuditUserIdentity;
 
 
 /**
@@ -209,7 +210,7 @@ public class AuditEvent {
   }
 
   @JsonInclude(JsonInclude.Include.NON_NULL)
-  public static class UserIdentity {
+  public static class UserIdentity implements AuditUserIdentity {
 
     @JsonProperty("principal")
     private String _principal;
diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/audit/AuditIdentityResolver.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/audit/AuditIdentityResolver.java
index 19fe2cd85fe..1522cc56214 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/audit/AuditIdentityResolver.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/audit/AuditIdentityResolver.java
@@ -18,14 +18,20 @@
  */
 package org.apache.pinot.common.audit;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.nimbusds.jwt.JWT;
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.JWTParser;
+import java.util.concurrent.atomic.AtomicReference;
+import javax.annotation.Nullable;
 import javax.inject.Inject;
 import javax.inject.Singleton;
 import javax.ws.rs.container.ContainerRequestContext;
 import javax.ws.rs.core.HttpHeaders;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.pinot.spi.audit.AuditTokenResolver;
+import org.apache.pinot.spi.audit.AuditUserIdentity;
+import org.apache.pinot.spi.plugin.PluginManager;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -36,6 +42,7 @@ import org.slf4j.LoggerFactory;
  * This resolver supports multiple identity resolution strategies in order of 
priority:
  * <ol>
  *   <li>Custom identity header - as configured in the audit configuration</li>
+ *   <li>Custom token resolver (via SPI) - for proprietary token formats</li>
  *   <li>JWT token in Authorization header - extracting principal from JWT 
claims</li>
  * </ol>
  * <p>
@@ -51,18 +58,26 @@ public class AuditIdentityResolver {
   private static final String BEARER_PREFIX = "Bearer ";
 
   private final AuditConfigManager _configManager;
+  private final AtomicReference<ResolverHolder> _resolverHolder = new 
AtomicReference<>(new ResolverHolder());
 
   @Inject
   public AuditIdentityResolver(AuditConfigManager configManager) {
     _configManager = configManager;
   }
 
+  @VisibleForTesting
+  AuditIdentityResolver(AuditConfigManager configManager, @Nullable 
AuditTokenResolver tokenResolver) {
+    _configManager = configManager;
+    _resolverHolder.set(new ResolverHolder(tokenResolver));
+  }
+
   /**
    * Resolves user identity from the given HTTP request context.
    * <p>
    * The resolution follows a priority order:
    * <ol>
    *   <li>Check for a custom identity header as specified in the audit 
configuration</li>
+   *   <li>Use custom token resolver (if configured) to resolve from 
Authorization header</li>
    *   <li>Extract principal from JWT token in the Authorization header</li>
    * </ol>
    * <p>
@@ -73,6 +88,7 @@ public class AuditIdentityResolver {
    * @return a {@link AuditEvent.UserIdentity} containing the resolved 
principal, or {@code null} if no identity
    * could be resolved
    */
+  @Nullable
   public AuditEvent.UserIdentity resolveIdentity(ContainerRequestContext 
requestContext) {
     AuditConfig config = _configManager.getCurrentConfig();
 
@@ -85,9 +101,23 @@ public class AuditIdentityResolver {
       }
     }
 
-    // Priority 2: Check JWT in Authorization header
+    // Get Authorization header for subsequent checks
     String authHeader = 
requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
-    if (StringUtils.isNotBlank(authHeader) && 
authHeader.startsWith(BEARER_PREFIX)) {
+    if (StringUtils.isBlank(authHeader)) {
+      return null;
+    }
+
+    // Priority 2: Try custom token resolver
+    AuditTokenResolver resolver = getTokenResolver(config);
+    if (resolver != null) {
+      AuditUserIdentity identity = resolver.resolve(authHeader);
+      if (identity != null && StringUtils.isNotBlank(identity.getPrincipal())) 
{
+        return new 
AuditEvent.UserIdentity().setPrincipal(identity.getPrincipal());
+      }
+    }
+
+    // Priority 3: Fallback to JWT parsing
+    if (authHeader.startsWith(BEARER_PREFIX)) {
       String token = authHeader.substring(BEARER_PREFIX.length()).trim();
       String principal = extractJwtPrincipal(token, 
config.getUseridJwtClaimName());
       if (StringUtils.isNotBlank(principal)) {
@@ -95,10 +125,45 @@ public class AuditIdentityResolver {
       }
     }
 
-    // Return null instead of anonymous
     return null;
   }
 
+  @Nullable
+  private AuditTokenResolver getTokenResolver(AuditConfig config) {
+    String resolverClass = config.getTokenResolverClass();
+    ResolverHolder currentHolder = _resolverHolder.get();
+
+    // If no resolver class configured or already loaded, return current 
resolver
+    if (StringUtils.isBlank(resolverClass) || 
currentHolder.isLoaded(resolverClass)) {
+      return currentHolder.getResolver();
+    }
+
+    // Need to load new resolver - use synchronized to prevent concurrent 
loading
+    synchronized (this) {
+      currentHolder = _resolverHolder.get();
+      if (currentHolder.isLoaded(resolverClass)) {
+        return currentHolder.getResolver();
+      }
+
+      AuditTokenResolver newResolver = loadTokenResolver(resolverClass);
+      // Don't cache the instance if it does not exist. Occasionally, we can 
run into loading failures.
+      _resolverHolder.set(new ResolverHolder(newResolver, newResolver != null 
? resolverClass : null));
+      return newResolver;
+    }
+  }
+
+  @Nullable
+  private AuditTokenResolver loadTokenResolver(String className) {
+    try {
+      AuditTokenResolver resolver = 
PluginManager.get().createInstance(className);
+      LOG.info("Successfully loaded AuditTokenResolver: {}", className);
+      return resolver;
+    } catch (Exception e) {
+      LOG.error("Failed to load AuditTokenResolver: {}", className, e);
+      return null;
+    }
+  }
+
   private String extractJwtPrincipal(String token, String claimName) {
     try {
       JWT jwt = JWTParser.parse(token);
@@ -119,4 +184,38 @@ public class AuditIdentityResolver {
       return null;
     }
   }
+
+  /**
+   * Immutable holder for resolver and its class name to enable atomic updates.
+   */
+  private static final class ResolverHolder {
+    @Nullable
+    private final AuditTokenResolver _resolver;
+    @Nullable
+    private final String _className;
+
+    ResolverHolder() {
+      _resolver = null;
+      _className = null;
+    }
+
+    ResolverHolder(@Nullable AuditTokenResolver resolver) {
+      _resolver = resolver;
+      _className = resolver != null ? resolver.getClass().getName() : null;
+    }
+
+    ResolverHolder(@Nullable AuditTokenResolver resolver, @Nullable String 
className) {
+      _resolver = resolver;
+      _className = className;
+    }
+
+    @Nullable
+    AuditTokenResolver getResolver() {
+      return _resolver;
+    }
+
+    boolean isLoaded(@Nullable String className) {
+      return className != null && className.equals(_className);
+    }
+  }
 }
diff --git 
a/pinot-common/src/test/java/org/apache/pinot/common/audit/AuditIdentityResolverTest.java
 
b/pinot-common/src/test/java/org/apache/pinot/common/audit/AuditIdentityResolverTest.java
index d33bc9cf650..04e12eeb546 100644
--- 
a/pinot-common/src/test/java/org/apache/pinot/common/audit/AuditIdentityResolverTest.java
+++ 
b/pinot-common/src/test/java/org/apache/pinot/common/audit/AuditIdentityResolverTest.java
@@ -29,6 +29,7 @@ import java.util.HashMap;
 import java.util.Map;
 import javax.ws.rs.container.ContainerRequestContext;
 import javax.ws.rs.core.HttpHeaders;
+import org.apache.pinot.spi.audit.AuditTokenResolver;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.testng.annotations.BeforeMethod;
@@ -53,14 +54,17 @@ public class AuditIdentityResolverTest {
   public void setUp()
       throws Exception {
     MockitoAnnotations.openMocks(this);
+    MockAuditTokenResolver.reset();
     _auditIdentityResolver = new AuditIdentityResolver(_auditConfigManager);
 
     _auditConfig = new AuditConfig();
     when(_auditConfigManager.getCurrentConfig()).thenReturn(_auditConfig);
   }
 
+  // ==================== Custom Header Tests ====================
+
   @Test
-  public void testResolveIdentityFromCustomHeaderSuccess() {
+  public void testResolveIdentityFromCustomHeader() {
     _auditConfig.setUseridHeader("X-User-Email");
     
when(_requestContext.getHeaderString("X-User-Email")).thenReturn("[email protected]");
 
@@ -70,13 +74,12 @@ public class AuditIdentityResolverTest {
     assertThat(result.getPrincipal()).isEqualTo("[email protected]");
   }
 
+  // ==================== JWT Tests ====================
+
   @Test
-  public void testResolveIdentityFromCustomHeaderEmptyHeaderValue()
+  public void testResolveIdentityFromJwtCustomClaim()
       throws Exception {
-    _auditConfig.setUseridHeader("X-User-Email");
     _auditConfig.setUseridJwtClaimName("email");
-
-    when(_requestContext.getHeaderString("X-User-Email")).thenReturn("");
     String validJwt = createJwtToken("user123", Map.of("email", 
"[email protected]"));
     
when(_requestContext.getHeaderString(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer
 " + validJwt);
 
@@ -87,157 +90,143 @@ public class AuditIdentityResolverTest {
   }
 
   @Test
-  public void testResolveIdentityFromCustomHeaderMissingHeader()
+  public void testResolveIdentityFromJwtSubjectWhenClaimMissing()
       throws Exception {
-    _auditConfig.setUseridHeader("X-User-Email");
     _auditConfig.setUseridJwtClaimName("email");
-
-    when(_requestContext.getHeaderString("X-User-Email")).thenReturn(null);
-    String validJwt = createJwtToken("user123", Map.of("email", 
"[email protected]"));
+    String validJwt = createJwtToken("user123", new HashMap<>());
     
when(_requestContext.getHeaderString(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer
 " + validJwt);
 
     AuditEvent.UserIdentity result = 
_auditIdentityResolver.resolveIdentity(_requestContext);
 
     assertThat(result).isNotNull();
-    assertThat(result.getPrincipal()).isEqualTo("[email protected]");
+    assertThat(result.getPrincipal()).isEqualTo("user123");
   }
 
   @Test
-  public void testResolveIdentityFromJwtCustomClaimSuccess()
-      throws Exception {
-    _auditConfig.setUseridJwtClaimName("email");
-
-    String validJwt = createJwtToken("user123", Map.of("email", 
"[email protected]"));
-    
when(_requestContext.getHeaderString(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer
 " + validJwt);
+  public void testInvalidJwtTokenReturnsNull() {
+    
when(_requestContext.getHeaderString(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer
 invalid-token");
 
     AuditEvent.UserIdentity result = 
_auditIdentityResolver.resolveIdentity(_requestContext);
 
-    assertThat(result).isNotNull();
-    assertThat(result.getPrincipal()).isEqualTo("[email protected]");
+    assertThat(result).isNull();
   }
 
+  // ==================== Custom Resolver Tests ====================
+
   @Test
-  public void testResolveIdentityFromJwtSubjectWhenCustomClaimNotConfigured()
-      throws Exception {
-    String validJwt = createJwtToken("user123", new HashMap<>());
-    
when(_requestContext.getHeaderString(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer
 " + validJwt);
+  public void testCustomTokenResolverSuccess() {
+    AuditTokenResolver mockResolver = new 
MockAuditTokenResolver("resolved-user");
+    AuditIdentityResolver resolver = new 
AuditIdentityResolver(_auditConfigManager, mockResolver);
+    
when(_requestContext.getHeaderString(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer
 custom-token");
 
-    AuditEvent.UserIdentity result = 
_auditIdentityResolver.resolveIdentity(_requestContext);
+    AuditEvent.UserIdentity result = resolver.resolveIdentity(_requestContext);
 
     assertThat(result).isNotNull();
-    assertThat(result.getPrincipal()).isEqualTo("user123");
+    assertThat(result.getPrincipal()).isEqualTo("resolved-user");
   }
 
   @Test
-  public void testResolveIdentityFromJwtSubjectWhenCustomClaimMissing()
+  public void testCustomResolverReturnsNullFallsBackToJwt()
       throws Exception {
-    _auditConfig.setUseridJwtClaimName("email");
-
-    String validJwt = createJwtToken("user123", new HashMap<>());
+    AuditTokenResolver mockResolver = new MockAuditTokenResolver(null);
+    AuditIdentityResolver resolver = new 
AuditIdentityResolver(_auditConfigManager, mockResolver);
+    String validJwt = createJwtToken("jwt-user", new HashMap<>());
     
when(_requestContext.getHeaderString(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer
 " + validJwt);
 
-    AuditEvent.UserIdentity result = 
_auditIdentityResolver.resolveIdentity(_requestContext);
+    AuditEvent.UserIdentity result = resolver.resolveIdentity(_requestContext);
 
     assertThat(result).isNotNull();
-    assertThat(result.getPrincipal()).isEqualTo("user123");
+    assertThat(result.getPrincipal()).isEqualTo("jwt-user");
   }
 
   @Test
-  public void testInvalidJwtToken() {
-    
when(_requestContext.getHeaderString(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer
 invalid-token");
+  public void testCustomResolverReceivesFullAuthHeader() {
+    String expectedAuthHeader = "Bearer custom-token-value";
+    MockAuditTokenResolver mockResolver = new MockAuditTokenResolver("user");
+    AuditIdentityResolver resolver = new 
AuditIdentityResolver(_auditConfigManager, mockResolver);
+    
when(_requestContext.getHeaderString(HttpHeaders.AUTHORIZATION)).thenReturn(expectedAuthHeader);
 
-    AuditEvent.UserIdentity result = 
_auditIdentityResolver.resolveIdentity(_requestContext);
+    resolver.resolveIdentity(_requestContext);
 
-    assertThat(result).isNull();
+    
assertThat(MockAuditTokenResolver.getLastAuthHeader()).isEqualTo(expectedAuthHeader);
   }
 
-  @Test
-  public void testHeaderTakesPriorityOverJwt()
-      throws Exception {
-    _auditConfig.setUseridHeader("X-User-Email");
-    _auditConfig.setUseridJwtClaimName("email");
+  // ==================== PluginManager Tests ====================
 
-    
when(_requestContext.getHeaderString("X-User-Email")).thenReturn("[email protected]");
-    String validJwt = createJwtToken("user123", Map.of("email", 
"[email protected]"));
-    
when(_requestContext.getHeaderString(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer
 " + validJwt);
+  @Test
+  public void testResolverLoadedViaPluginManager() {
+    _auditConfig.setTokenResolverClass(MockAuditTokenResolver.class.getName());
+    
when(_requestContext.getHeaderString(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer
 test-token");
 
     AuditEvent.UserIdentity result = 
_auditIdentityResolver.resolveIdentity(_requestContext);
 
     assertThat(result).isNotNull();
-    assertThat(result.getPrincipal()).isEqualTo("[email protected]");
+    assertThat(result.getPrincipal()).isEqualTo("mock-resolved-user");
   }
 
   @Test
-  public void testFallbackChainNoHeaderFallsBackToJwtClaim()
+  public void testInvalidResolverClassFallsBackToJwt()
       throws Exception {
-    _auditConfig.setUseridHeader("X-User-Email");
-    _auditConfig.setUseridJwtClaimName("email");
-
-    when(_requestContext.getHeaderString("X-User-Email")).thenReturn(null);
-    String validJwt = createJwtToken("user123", Map.of("email", 
"[email protected]"));
+    _auditConfig.setTokenResolverClass("com.invalid.NonExistentResolver");
+    String validJwt = createJwtToken("jwt-user", new HashMap<>());
     
when(_requestContext.getHeaderString(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer
 " + validJwt);
 
     AuditEvent.UserIdentity result = 
_auditIdentityResolver.resolveIdentity(_requestContext);
 
     assertThat(result).isNotNull();
-    assertThat(result.getPrincipal()).isEqualTo("[email protected]");
+    assertThat(result.getPrincipal()).isEqualTo("jwt-user");
   }
 
+  // ==================== Priority Tests ====================
+
   @Test
-  public void testFallbackChainNoHeaderNoClaimFallsBackToSubject()
+  public void testPriorityHeaderOverJwt()
       throws Exception {
     _auditConfig.setUseridHeader("X-User-Email");
-    _auditConfig.setUseridJwtClaimName("email");
-
-    when(_requestContext.getHeaderString("X-User-Email")).thenReturn(null);
-    String validJwt = createJwtToken("user123", new HashMap<>());
+    
when(_requestContext.getHeaderString("X-User-Email")).thenReturn("header-user");
+    String validJwt = createJwtToken("jwt-user", new HashMap<>());
     
when(_requestContext.getHeaderString(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer
 " + validJwt);
 
     AuditEvent.UserIdentity result = 
_auditIdentityResolver.resolveIdentity(_requestContext);
 
     assertThat(result).isNotNull();
-    assertThat(result.getPrincipal()).isEqualTo("user123");
+    assertThat(result.getPrincipal()).isEqualTo("header-user");
   }
 
   @Test
-  public void testNoAuthenticationPresentReturnsNull() {
-    AuditEvent.UserIdentity result = 
_auditIdentityResolver.resolveIdentity(_requestContext);
-
-    assertThat(result).isNull();
-  }
-
-  @Test
-  public void testBlankConfigurationUsesDefaults()
-      throws Exception {
-    String validJwt = createJwtToken("user123", new HashMap<>());
-    
when(_requestContext.getHeaderString(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer
 " + validJwt);
+  public void testPriorityHeaderOverResolver() {
+    AuditTokenResolver mockResolver = new 
MockAuditTokenResolver("resolver-user");
+    AuditIdentityResolver resolver = new 
AuditIdentityResolver(_auditConfigManager, mockResolver);
+    _auditConfig.setUseridHeader("X-User-Email");
+    
when(_requestContext.getHeaderString("X-User-Email")).thenReturn("header-user");
+    
when(_requestContext.getHeaderString(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer
 token");
 
-    AuditEvent.UserIdentity result = 
_auditIdentityResolver.resolveIdentity(_requestContext);
+    AuditEvent.UserIdentity result = resolver.resolveIdentity(_requestContext);
 
     assertThat(result).isNotNull();
-    assertThat(result.getPrincipal()).isEqualTo("user123");
+    assertThat(result.getPrincipal()).isEqualTo("header-user");
   }
 
-  @Test
-  public void testNonBearerAuthorizationHeaderReturnsNull() {
-    
when(_requestContext.getHeaderString(HttpHeaders.AUTHORIZATION)).thenReturn("Basic
 dXNlcjpwYXNz");
+  // ==================== Edge Cases ====================
 
+  @Test
+  public void testNoAuthenticationReturnsNull() {
     AuditEvent.UserIdentity result = 
_auditIdentityResolver.resolveIdentity(_requestContext);
 
     assertThat(result).isNull();
   }
 
   @Test
-  public void testWhitespaceInHeaderValueTrimmed() {
-    _auditConfig.setUseridHeader("X-User-Email");
-    when(_requestContext.getHeaderString("X-User-Email")).thenReturn("  
[email protected]  ");
+  public void testNonBearerAuthorizationReturnsNull() {
+    
when(_requestContext.getHeaderString(HttpHeaders.AUTHORIZATION)).thenReturn("Basic
 dXNlcjpwYXNz");
 
     AuditEvent.UserIdentity result = 
_auditIdentityResolver.resolveIdentity(_requestContext);
 
-    assertThat(result).isNotNull();
-    assertThat(result.getPrincipal()).isEqualTo("  [email protected]  ");
+    assertThat(result).isNull();
   }
 
+  // ==================== Helper Methods ====================
+
   private String createJwtToken(String subject, Map<String, Object> 
customClaims)
       throws Exception {
     JWTClaimsSet.Builder claimsBuilder = new 
JWTClaimsSet.Builder().subject(subject)
diff --git 
a/pinot-common/src/test/java/org/apache/pinot/common/audit/MockAuditTokenResolver.java
 
b/pinot-common/src/test/java/org/apache/pinot/common/audit/MockAuditTokenResolver.java
new file mode 100644
index 00000000000..19e6943576b
--- /dev/null
+++ 
b/pinot-common/src/test/java/org/apache/pinot/common/audit/MockAuditTokenResolver.java
@@ -0,0 +1,93 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.common.audit;
+
+import javax.annotation.Nullable;
+import org.apache.pinot.spi.audit.AuditTokenResolver;
+import org.apache.pinot.spi.audit.AuditUserIdentity;
+
+
+/**
+ * Mock implementation of AuditTokenResolver for unit testing.
+ * <p>
+ * Supports two modes:
+ * <ul>
+ *   <li>Direct instantiation with configurable return value</li>
+ *   <li>PluginManager loading (no-arg constructor) with static 
configuration</li>
+ * </ul>
+ */
+public class MockAuditTokenResolver implements AuditTokenResolver {
+
+  private static final String TEST_PREFIX = "Bearer test-";
+  private static final String DEFAULT_PRINCIPAL = "mock-resolved-user";
+
+  private static String _staticReturnValue = DEFAULT_PRINCIPAL;
+  private static String _lastAuthHeader;
+
+  @Nullable
+  private final String _returnValue;
+
+  /**
+   * No-arg constructor for PluginManager loading.
+   */
+  public MockAuditTokenResolver() {
+    _returnValue = null;
+  }
+
+  /**
+   * Constructor for direct instantiation with configurable return value.
+   */
+  public MockAuditTokenResolver(@Nullable String returnValue) {
+    _returnValue = returnValue;
+  }
+
+  @Override
+  @Nullable
+  public AuditUserIdentity resolve(String authHeaderValue) {
+    _lastAuthHeader = authHeaderValue;
+
+    // If instantiated with a specific return value, use it
+    if (_returnValue != null) {
+      return () -> _returnValue;
+    }
+
+    // For PluginManager-loaded instances, check prefix and use static config
+    if (authHeaderValue.startsWith(TEST_PREFIX)) {
+      String principal = _staticReturnValue;
+      return () -> principal;
+    }
+    return null;
+  }
+
+  /**
+   * Resets static state to defaults.
+   */
+  public static void reset() {
+    _staticReturnValue = DEFAULT_PRINCIPAL;
+    _lastAuthHeader = null;
+  }
+
+  /**
+   * Returns the last auth header value passed to resolve().
+   */
+  @Nullable
+  public static String getLastAuthHeader() {
+    return _lastAuthHeader;
+  }
+}
diff --git 
a/pinot-spi/src/main/java/org/apache/pinot/spi/audit/AuditTokenResolver.java 
b/pinot-spi/src/main/java/org/apache/pinot/spi/audit/AuditTokenResolver.java
new file mode 100644
index 00000000000..0a3b788aa21
--- /dev/null
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/audit/AuditTokenResolver.java
@@ -0,0 +1,51 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.spi.audit;
+
+import javax.annotation.Nullable;
+
+
+/**
+ * Service Provider Interface for resolving user identity from authentication 
tokens
+ * for audit logging purposes.
+ * <p>
+ * Implementations can handle custom token formats (e.g., proprietary tokens, 
API keys)
+ * that are not standard JWTs. The resolver receives the full Authorization 
header value
+ * and returns the principal if it can handle the token format.
+ * <p>
+ * If the resolver cannot handle the token format, it should return null to 
allow
+ * fallback to the default JWT-based resolution.
+ */
+public interface AuditTokenResolver {
+
+  /**
+   * Resolves the user identity from an authorization header value.
+   * <p>
+   * The implementation should:
+   * <ul>
+   *   <li>Return an {@link AuditUserIdentity} if it can handle the token 
format</li>
+   *   <li>Return null if it cannot handle the token format (to allow 
fallback)</li>
+   * </ul>
+   *
+   * @param authHeaderValue the full Authorization header value (e.g., "Bearer 
&lt;token&gt;")
+   * @return the resolved identity, or null if this resolver cannot handle the 
token
+   */
+  @Nullable
+  AuditUserIdentity resolve(String authHeaderValue);
+}
diff --git 
a/pinot-spi/src/main/java/org/apache/pinot/spi/audit/AuditUserIdentity.java 
b/pinot-spi/src/main/java/org/apache/pinot/spi/audit/AuditUserIdentity.java
new file mode 100644
index 00000000000..e4d6efd88ff
--- /dev/null
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/audit/AuditUserIdentity.java
@@ -0,0 +1,41 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.spi.audit;
+
+import javax.annotation.Nullable;
+
+
+/**
+ * Represents a resolved user identity for audit logging purposes.
+ * <p>
+ * This interface allows {@link AuditTokenResolver} implementations to return
+ * structured identity information that can be extended in the future
+ * (e.g., roles, groups) without breaking the SPI contract.
+ */
+@FunctionalInterface
+public interface AuditUserIdentity {
+
+  /**
+   * Returns the principal (user identifier) for this identity.
+   *
+   * @return the principal, or {@code null} if not available
+   */
+  @Nullable
+  String getPrincipal();
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to