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

tomsun28 pushed a commit to branch 2.0.0
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git

commit 62df445dbae5d5af529fa57511dfecb0cb9d3f70
Merge: 288dcab3ad aafa8cc2e7
Author: tomsun28 <[email protected]>
AuthorDate: Fri Oct 9 18:41:54 2026 +0800

    Merge remote-tracking branch 'origin/master' into 2.0.0
    
    # Conflicts:
    #       
hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ApiTokenValidationFilter.java
    #       
hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/ApiTokenValidationFilterTest.java

 .github/workflows/backend-build-test.yml           |  15 +-
 .github/workflows/docker-compose-config-test.yml   |   6 +-
 e2e/README.md                                      |   3 +
 .../org/apache/hertzbeat/common/util/XmlUtil.java  |   2 +-
 .../apache/hertzbeat/common/util/XmlUtilTest.java  |  83 +++++++
 .../manager/config/ApiTokenValidationFilter.java   |  90 +++++---
 .../config/ApiTokenValidationFilterTest.java       | 250 +++++++++++++--------
 .../src/main/resources/application.yml             |   1 -
 home/docs/start/docker-compose-deploy.md           |   2 +
 home/docs/start/docker-deploy.md                   |  17 ++
 home/docs/start/quickstart.md                      |   2 +-
 .../current/help/tdengine_promql.md                |   2 +-
 .../current/start/docker-compose-deploy.md         |   2 +
 .../current/start/docker-deploy.md                 |  17 ++
 .../current/start/quickstart.md                    |   2 +-
 .../current/start/usecase/tdengine-practice.md     |   2 +-
 script/application.yml                             |   1 -
 script/ci/auth_contract_test.py                    | 198 ++++++++++++++++
 script/ci/check-quickstart-compose.sh              |  23 +-
 script/docker-compose/README.md                    |  10 +
 .../hertzbeat-mysql-iotdb/conf/application.yml     |   1 -
 .../hertzbeat-mysql-tdengine/conf/application.yml  |   1 -
 .../conf/application.yml                           |   1 -
 .../conf/application.yml                           |   1 -
 .../conf/application.yml                           |   1 -
 25 files changed, 592 insertions(+), 141 deletions(-)

diff --cc 
hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ApiTokenValidationFilter.java
index 3651562ab6,0da8c65197..2e39090b8e
--- 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ApiTokenValidationFilter.java
+++ 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ApiTokenValidationFilter.java
@@@ -25,19 -27,19 +27,23 @@@ import jakarta.servlet.http.HttpServlet
  
  import java.io.IOException;
  import java.io.PrintWriter;
+ import java.util.ArrayList;
  import java.util.Collections;
+ import java.util.LinkedHashSet;
  import java.util.List;
  import java.util.Map;
+ import java.util.Set;
  
  import lombok.extern.slf4j.Slf4j;
 +import org.apache.commons.lang3.StringUtils;
  import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
  import org.apache.hertzbeat.common.constants.NetworkConstants;
 +import 
org.apache.hertzbeat.common.observability.gateway.AuthTokenRequestContext;
 +import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
 +import 
org.apache.hertzbeat.common.observability.gateway.ObservabilityAccessTokenGateway;
  import org.apache.hertzbeat.common.util.JsonUtil;
  import org.apache.hertzbeat.manager.service.AccountService;
+ import org.apache.hertzbeat.manager.service.impl.AccountServiceImpl;
  import org.jspecify.annotations.NonNull;
  import org.springframework.http.HttpStatus;
  import org.springframework.stereotype.Component;
@@@ -66,7 -68,8 +72,9 @@@ public class ApiTokenValidationFilter i
  
      private static final String ROLES_CLAIM = "roles";
      private static final String TOKEN_VALIDATION_UNAVAILABLE = "Token 
validation unavailable";
 +    private static final String WORKSPACE_ACCESS_DENIED = "Workspace access 
denied";
+     private static final String TOKEN_NOT_RESOLVED = "Token could not be 
resolved";
+     private static final String TOKEN_PARAM = "token";
  
      private final AccountService accountService;
  
@@@ -77,53 -80,60 +85,79 @@@
      @Override
      public boolean preHandle(@NonNull HttpServletRequest request, @NonNull 
HttpServletResponse response, @NonNull Object handler)
          throws IOException {
 +        AuthTokenRequestContext.clear();
          SubjectSum subject = SurenessContextHolder.getBindSubject();
 +        bindAuthenticatedWorkspace(subject);
 +        String authenticatedWorkspaceId = 
AuthTokenRequestContext.currentAuthenticatedWorkspaceId();
          if (subject == null || !isManagedToken(subject)) {
 -            return true;
 +            return bindAuthorizedWorkspace(request, authenticatedWorkspaceId)
 +                    || writeWorkspaceError(response);
          }
-         String authorization = 
request.getHeader(NetworkConstants.AUTHORIZATION);
- 
-         if (authorization != null && 
authorization.startsWith(DispatchConstants.BEARER)) {
-             String token = 
authorization.substring(DispatchConstants.BEARER.length()).trim();
-             if (!token.isEmpty()) {
-                 try {
-                     String rejectReason = checkManagedToken(
-                             subject,
-                             token,
-                             resolveRequiredScope(request),
-                             authenticatedWorkspaceId);
-                     if (rejectReason != null) {
-                         return writeError(response, HttpStatus.UNAUTHORIZED, 
rejectReason);
-                     }
-                     if (!bindAuthorizedWorkspace(request, 
authenticatedWorkspaceId)) {
-                         return writeWorkspaceError(response);
-                     }
-                     rejectReason = bindManagedCollectorBoundary(request, 
subject);
-                     if (rejectReason != null) {
-                         return writeError(response, HttpStatus.UNAUTHORIZED, 
rejectReason);
-                     }
-                     touchTokenLastUsedTime(token);
-                 } catch (RuntimeException e) {
-                     log.warn("Managed token validation failed ({})", 
e.getClass().getSimpleName());
-                     return writeError(response, 
HttpStatus.SERVICE_UNAVAILABLE, TOKEN_VALIDATION_UNAVAILABLE);
+         // Sureness reads a jwt from the Authorization header and from the 
token query parameter,
+         // so every managed token the request carries is validated, and a 
managed subject whose
+         // token cannot be found is rejected rather than let through unchecked
+         List<String> managedTokens = resolveManagedTokens(request);
+         if (managedTokens.isEmpty()) {
+             return writeError(response, HttpStatus.UNAUTHORIZED, 
TOKEN_NOT_RESOLVED);
+         }
+         try {
+             for (String token : managedTokens) {
 -                String rejectReason = checkManagedToken(subject, token);
++                String rejectReason = checkManagedToken(
++                        subject, token, resolveRequiredScope(request), 
authenticatedWorkspaceId);
+                 if (rejectReason != null) {
+                     return writeError(response, HttpStatus.UNAUTHORIZED, 
rejectReason);
                  }
              }
+         } catch (RuntimeException e) {
+             log.warn("Managed token validation failed", e);
+             return writeError(response, HttpStatus.SERVICE_UNAVAILABLE, 
TOKEN_VALIDATION_UNAVAILABLE);
          }
- 
-         return bindAuthorizedWorkspace(request, authenticatedWorkspaceId)
-                 || writeWorkspaceError(response);
++        if (!bindAuthorizedWorkspace(request, authenticatedWorkspaceId)) {
++            return writeWorkspaceError(response);
++        }
++        String rejectReason = bindManagedCollectorBoundary(request, subject);
++        if (rejectReason != null) {
++            return writeError(response, HttpStatus.UNAUTHORIZED, 
rejectReason);
++        }
++        managedTokens.forEach(this::touchTokenLastUsedTime);
+         return true;
      }
  
 +    @Override
 +    public void afterCompletion(@NonNull HttpServletRequest request, @NonNull 
HttpServletResponse response,
 +                                @NonNull Object handler, Exception ex) {
 +        AuthTokenRequestContext.clear();
 +    }
 +
+     private List<String> resolveManagedTokens(HttpServletRequest request) {
+         Set<String> candidates = new LinkedHashSet<>(2);
+         String authorization = 
request.getHeader(NetworkConstants.AUTHORIZATION);
+         if (authorization != null && 
authorization.startsWith(DispatchConstants.BEARER)) {
+             // mirror Sureness JwtSubjectJakartaServletCreator exactly, so 
the validated token is the one it authenticated
+             candidates.add(authorization.replace(DispatchConstants.BEARER, 
"").trim());
+         }
+         String queryToken = request.getParameter(TOKEN_PARAM);
+         if (queryToken != null) {
+             candidates.add(queryToken.trim());
+         }
+         List<String> managedTokens = new ArrayList<>(candidates.size());
+         for (String candidate : candidates) {
+             if (!candidate.isEmpty() && isManagedJwt(candidate)) {
+                 managedTokens.add(candidate);
+             }
+         }
+         return managedTokens;
+     }
+ 
+     private boolean isManagedJwt(String token) {
+         try {
+             Claims claims = JsonWebTokenUtil.parseJwt(token);
+             return 
Boolean.TRUE.equals(claims.get(AccountServiceImpl.CLAIM_MANAGED, 
Boolean.class));
+         } catch (Exception e) {
+             return false;
+         }
+     }
+ 
      /**
       * Check if a managed token should be rejected.
       * <p>
diff --cc 
hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/ApiTokenValidationFilterTest.java
index 88e53c78c5,042d966725..c4773c8998
--- 
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/ApiTokenValidationFilterTest.java
+++ 
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/ApiTokenValidationFilterTest.java
@@@ -17,9 -17,11 +17,12 @@@
  
  package org.apache.hertzbeat.manager.config;
  
+ import static org.junit.jupiter.api.Assertions.assertFalse;
+ import static org.junit.jupiter.api.Assertions.assertTrue;
  import static org.mockito.ArgumentMatchers.any;
  import static org.mockito.Mockito.doNothing;
 +import static org.mockito.Mockito.lenient;
+ import static org.mockito.Mockito.mockStatic;
  import static org.mockito.Mockito.never;
  import static org.mockito.Mockito.verify;
  import static org.mockito.Mockito.when;
@@@ -32,19 -34,17 +35,23 @@@ import jakarta.servlet.http.HttpServlet
  import jakarta.servlet.http.HttpServletResponse;
  import java.io.PrintWriter;
  import java.io.StringWriter;
+ import java.util.HashMap;
  import java.util.List;
+ import java.util.Map;
  import org.apache.hertzbeat.common.constants.NetworkConstants;
 +import 
org.apache.hertzbeat.common.observability.gateway.AuthTokenRequestContext;
 +import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
 +import 
org.apache.hertzbeat.common.observability.gateway.ObservabilityAccessTokenGateway;
  import org.apache.hertzbeat.manager.service.AccountService;
 +import org.junit.jupiter.api.AfterEach;
+ import org.junit.jupiter.api.BeforeAll;
  import org.junit.jupiter.api.BeforeEach;
  import org.junit.jupiter.api.Test;
  import org.junit.jupiter.api.extension.ExtendWith;
 +import org.junit.jupiter.params.ParameterizedTest;
 +import org.junit.jupiter.params.provider.ValueSource;
  import org.mockito.Mock;
+ import org.mockito.MockedStatic;
  import org.mockito.junit.jupiter.MockitoExtension;
  
  /**
@@@ -112,362 -126,130 +145,392 @@@ class ApiTokenValidationFilterTest 
      void testLegacyTokenPassesThrough() throws Exception {
          SubjectSum subject = mockSubject();
  
-         try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
-             
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
- 
-             
org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response, 
new Object()));
-             verify(accountService, never()).checkTokenStatus(any());
-         }
+         assertTrue(preHandle(subject));
+         verify(accountService, never()).checkTokenStatus(any());
      }
  
 +    @Test
 +    void testUiSessionRejectsWorkspaceOverrideWithoutManagedValidation() 
throws Exception {
 +        SubjectSum subject = mockSubject();
 +        
when(principalMap.getPrincipal(AuthTokenScopes.CLAIM_WORKSPACE_ID)).thenReturn("team-a");
 +        when(request.getParameter("workspaceId")).thenReturn("team-b");
 +        when(response.getWriter()).thenReturn(new PrintWriter(new 
StringWriter()));
 +
 +        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
 +            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
 +
 +            
org.junit.jupiter.api.Assertions.assertFalse(filter.preHandle(request, 
response, new Object()));
 +            verify(response).setStatus(403);
 +            
org.junit.jupiter.api.Assertions.assertNull(AuthTokenRequestContext.currentWorkspaceId());
 +            
org.junit.jupiter.api.Assertions.assertNull(AuthTokenRequestContext.currentAuthenticatedWorkspaceId());
 +            verify(accountService, never()).checkTokenStatus(any());
 +        }
 +    }
 +
 +    @Test
 +    void testMissingSubjectWorkspaceRejectsRequestedWorkspace() throws 
Exception {
 +        SubjectSum subject = mockSubject();
 +        
when(request.getHeader(AuthTokenScopes.WORKSPACE_ID_HEADER)).thenReturn("team-b");
 +        when(response.getWriter()).thenReturn(new PrintWriter(new 
StringWriter()));
 +
 +        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
 +            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
 +
 +            
org.junit.jupiter.api.Assertions.assertFalse(filter.preHandle(request, 
response, new Object()));
 +            verify(response).setStatus(403);
 +            
org.junit.jupiter.api.Assertions.assertNull(AuthTokenRequestContext.currentWorkspaceId());
 +            
org.junit.jupiter.api.Assertions.assertNull(AuthTokenRequestContext.currentAuthenticatedWorkspaceId());
 +        }
 +    }
 +
 +    @Test
 +    void testUiSessionAllowsMatchingRequestedWorkspace() throws Exception {
 +        SubjectSum subject = mockSubject();
 +        
when(principalMap.getPrincipal(AuthTokenScopes.CLAIM_WORKSPACE_ID)).thenReturn("team-a");
 +        
when(request.getHeader(AuthTokenScopes.WORKSPACE_ID_HEADER)).thenReturn(" 
team-a ");
 +
 +        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
 +            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
 +
 +            
org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response, 
new Object()));
 +            org.junit.jupiter.api.Assertions.assertEquals("team-a", 
AuthTokenRequestContext.currentWorkspaceId());
 +            org.junit.jupiter.api.Assertions.assertEquals(
 +                    "team-a", 
AuthTokenRequestContext.currentAuthenticatedWorkspaceId());
 +        }
 +    }
 +
 +    @Test
 +    void testUiSessionRejectsSnakeCaseWorkspaceOverride() throws Exception {
 +        SubjectSum subject = mockSubject();
 +        
when(principalMap.getPrincipal(AuthTokenScopes.CLAIM_WORKSPACE_ID)).thenReturn("team-a");
 +        when(request.getParameter("workspace_id")).thenReturn("team-b");
 +        when(response.getWriter()).thenReturn(new PrintWriter(new 
StringWriter()));
 +
 +        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
 +            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
 +
 +            
org.junit.jupiter.api.Assertions.assertFalse(filter.preHandle(request, 
response, new Object()));
 +            verify(response).setStatus(403);
 +            
org.junit.jupiter.api.Assertions.assertNull(AuthTokenRequestContext.currentWorkspaceId());
 +            
org.junit.jupiter.api.Assertions.assertNull(AuthTokenRequestContext.currentAuthenticatedWorkspaceId());
 +        }
 +    }
 +
      @Test
      void testManagedTokenActivePassesThrough() throws Exception {
-         String managedToken = "managed-token";
          
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + 
managedToken);
 -        when(accountService.checkTokenStatus(managedToken)).thenReturn(null);
 -        when(accountService.checkManagedTokenAccess("admin", 
List.of("admin"))).thenReturn(null);
 +        when(request.getMethod()).thenReturn("POST");
 +        when(request.getRequestURI()).thenReturn("/api/monitor");
 +        when(accountService.checkTokenStatus(
 +                managedToken, AuthTokenScopes.API_ADMIN, 
AuthTokenScopes.DEFAULT_WORKSPACE_ID)).thenReturn(null);
 +        when(accountService.checkManagedTokenAccess("admin", 
List.of("admin"), 7L)).thenReturn(null);
          doNothing().when(accountService).touchTokenLastUsedTime(managedToken);
          SubjectSum subject = mockManagedSubjectWithClaims();
 +        
when(principalMap.getPrincipal(ObservabilityAccessTokenGateway.CLAIM_CREDENTIAL_VERSION)).thenReturn(7L);
  
 -        assertTrue(preHandle(subject));
 -        verify(accountService).checkTokenStatus(managedToken);
 -        verify(accountService).checkManagedTokenAccess("admin", 
List.of("admin"));
 -        verify(accountService).touchTokenLastUsedTime(managedToken);
 +        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
 +            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
 +
 +            
org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response, 
new Object()));
 +            verify(accountService).checkTokenStatus(
 +                    managedToken, AuthTokenScopes.API_ADMIN, 
AuthTokenScopes.DEFAULT_WORKSPACE_ID);
 +            verify(accountService).checkManagedTokenAccess("admin", 
List.of("admin"), 7L);
 +            verify(accountService).touchTokenLastUsedTime(managedToken);
 +        }
 +    }
 +
 +    @ParameterizedTest
 +    @ValueSource(strings = {"POST", "PUT"})
 +    void monitorManageWritesRequireApiAdminScopeExactlyOnce(String method) 
throws Exception {
-         String managedToken = "managed-token";
 +        
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + 
managedToken);
 +        when(request.getMethod()).thenReturn(method);
 +        when(request.getRequestURI()).thenReturn("/api/monitors/manage");
 +        when(accountService.checkTokenStatus(
 +                managedToken, AuthTokenScopes.API_ADMIN, 
AuthTokenScopes.DEFAULT_WORKSPACE_ID)).thenReturn(null);
 +        when(accountService.checkManagedTokenAccess("admin", 
List.of("admin"), null)).thenReturn(null);
 +        doNothing().when(accountService).touchTokenLastUsedTime(managedToken);
 +        SubjectSum subject = mockManagedSubjectWithClaims();
 +
 +        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
 +            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
 +
 +            
org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response, 
new Object()));
 +            verify(accountService).checkTokenStatus(
 +                    managedToken, AuthTokenScopes.API_ADMIN, 
AuthTokenScopes.DEFAULT_WORKSPACE_ID);
 +            verify(accountService).checkManagedTokenAccess("admin", 
List.of("admin"), null);
 +            verify(accountService).touchTokenLastUsedTime(managedToken);
 +        }
 +    }
 +
 +    @Test
 +    void testManagedTokenReadRequestRequiresReadonlyScope() throws Exception {
-         String managedToken = "managed-token";
 +        
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + 
managedToken);
 +        when(request.getMethod()).thenReturn("GET");
 +        when(request.getRequestURI()).thenReturn("/api/monitor");
 +        when(accountService.checkTokenStatus(
 +                managedToken, AuthTokenScopes.READONLY_QUERY, 
AuthTokenScopes.DEFAULT_WORKSPACE_ID)).thenReturn(null);
 +        when(accountService.checkManagedTokenAccess("admin", 
List.of("admin"), null)).thenReturn(null);
 +        doNothing().when(accountService).touchTokenLastUsedTime(managedToken);
 +        SubjectSum subject = mockManagedSubjectWithClaims();
 +
 +        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
 +            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
 +
 +            
org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response, 
new Object()));
 +            verify(accountService).checkTokenStatus(
 +                    managedToken, AuthTokenScopes.READONLY_QUERY, 
AuthTokenScopes.DEFAULT_WORKSPACE_ID);
 +        }
 +    }
 +
 +    @ParameterizedTest
 +    @ValueSource(strings = {"/api/logs/analysis/compare", 
"/api/logs/analysis/compare/other", "/api/monitors/manage"})
 +    void comparisonPostAloneUsesReadOnlyScope(String uri) throws Exception {
 +        String required = "/api/logs/analysis/compare".equals(uri) ? 
AuthTokenScopes.READONLY_QUERY : AuthTokenScopes.API_ADMIN;
-         String managedToken = "managed-token";
 +        
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + 
managedToken);
 +        when(request.getMethod()).thenReturn("POST");
 +        when(request.getRequestURI()).thenReturn(uri);
 +        when(accountService.checkTokenStatus(
 +                managedToken, required, 
AuthTokenScopes.DEFAULT_WORKSPACE_ID)).thenReturn(null);
 +        when(accountService.checkManagedTokenAccess("admin", 
List.of("admin"), null)).thenReturn(null);
 +        doNothing().when(accountService).touchTokenLastUsedTime(managedToken);
 +        SubjectSum subject = mockManagedSubjectWithClaims();
 +
 +        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
 +            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
 +
 +            
org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response, 
new Object()));
 +            verify(accountService).checkTokenStatus(
 +                    managedToken, required, 
AuthTokenScopes.DEFAULT_WORKSPACE_ID);
 +        }
 +    }
 +
 +    @Test
 +    void testManagedTokenOtlpRequestRequiresIngestScope() throws Exception {
-         String managedToken = "managed-token";
 +        
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + 
managedToken);
 +        when(request.getRequestURI()).thenReturn("/api/otlp/v1/metrics");
 +        when(accountService.checkTokenStatus(
 +                managedToken, AuthTokenScopes.OTLP_INGEST, 
AuthTokenScopes.DEFAULT_WORKSPACE_ID)).thenReturn(null);
 +        when(accountService.checkManagedTokenAccess("admin", 
List.of("admin"), null)).thenReturn(null);
 +        doNothing().when(accountService).touchTokenLastUsedTime(managedToken);
 +        SubjectSum subject = mockManagedSubjectWithClaims();
 +
 +        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
 +            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
 +
 +            
org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response, 
new Object()));
 +            verify(accountService).checkTokenStatus(
 +                    managedToken, AuthTokenScopes.OTLP_INGEST, 
AuthTokenScopes.DEFAULT_WORKSPACE_ID);
 +            
org.junit.jupiter.api.Assertions.assertNull(AuthTokenRequestContext.currentCollectorId());
 +        }
 +    }
 +
 +    @Test
 +    void testManagedCollectorTokenBindsIdentityForAllowedSignal() throws 
Exception {
-         String managedToken = "collector-token";
 +        
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + 
managedToken);
 +        when(request.getRequestURI()).thenReturn("/api/otlp/v1/metrics");
 +        when(accountService.checkTokenStatus(
 +                managedToken, AuthTokenScopes.OTLP_INGEST, 
AuthTokenScopes.DEFAULT_WORKSPACE_ID)).thenReturn(null);
 +        when(accountService.checkManagedTokenAccess("admin", 
List.of("admin"), null)).thenReturn(null);
 +        SubjectSum subject = mockManagedSubjectWithClaims();
 +        when(principalMap.getPrincipal(AuthTokenScopes.CLAIM_TOKEN_AUDIENCE))
 +                .thenReturn(AuthTokenScopes.MANAGED_COLLECTOR_AUDIENCE);
 +        
when(principalMap.getPrincipal(AuthTokenScopes.CLAIM_COLLECTOR_ID)).thenReturn("edge-west");
 +        when(principalMap.getPrincipal(AuthTokenScopes.CLAIM_ALLOWED_SIGNALS))
 +                .thenReturn(List.of("metrics", "logs", "traces"));
 +
 +        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
 +            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
 +
 +            
org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response, 
new Object()));
 +            org.junit.jupiter.api.Assertions.assertEquals("edge-west", 
AuthTokenRequestContext.currentCollectorId());
 +
 +            filter.afterCompletion(request, response, new Object(), null);
 +            
org.junit.jupiter.api.Assertions.assertNull(AuthTokenRequestContext.currentCollectorId());
 +        }
 +    }
 +
 +    @Test
 +    void testManagedCollectorTokenRejectsUnallowedSignal() throws Exception {
-         String managedToken = "collector-token";
 +        
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + 
managedToken);
 +        when(request.getRequestURI()).thenReturn("/api/otlp/v1/traces");
 +        when(accountService.checkTokenStatus(
 +                managedToken, AuthTokenScopes.OTLP_INGEST, 
AuthTokenScopes.DEFAULT_WORKSPACE_ID)).thenReturn(null);
 +        when(accountService.checkManagedTokenAccess("admin", 
List.of("admin"), null)).thenReturn(null);
 +        when(response.getWriter()).thenReturn(new PrintWriter(new 
StringWriter()));
 +        SubjectSum subject = mockManagedSubjectWithClaims();
 +        when(principalMap.getPrincipal(AuthTokenScopes.CLAIM_TOKEN_AUDIENCE))
 +                .thenReturn(AuthTokenScopes.MANAGED_COLLECTOR_AUDIENCE);
 +        
when(principalMap.getPrincipal(AuthTokenScopes.CLAIM_COLLECTOR_ID)).thenReturn("edge-west");
 +        
when(principalMap.getPrincipal(AuthTokenScopes.CLAIM_ALLOWED_SIGNALS)).thenReturn(List.of("metrics"));
 +
 +        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
 +            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
 +
 +            
org.junit.jupiter.api.Assertions.assertFalse(filter.preHandle(request, 
response, new Object()));
 +            verify(response).setStatus(401);
 +            verify(accountService, 
never()).touchTokenLastUsedTime(managedToken);
 +        }
 +    }
 +
 +    @Test
 +    void 
testManagedTokenWorkspaceOverrideRejectedAfterTokenBoundaryValidation() throws 
Exception {
-         String managedToken = "managed-token";
 +        
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + 
managedToken);
 +        
when(request.getHeader(AuthTokenScopes.WORKSPACE_ID_HEADER)).thenReturn("prod-west");
 +        
when(principalMap.getPrincipal(AuthTokenScopes.CLAIM_WORKSPACE_ID)).thenReturn("team-a");
 +        when(request.getMethod()).thenReturn("POST");
 +        when(request.getRequestURI()).thenReturn("/api/monitor");
 +        when(accountService.checkTokenStatus(managedToken, 
AuthTokenScopes.API_ADMIN, "team-a")).thenReturn(null);
 +        when(accountService.checkManagedTokenAccess("admin", 
List.of("admin"), null)).thenReturn(null);
 +        when(response.getWriter()).thenReturn(new PrintWriter(new 
StringWriter()));
 +        SubjectSum subject = mockManagedSubjectWithClaims();
 +
 +        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
 +            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
 +
 +            
org.junit.jupiter.api.Assertions.assertFalse(filter.preHandle(request, 
response, new Object()));
 +            verify(accountService).checkTokenStatus(managedToken, 
AuthTokenScopes.API_ADMIN, "team-a");
 +            verify(accountService).checkManagedTokenAccess("admin", 
List.of("admin"), null);
 +            verify(accountService, 
never()).touchTokenLastUsedTime(managedToken);
 +            verify(response).setStatus(403);
 +            
org.junit.jupiter.api.Assertions.assertNull(AuthTokenRequestContext.currentWorkspaceId());
 +            
org.junit.jupiter.api.Assertions.assertNull(AuthTokenRequestContext.currentAuthenticatedWorkspaceId());
 +        }
      }
  
      @Test
      void testManagedTokenRevokedRejected() throws Exception {
-         String revokedToken = "revoked-token";
-         
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + 
revokedToken);
+         
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + 
managedToken);
 -        when(accountService.checkTokenStatus(managedToken)).thenReturn("Token 
has been revoked");
 +        when(request.getMethod()).thenReturn("POST");
 +        when(request.getRequestURI()).thenReturn("/api/monitor");
 +        when(accountService.checkTokenStatus(
-                 revokedToken, AuthTokenScopes.API_ADMIN, 
AuthTokenScopes.DEFAULT_WORKSPACE_ID))
++                managedToken, AuthTokenScopes.API_ADMIN, 
AuthTokenScopes.DEFAULT_WORKSPACE_ID))
 +                .thenReturn("Token has been revoked");
+         mockErrorWriter();
+         SubjectSum subject = mockManagedSubject();
  
-         StringWriter stringWriter = new StringWriter();
-         PrintWriter printWriter = new PrintWriter(stringWriter);
-         when(response.getWriter()).thenReturn(printWriter);
+         assertFalse(preHandle(subject));
+         verify(response).setStatus(401);
+     }
+ 
+     @Test
+     void testManagedTokenInQueryParameterRevokedRejected() throws Exception {
+         
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn(null);
+         when(request.getParameter("token")).thenReturn(managedToken);
 -        when(accountService.checkTokenStatus(managedToken)).thenReturn("Token 
has been revoked");
++        when(accountService.checkTokenStatus(
++                managedToken, AuthTokenScopes.API_ADMIN, 
AuthTokenScopes.DEFAULT_WORKSPACE_ID))
++                .thenReturn("Token has been revoked");
+         mockErrorWriter();
          SubjectSum subject = mockManagedSubject();
  
-         try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
-             
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
+         assertFalse(preHandle(subject));
+         verify(response).setStatus(401);
+     }
  
-             
org.junit.jupiter.api.Assertions.assertFalse(filter.preHandle(request, 
response, new Object()));
-             verify(response).setStatus(401);
-         }
+     @Test
+     void testManagedTokenInQueryParameterActivePassesThrough() throws 
Exception {
+         
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn(null);
+         when(request.getParameter("token")).thenReturn(managedToken);
 -        when(accountService.checkTokenStatus(managedToken)).thenReturn(null);
 -        when(accountService.checkManagedTokenAccess("admin", 
List.of("admin"))).thenReturn(null);
++        when(accountService.checkTokenStatus(
++                managedToken, AuthTokenScopes.API_ADMIN, 
AuthTokenScopes.DEFAULT_WORKSPACE_ID))
++                .thenReturn(null);
++        when(accountService.checkManagedTokenAccess("admin", 
List.of("admin"), null)).thenReturn(null);
+         SubjectSum subject = mockManagedSubjectWithClaims();
+ 
+         assertTrue(preHandle(subject));
+         verify(accountService).touchTokenLastUsedTime(managedToken);
+     }
+ 
+     @Test
+     void testRevokedQueryTokenRejectedEvenWithActiveHeaderToken() throws 
Exception {
+         
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + 
otherManagedToken);
+         when(request.getParameter("token")).thenReturn(managedToken);
 -        
when(accountService.checkTokenStatus(otherManagedToken)).thenReturn(null);
 -        when(accountService.checkTokenStatus(managedToken)).thenReturn("Token 
has been revoked");
 -        when(accountService.checkManagedTokenAccess("admin", 
List.of("admin"))).thenReturn(null);
++        when(accountService.checkTokenStatus(
++                otherManagedToken, AuthTokenScopes.API_ADMIN, 
AuthTokenScopes.DEFAULT_WORKSPACE_ID))
++                .thenReturn(null);
++        when(accountService.checkTokenStatus(
++                managedToken, AuthTokenScopes.API_ADMIN, 
AuthTokenScopes.DEFAULT_WORKSPACE_ID))
++                .thenReturn("Token has been revoked");
++        when(accountService.checkManagedTokenAccess("admin", 
List.of("admin"), null)).thenReturn(null);
+         mockErrorWriter();
+         SubjectSum subject = mockManagedSubjectWithClaims();
+ 
+         assertFalse(preHandle(subject));
+         verify(response).setStatus(401);
+     }
+ 
+     @Test
+     void testHeaderTokenExtractedLikeSurenessRejectsRevokedToken() throws 
Exception {
+         // Sureness strips every "Bearer" from the header, so a trailing 
"Bearer" still authenticates the token
+         
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + 
managedToken + "Bearer");
+         when(request.getParameter("token")).thenReturn(otherManagedToken);
 -        when(accountService.checkTokenStatus(managedToken)).thenReturn("Token 
has been revoked");
++        when(accountService.checkTokenStatus(
++                managedToken, AuthTokenScopes.API_ADMIN, 
AuthTokenScopes.DEFAULT_WORKSPACE_ID))
++                .thenReturn("Token has been revoked");
+         mockErrorWriter();
+         SubjectSum subject = mockManagedSubject();
+ 
+         assertFalse(preHandle(subject));
+         verify(response).setStatus(401);
 -        verify(accountService, never()).checkTokenStatus(otherManagedToken);
++        verify(accountService, never()).checkTokenStatus(
++                org.mockito.ArgumentMatchers.eq(otherManagedToken), any(), 
any());
+     }
+ 
+     @Test
+     void testNonManagedCandidatesAreIgnored() throws Exception {
+         
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + 
managedToken);
+         when(request.getParameter("token")).thenReturn(legacyToken);
 -        when(accountService.checkTokenStatus(managedToken)).thenReturn(null);
 -        when(accountService.checkManagedTokenAccess("admin", 
List.of("admin"))).thenReturn(null);
++        when(accountService.checkTokenStatus(
++                managedToken, AuthTokenScopes.API_ADMIN, 
AuthTokenScopes.DEFAULT_WORKSPACE_ID))
++                .thenReturn(null);
++        when(accountService.checkManagedTokenAccess("admin", 
List.of("admin"), null)).thenReturn(null);
+         SubjectSum subject = mockManagedSubjectWithClaims();
+ 
+         assertTrue(preHandle(subject));
 -        verify(accountService, never()).checkTokenStatus(legacyToken);
++        verify(accountService, never()).checkTokenStatus(
++                org.mockito.ArgumentMatchers.eq(legacyToken), any(), any());
      }
  
      @Test
      void testManagedTokenStatusCheckFailureRejectsRequest() throws Exception {
-         
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer 
managed-token");
+         
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + 
managedToken);
 -        when(accountService.checkTokenStatus(managedToken)).thenThrow(new 
RuntimeException("DB down"));
 +        when(request.getMethod()).thenReturn("POST");
 +        when(request.getRequestURI()).thenReturn("/api/monitor");
 +        when(accountService.checkTokenStatus(
-                 "managed-token", AuthTokenScopes.API_ADMIN, 
AuthTokenScopes.DEFAULT_WORKSPACE_ID))
++                managedToken, AuthTokenScopes.API_ADMIN, 
AuthTokenScopes.DEFAULT_WORKSPACE_ID))
 +                .thenThrow(new RuntimeException("DB down"));
- 
-         StringWriter stringWriter = new StringWriter();
-         PrintWriter printWriter = new PrintWriter(stringWriter);
-         when(response.getWriter()).thenReturn(printWriter);
+         mockErrorWriter();
          SubjectSum subject = mockManagedSubject();
  
-         try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
-             
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
- 
-             
org.junit.jupiter.api.Assertions.assertFalse(filter.preHandle(request, 
response, new Object()));
-             verify(response).setStatus(503);
-             verify(accountService, never()).touchTokenLastUsedTime(any());
-         }
+         assertFalse(preHandle(subject));
+         verify(response).setStatus(503);
+         verify(accountService, never()).touchTokenLastUsedTime(any());
      }
  
      @Test
      void testManagedTokenOutdatedRolesRejected() throws Exception {
-         String managedToken = "managed-token";
          
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + 
managedToken);
 -        when(accountService.checkTokenStatus(managedToken)).thenReturn(null);
 -        when(accountService.checkManagedTokenAccess("admin", 
List.of("admin")))
 +        when(request.getMethod()).thenReturn("POST");
 +        when(request.getRequestURI()).thenReturn("/api/monitor");
 +        when(accountService.checkTokenStatus(
 +                managedToken, AuthTokenScopes.API_ADMIN, 
AuthTokenScopes.DEFAULT_WORKSPACE_ID)).thenReturn(null);
 +        when(accountService.checkManagedTokenAccess("admin", 
List.of("admin"), null))
                  .thenReturn("Token permissions are outdated");
- 
-         StringWriter stringWriter = new StringWriter();
-         PrintWriter printWriter = new PrintWriter(stringWriter);
-         when(response.getWriter()).thenReturn(printWriter);
+         mockErrorWriter();
          SubjectSum subject = mockManagedSubjectWithClaims();
  
-         try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
-             
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
- 
-             
org.junit.jupiter.api.Assertions.assertFalse(filter.preHandle(request, 
response, new Object()));
-             verify(response).setStatus(401);
-             verify(accountService, 
never()).touchTokenLastUsedTime(managedToken);
-         }
-     }
- 
-     @Test
-     void testEmptyBearerTokenPassesThrough() throws Exception {
-         
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer ");
-         SubjectSum subject = mockManagedSubject();
- 
-         try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
-             
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
- 
-             
org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response, 
new Object()));
-         }
+         assertFalse(preHandle(subject));
+         verify(response).setStatus(401);
+         verify(accountService, never()).touchTokenLastUsedTime(managedToken);
      }
  
      @Test
      void testTouchLastUsedTimeFailureDoesNotRejectRequest() throws Exception {
-         String managedToken = "managed-token";
          
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + 
managedToken);
 -        when(accountService.checkTokenStatus(managedToken)).thenReturn(null);
 -        when(accountService.checkManagedTokenAccess("admin", 
List.of("admin"))).thenReturn(null);
 +        when(request.getMethod()).thenReturn("POST");
 +        when(request.getRequestURI()).thenReturn("/api/monitor");
 +        when(accountService.checkTokenStatus(
 +                managedToken, AuthTokenScopes.API_ADMIN, 
AuthTokenScopes.DEFAULT_WORKSPACE_ID)).thenReturn(null);
 +        when(accountService.checkManagedTokenAccess("admin", 
List.of("admin"), null)).thenReturn(null);
          // touchTokenLastUsedTime throws exception
          org.mockito.Mockito.doThrow(new RuntimeException("DB error"))
                  .when(accountService).touchTokenLastUsedTime(managedToken);
@@@ -492,26 -266,22 +547,25 @@@
  
      @Test
      void testManagedTokenAccountValidationFailureRejectsRequest() throws 
Exception {
-         String managedToken = "managed-token";
          
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + 
managedToken);
 -        when(accountService.checkTokenStatus(managedToken)).thenReturn(null);
 -        when(accountService.checkManagedTokenAccess("admin", 
List.of("admin")))
 +        when(request.getMethod()).thenReturn("POST");
 +        when(request.getRequestURI()).thenReturn("/api/monitor");
 +        when(accountService.checkTokenStatus(
 +                managedToken, AuthTokenScopes.API_ADMIN, 
AuthTokenScopes.DEFAULT_WORKSPACE_ID)).thenReturn(null);
 +        when(accountService.checkManagedTokenAccess("admin", 
List.of("admin"), null))
                  .thenThrow(new RuntimeException("account store unavailable"));
- 
-         StringWriter stringWriter = new StringWriter();
-         PrintWriter printWriter = new PrintWriter(stringWriter);
-         when(response.getWriter()).thenReturn(printWriter);
+         mockErrorWriter();
          SubjectSum subject = mockManagedSubjectWithClaims();
  
-         try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
-             
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
+         assertFalse(preHandle(subject));
+         verify(response).setStatus(503);
+         verify(accountService, never()).touchTokenLastUsedTime(managedToken);
+     }
  
-             
org.junit.jupiter.api.Assertions.assertFalse(filter.preHandle(request, 
response, new Object()));
-             verify(response).setStatus(503);
-             verify(accountService, 
never()).touchTokenLastUsedTime(managedToken);
+     private boolean preHandle(SubjectSum subject) throws Exception {
+         try (MockedStatic<SurenessContextHolder> holder = 
mockStatic(SurenessContextHolder.class)) {
+             
holder.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
+             return filter.preHandle(request, response, new Object());
          }
      }
  


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

Reply via email to