This is an automated email from the ASF dual-hosted git repository.
zqr10159 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git
The following commit(s) were added to refs/heads/master by this push:
new aafa8cc2e7 [fix] validate managed API tokens passed as query parameter
(#4442)
aafa8cc2e7 is described below
commit aafa8cc2e717e20814686b848aea0c0350aa0b2a
Author: Duansg <[email protected]>
AuthorDate: Fri Oct 9 14:32:51 2026 +0800
[fix] validate managed API tokens passed as query parameter (#4442)
---
.../manager/config/ApiTokenValidationFilter.java | 63 ++++--
.../config/ApiTokenValidationFilterTest.java | 235 ++++++++++++---------
2 files changed, 191 insertions(+), 107 deletions(-)
diff --git
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
index 4075d2e1d6..0da8c65197 100644
---
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
@@ -19,15 +19,20 @@ package org.apache.hertzbeat.manager.config;
import com.usthe.sureness.subject.PrincipalMap;
import com.usthe.sureness.subject.SubjectSum;
+import com.usthe.sureness.util.JsonWebTokenUtil;
import com.usthe.sureness.util.SurenessContextHolder;
+import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
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.hertzbeat.collector.dispatch.DispatchConstants;
@@ -63,6 +68,8 @@ public class ApiTokenValidationFilter implements
HandlerInterceptor {
private static final String ROLES_CLAIM = "roles";
private static final String TOKEN_VALIDATION_UNAVAILABLE = "Token
validation unavailable";
+ private static final String TOKEN_NOT_RESOLVED = "Token could not be
resolved";
+ private static final String TOKEN_PARAM = "token";
private final AccountService accountService;
@@ -77,24 +84,54 @@ public class ApiTokenValidationFilter implements
HandlerInterceptor {
if (subject == null || !isManagedToken(subject)) {
return true;
}
- String authorization =
request.getHeader(NetworkConstants.AUTHORIZATION);
+ // 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);
+ 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 true;
+ }
+ 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)) {
- String token =
authorization.substring(DispatchConstants.BEARER.length()).trim();
- if (!token.isEmpty()) {
- try {
- String rejectReason = checkManagedToken(subject, token);
- 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);
- }
+ // 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;
+ }
- return true;
+ 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;
+ }
}
/**
diff --git
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
index 36414d88f2..042d966725 100644
---
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,27 +17,34 @@
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.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import static org.mockito.Mockito.mockStatic;
import com.usthe.sureness.subject.PrincipalMap;
import com.usthe.sureness.subject.SubjectSum;
+import com.usthe.sureness.util.JsonWebTokenUtil;
import com.usthe.sureness.util.SurenessContextHolder;
import jakarta.servlet.http.HttpServletRequest;
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.manager.service.AccountService;
+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.mockito.Mock;
+import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
/**
@@ -46,6 +53,12 @@ import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class ApiTokenValidationFilterTest {
+ private static String managedToken;
+
+ private static String otherManagedToken;
+
+ private static String legacyToken;
+
private ApiTokenValidationFilter filter;
@Mock
@@ -60,141 +73,180 @@ class ApiTokenValidationFilterTest {
@Mock
private PrincipalMap principalMap;
+ @BeforeAll
+ static void issueTokens() {
+
JsonWebTokenUtil.setDefaultSecretKey("dKhaX0csgOCTlCxq20yhmUea6H6JIpSE2Rwp"
+ + "CyaFv0bwq2Eik0jdrKUtsA6bx3sDJeFV643R"
+ + "LnfKefTjsIfJLBa2YkhEqEGtcHDTNe4CU6+9"
+ + "dKhaX0csgOCTlCxq20yhmUea6H6JIpSE2Rwp");
+ Map<String, Object> managedClaims = new HashMap<>(1);
+ managedClaims.put("managed", true);
+ managedToken = JsonWebTokenUtil.issueJwt("admin", 3600L,
List.of("admin"), managedClaims);
+ otherManagedToken = JsonWebTokenUtil.issueJwt("admin", 7200L,
List.of("admin"), managedClaims);
+ legacyToken = JsonWebTokenUtil.issueJwt("admin", 3600L,
List.of("admin"), new HashMap<>(0));
+ }
+
@BeforeEach
void setUp() {
filter = new ApiTokenValidationFilter(accountService);
}
@Test
- void testNoAuthorizationHeaderPassesThrough() throws Exception {
+ void testManagedSubjectWithoutTokenRejected() throws Exception {
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn(null);
+ mockErrorWriter();
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()).checkTokenStatus(any());
}
@Test
- void testNonBearerAuthorizationPassesThrough() throws Exception {
+ void testManagedSubjectWithNonBearerAuthorizationRejected() throws
Exception {
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Basic
dXNlcjpwYXNz");
+ 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.assertTrue(filter.preHandle(request, response,
new Object()));
- }
+ @Test
+ void testManagedSubjectWithEmptyBearerRejected() throws Exception {
+
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer ");
+ mockErrorWriter();
+ SubjectSum subject = mockManagedSubject();
+
+ assertFalse(preHandle(subject));
+ verify(response).setStatus(401);
}
@Test
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 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);
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);
- verify(accountService).checkManagedTokenAccess("admin",
List.of("admin"));
- verify(accountService).touchTokenLastUsedTime(managedToken);
- }
+ assertTrue(preHandle(subject));
+ verify(accountService).checkTokenStatus(managedToken);
+ verify(accountService).checkManagedTokenAccess("admin",
List.of("admin"));
+ verify(accountService).touchTokenLastUsedTime(managedToken);
}
@Test
void testManagedTokenRevokedRejected() throws Exception {
- String revokedToken = "revoked-token";
-
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " +
revokedToken);
- when(accountService.checkTokenStatus(revokedToken)).thenReturn("Token
has been revoked");
+
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " +
managedToken);
+ when(accountService.checkTokenStatus(managedToken)).thenReturn("Token
has been revoked");
+ mockErrorWriter();
+ SubjectSum subject = mockManagedSubject();
+
+ assertFalse(preHandle(subject));
+ verify(response).setStatus(401);
+ }
- StringWriter stringWriter = new StringWriter();
- PrintWriter printWriter = new PrintWriter(stringWriter);
- when(response.getWriter()).thenReturn(printWriter);
+ @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");
+ 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);
+ SubjectSum subject = mockManagedSubjectWithClaims();
+
+ assertTrue(preHandle(subject));
+ verify(accountService).touchTokenLastUsedTime(managedToken);
}
@Test
- void testManagedTokenStatusCheckFailureRejectsRequest() throws Exception {
-
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer
managed-token");
- when(accountService.checkTokenStatus("managed-token")).thenThrow(new
RuntimeException("DB down"));
+ 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);
+ mockErrorWriter();
+ SubjectSum subject = mockManagedSubjectWithClaims();
- StringWriter stringWriter = new StringWriter();
- PrintWriter printWriter = new PrintWriter(stringWriter);
- when(response.getWriter()).thenReturn(printWriter);
- SubjectSum subject = mockManagedSubject();
+ assertFalse(preHandle(subject));
+ verify(response).setStatus(401);
+ }
- try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
-
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
+ @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");
+ mockErrorWriter();
+ SubjectSum subject = mockManagedSubject();
-
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(401);
+ verify(accountService, never()).checkTokenStatus(otherManagedToken);
}
@Test
- void testManagedTokenOutdatedRolesRejected() throws Exception {
- String managedToken = "managed-token";
+ 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("Token permissions are outdated");
-
- StringWriter stringWriter = new StringWriter();
- PrintWriter printWriter = new PrintWriter(stringWriter);
- when(response.getWriter()).thenReturn(printWriter);
+ when(accountService.checkManagedTokenAccess("admin",
List.of("admin"))).thenReturn(null);
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);
- }
+ assertTrue(preHandle(subject));
+ verify(accountService, never()).checkTokenStatus(legacyToken);
}
@Test
- void testEmptyBearerTokenPassesThrough() throws Exception {
-
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer ");
+ void testManagedTokenStatusCheckFailureRejectsRequest() throws Exception {
+
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " +
managedToken);
+ when(accountService.checkTokenStatus(managedToken)).thenThrow(new
RuntimeException("DB down"));
+ mockErrorWriter();
SubjectSum subject = mockManagedSubject();
- try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
-
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
+ assertFalse(preHandle(subject));
+ verify(response).setStatus(503);
+ verify(accountService, never()).touchTokenLastUsedTime(any());
+ }
+
+ @Test
+ void testManagedTokenOutdatedRolesRejected() throws Exception {
+
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " +
managedToken);
+ when(accountService.checkTokenStatus(managedToken)).thenReturn(null);
+ when(accountService.checkManagedTokenAccess("admin", List.of("admin")))
+ .thenReturn("Token permissions are outdated");
+ mockErrorWriter();
+ SubjectSum subject = mockManagedSubjectWithClaims();
-
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);
@@ -203,45 +255,40 @@ class ApiTokenValidationFilterTest {
.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()));
- }
+ assertTrue(preHandle(subject));
}
@Test
void testUnauthenticatedRequestSkipsValidation() throws Exception {
- try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
-
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(null);
-
-
org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response,
new Object()));
- verify(accountService, never()).checkTokenStatus(any());
- }
+ assertTrue(preHandle(null));
+ verify(accountService, never()).checkTokenStatus(any());
}
@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")))
.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());
}
}
+ private void mockErrorWriter() throws Exception {
+ when(response.getWriter()).thenReturn(new PrintWriter(new
StringWriter()));
+ }
+
private SubjectSum mockSubject() {
SubjectSum subjectSum = org.mockito.Mockito.mock(SubjectSum.class);
when(subjectSum.getPrincipalMap()).thenReturn(principalMap);
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]