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
