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

hanicz pushed a commit to branch v3.0.0
in repository https://gitbox.apache.org/repos/asf/knox.git

commit cc04bd01d690c41d485b1400021a2e2c5f075e1e
Author: hanicz <[email protected]>
AuthorDate: Tue Aug 11 16:45:59 2026 +0200

    KNOX-3411: KnoxToken getUserTokens returns every user's token metadat… 
(#1344)
    
    * KNOX-3411: KnoxToken getUserTokens returns every user's token metadata 
without a caller authorization check
    
    * KNOX-3411: Add test clean-up
    
    (cherry picked from commit 049dc0cb894f907efa920a1a2c10fb6ed56fb109)
---
 .../gateway/service/knoxtoken/TokenResource.java   | 31 ++++++++
 .../service/knoxtoken/TokenServiceMessages.java    |  3 +
 .../knoxtoken/TokenServiceResourceTest.java        | 87 +++++++++++++++++++++-
 3 files changed, 119 insertions(+), 2 deletions(-)

diff --git 
a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java
 
b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java
index 5a4429d85..36fe65374 100644
--- 
a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java
+++ 
b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java
@@ -507,6 +507,14 @@ public class TokenResource {
       final String createdBy = 
uriInfo.getQueryParameters().getFirst("createdBy");
       final String userNameOrCreatedBy = 
uriInfo.getQueryParameters().getFirst("userNameOrCreatedBy");
       final boolean allTokens = 
Boolean.parseBoolean(uriInfo.getQueryParameters().getFirst("allTokens"));
+
+      final String caller = SubjectUtils.getCurrentEffectivePrincipalName();
+      if (!isAuthorizedToSeeTokens(caller, userName, createdBy, 
userNameOrCreatedBy, allTokens)) {
+        log.unauthorizedGetUserTokensRequest(getTopologyName(), caller);
+        return Response.status(Response.Status.FORBIDDEN)
+            .entity("{\n  \"error\": \"Caller (" + caller + ") is not 
authorized to see other users' tokens.\"\n}\n").build();
+      }
+
       final Collection<KnoxToken> userTokens;
       if (allTokens) {
         userTokens = tokenStateService.getAllTokens();
@@ -540,6 +548,29 @@ public class TokenResource {
     }
   }
 
+  private boolean isAuthorizedToSeeTokens(String caller, String userName, 
String createdBy, String userNameOrCreatedBy, boolean allTokens) {
+    if (StringUtils.isBlank(caller)) {
+      return false;
+    }
+    final GatewayConfig config = (GatewayConfig) 
context.getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE);
+    if (config != null && config.canSeeAllTokens(caller)) {
+      return true;
+    }
+    if (allTokens) {
+      return false;
+    }
+    // an ordinary caller must scope the query to their own tokens
+    final boolean hasIdentifier = userName != null || createdBy != null || 
userNameOrCreatedBy != null;
+    return hasIdentifier
+        && requestedIsCallerIfPresent(caller, userName)
+        && requestedIsCallerIfPresent(caller, createdBy)
+        && requestedIsCallerIfPresent(caller, userNameOrCreatedBy);
+  }
+
+  private static boolean requestedIsCallerIfPresent(String caller, String 
requested) {
+    return requested == null || caller.equals(requested);
+  }
+
   @GET
   @Path(GET_TSS_STATUS_PATH)
   @Produces({APPLICATION_JSON})
diff --git 
a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceMessages.java
 
b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceMessages.java
index 278e42434..5341edd4a 100644
--- 
a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceMessages.java
+++ 
b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceMessages.java
@@ -77,6 +77,9 @@ public interface TokenServiceMessages {
   @Message( level = MessageLevel.ERROR, text = "Knox Token service ({0}) 
rejected a bad set enabled flag request for token {1}: {2}")
   void badSetEnabledFlagRequest(String topologyName, String tokenId, String 
error);
 
+  @Message( level = MessageLevel.ERROR, text = "Knox Token service ({0}) 
rejected an unauthorized getUserTokens request from caller ({1})")
+  void unauthorizedGetUserTokensRequest(String topologyName, String caller);
+
   @Message( level = MessageLevel.DEBUG, text = "Knox Token service ({0}) 
stored state for token {1} ({2})")
   void storedToken(String topologyName, String tokenDisplayText, String 
tokenId);
 
diff --git 
a/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java
 
b/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java
index 80a17fdce..4658f07d9 100644
--- 
a/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java
+++ 
b/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java
@@ -113,6 +113,7 @@ import 
org.apache.knox.gateway.services.token.impl.JDBCTokenStateService;
 import org.apache.knox.gateway.util.AuthFilterUtils;
 import org.apache.knox.gateway.util.JsonUtils;
 import org.easymock.EasyMock;
+import org.junit.After;
 import org.junit.Assert;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -135,6 +136,7 @@ public class TokenServiceResourceTest {
   private JWTokenAuthority authority;
   private TestTokenStateService tss = new TestTokenStateService();
   private char[] hmacSecret;
+  private final Set<String> usersCanSeeAllTokens = new HashSet<>();
 
   private enum TokenLifecycleOperation {
     Renew,
@@ -153,6 +155,11 @@ public class TokenServiceResourceTest {
     privateKey = (RSAPrivateKey) KPair.getPrivate();
   }
 
+  @After
+  public void cleanUp() {
+    this.usersCanSeeAllTokens.clear();
+  }
+
   private void configureCommonExpectations(Map<String, String> 
contextExpectations) throws Exception {
     configureCommonExpectations(contextExpectations, null, null);
   }
@@ -208,6 +215,8 @@ public class TokenServiceResourceTest {
       EasyMock.expect(config.getServiceParameter(tokenStateServiceType, 
"impl")).andReturn(contextExpectations.get(tokenStateServiceType)).anyTimes();
     }
     
EasyMock.expect(config.getKnoxTokenHashAlgorithm()).andReturn(HmacAlgorithms.HMAC_SHA_256.getName()).anyTimes();
+    EasyMock.expect(config.canSeeAllTokens(EasyMock.anyObject(String.class)))
+        .andAnswer(() -> usersCanSeeAllTokens.contains((String) 
EasyMock.getCurrentArguments()[0])).anyTimes();
     EasyMock.expect(config.getMaximumNumberOfTokensPerUser())
         .andReturn(contextExpectations.containsKey(KNOX_TOKEN_USER_LIMIT) ? 
Integer.parseInt(contextExpectations.get(KNOX_TOKEN_USER_LIMIT)) : 
-1).anyTimes();
     
EasyMock.expect(services.getService(ServiceType.TOKEN_STATE_SERVICE)).andReturn(tss).anyTimes();
@@ -1342,12 +1351,86 @@ public class TokenServiceResourceTest {
   }
 
   private Response getUserTokensResponse(TokenResource tokenResource, boolean 
createdBy) {
+    return getUserTokensResponse(tokenResource, createTestSubject(USER_NAME),
+        Collections.singletonMap(createdBy ? "createdBy" : "userName", 
USER_NAME));
+  }
+
+  private Response getUserTokensResponse(TokenResource tokenResource, Subject 
caller, Map<String, String> queryParams) {
     final MultivaluedMap<String, String> queryParameters = new 
MultivaluedHashMap<>();
-    queryParameters.put(createdBy ? "createdBy" : "userName", 
Arrays.asList(USER_NAME));
+    queryParams.forEach((key, value) -> queryParameters.put(key, 
Arrays.asList(value)));
     final UriInfo uriInfo = EasyMock.createNiceMock(UriInfo.class);
     
EasyMock.expect(uriInfo.getQueryParameters()).andReturn(queryParameters).anyTimes();
     EasyMock.replay(uriInfo);
-    return tokenResource.getUserTokens(uriInfo);
+    return Subject.doAs(caller, (PrivilegedAction<Response>) () -> 
tokenResource.getUserTokens(uriInfo));
+  }
+
+  private TokenResource createTokenResourceWithTokensFor(String... users) 
throws Exception {
+    configureCommonExpectations(new HashMap<>(), Boolean.TRUE);
+    final TokenResource tr = new TokenResource();
+    tr.request = request;
+    tr.context = context;
+    tr.init();
+    for (String user : users) {
+      Subject.doAs(createTestSubject(user), (PrivilegedAction<Response>) () -> 
tr.doGet());
+    }
+    return tr;
+  }
+
+  @SuppressWarnings("unchecked")
+  private int tokenCount(Response response) {
+    final Collection<Object> tokens = ((Map<String, Collection<Object>>) 
JsonUtils.getObjectFromJsonString(response.getEntity().toString()))
+        .get("tokens");
+    return tokens.size();
+  }
+
+  @Test
+  public void testGetUserTokensOwnerCanSeeOwnTokens() throws Exception {
+    final TokenResource tr = createTokenResourceWithTokensFor(USER_NAME);
+    final Response response = getUserTokensResponse(tr, 
createTestSubject(USER_NAME), Collections.singletonMap("userName", USER_NAME));
+    assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+    assertEquals(1, tokenCount(response));
+  }
+
+  @Test
+  public void testGetUserTokensUnauthorizedForOtherUser() throws Exception {
+    final TokenResource tr = createTokenResourceWithTokensFor(USER_NAME);
+    final Response response = getUserTokensResponse(tr, 
createTestSubject("bob"), Collections.singletonMap("userName", USER_NAME));
+    assertEquals(Response.Status.FORBIDDEN.getStatusCode(), 
response.getStatus());
+    assertTrue(response.getEntity().toString().contains("not authorized"));
+  }
+
+  @Test
+  public void testGetUserTokensUnauthorizedForUserNameOrCreatedByOfOtherUser() 
throws Exception {
+    final TokenResource tr = createTokenResourceWithTokensFor(USER_NAME);
+    final Response response = getUserTokensResponse(tr, 
createTestSubject("bob"), Collections.singletonMap("userNameOrCreatedBy", 
USER_NAME));
+    assertEquals(Response.Status.FORBIDDEN.getStatusCode(), 
response.getStatus());
+    assertTrue(response.getEntity().toString().contains("not authorized"));
+  }
+
+  @Test
+  public void testGetUserTokensAllTokensDeniedForOrdinaryUser() throws 
Exception {
+    final TokenResource tr = createTokenResourceWithTokensFor(USER_NAME);
+    final Response response = getUserTokensResponse(tr, 
createTestSubject("bob"), Collections.singletonMap("allTokens", "true"));
+    assertEquals(Response.Status.FORBIDDEN.getStatusCode(), 
response.getStatus());
+    assertTrue(response.getEntity().toString().contains("not authorized"));
+  }
+
+  @Test
+  public void testGetUserTokensAdminCanSeeAllTokens() throws Exception {
+    final TokenResource tr = createTokenResourceWithTokensFor(USER_NAME, 
"bob");
+    usersCanSeeAllTokens.add("admin");
+    final Response response = getUserTokensResponse(tr, 
createTestSubject("admin"), Collections.singletonMap("allTokens", "true"));
+    assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+    assertEquals(2, tokenCount(response));
+  }
+
+  @Test
+  public void testGetUserTokensAdminCanSeeOtherUsersTokens() throws Exception {
+    final TokenResource tr = createTokenResourceWithTokensFor(USER_NAME);
+    usersCanSeeAllTokens.add("admin");
+    final Response response = getUserTokensResponse(tr, 
createTestSubject("admin"), Collections.singletonMap("userName", USER_NAME));
+    assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+    assertEquals(1, tokenCount(response));
   }
 
   @Test

Reply via email to