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 8b86afb111386f58399a16a30f07b10a90902641 Author: hanicz <[email protected]> AuthorDate: Tue Aug 11 16:44:15 2026 +0200 KNOX-3410: KnoxToken enable/disable endpoints perform no caller autho… (#1343) * KNOX-3410: KnoxToken enable/disable endpoints perform no caller authorization * KNOX-3410: Address review comments (cherry picked from commit f2126008ab764003e9831e48d30302886be93496) --- .../gateway/service/knoxtoken/TokenResource.java | 21 ++- .../knoxtoken/TokenServiceResourceTest.java | 183 +++++++++++++++++++-- 2 files changed, 183 insertions(+), 21 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 6d876648b..5a4429d85 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 @@ -584,7 +584,7 @@ public class TokenResource { } else { final String renewer = SubjectUtils.getCurrentEffectivePrincipalName(); - if (tokenRenewalOrRevocationAuthorized(renewer)) { + if (tokenStateChangeAuthorized(renewer)) { try { JWTToken jwt = new JWTToken(token); if (tokenStateService.isExpired(jwt)) { @@ -624,7 +624,7 @@ public class TokenResource { return resp; } - private boolean tokenRenewalOrRevocationAuthorized(final String principalName) { + private boolean tokenStateChangeAuthorized(final String principalName) { final boolean userAllowed = allowedRenewers.contains(principalName); final boolean groupAllowed = SubjectUtils.getCurrentGroupPrincipals().stream() .map(GroupPrincipal::getName) @@ -675,7 +675,7 @@ public class TokenResource { errorStatus = Response.Status.FORBIDDEN; error = "SSO cookie (" + Tokens.getTokenIDDisplayText(tokenId) + ") cannot not be revoked."; errorCode = ErrorCode.UNAUTHORIZED; - } else if (triesToRevokeOwnToken(tokenId, revoker) || tokenRenewalOrRevocationAuthorized(revoker)) { + } else if (triesToChangeOwnToken(tokenId, revoker) || tokenStateChangeAuthorized(revoker)) { tokenStateService.revokeToken(tokenId); log.revokedToken(getTopologyName(), Tokens.getTokenDisplayText(token), @@ -715,7 +715,7 @@ public class TokenResource { return metadata == null ? false : metadata.isKnoxSsoCookie(); } - private boolean triesToRevokeOwnToken(String tokenId, String revoker) throws UnknownTokenException { + private boolean triesToChangeOwnToken(String tokenId, String revoker) throws UnknownTokenException { final TokenMetadata metadata = tokenStateService.getTokenMetadata(tokenId); final String tokenUserName = metadata == null ? "" : metadata.getUserName(); final String tokenCreatedBy = metadata == null ? "" : metadata.getCreatedBy(); @@ -785,13 +785,19 @@ public class TokenResource { private Response setTokenEnabledFlag(String tokenId, boolean enable, boolean batch) { String error = ""; ErrorCode errorCode = ErrorCode.UNKNOWN; + Response.Status responseStatus = Response.Status.BAD_REQUEST; if (tokenStateService == null) { error = "Unable to " + (enable ? "enable" : "disable") + " tokens because token management is not configured"; errorCode = ErrorCode.CONFIGURATION_ERROR; } else { try { final TokenMetadata tokenMetadata = tokenStateService.getTokenMetadata(tokenId); - if (!batch && enable && tokenMetadata.isEnabled()) { + final String caller = SubjectUtils.getCurrentEffectivePrincipalName(); + if (!(triesToChangeOwnToken(tokenId, caller) || tokenStateChangeAuthorized(caller))) { + responseStatus = Response.Status.FORBIDDEN; + error = "Caller (" + caller + ") not authorized to " + (enable ? "enable" : "disable") + " tokens."; + errorCode = ErrorCode.UNAUTHORIZED; + } else if (!batch && enable && tokenMetadata.isEnabled()) { error = "Token is already enabled"; errorCode = ErrorCode.ALREADY_ENABLED; } else if (!batch && !enable && !tokenMetadata.isEnabled()) { @@ -811,11 +817,12 @@ public class TokenResource { } if (error.isEmpty()) { + responseStatus = Response.Status.OK; log.setEnabledFlag(getTopologyName(), enable, Tokens.getTokenIDDisplayText(tokenId)); - return Response.status(Response.Status.OK).entity("{\n \"setEnabledFlag\": \"true\",\n \"isEnabled\": \"" + enable + "\"\n}\n").build(); + return Response.status(responseStatus).entity("{\n \"setEnabledFlag\": \"true\",\n \"isEnabled\": \"" + enable + "\"\n}\n").build(); } else { log.badSetEnabledFlagRequest(getTopologyName(), Tokens.getTokenIDDisplayText(tokenId), error); - return Response.status(Response.Status.BAD_REQUEST).entity("{\n \"setEnabledFlag\": \"false\",\n \"error\": \"" + error + "\",\n \"code\": " + errorCode.toInt() + "\n}\n").build(); + return Response.status(responseStatus).entity("{\n \"setEnabledFlag\": \"false\",\n \"error\": \"" + error + "\",\n \"code\": " + errorCode.toInt() + "\n}\n").build(); } } 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 8398e168b..80a17fdce 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 @@ -138,7 +138,9 @@ public class TokenServiceResourceTest { private enum TokenLifecycleOperation { Renew, - Revoke + Revoke, + Enable, + Disable } @BeforeClass @@ -1045,6 +1047,104 @@ public class TokenServiceResourceTest { "Caller (" + caller + ") not authorized to revoke tokens.", TokenResource.ErrorCode.UNAUTHORIZED); } + @Test + public void testTokenDisable_Enabled_NoSubject() throws Exception { + final TokenRenewalTestConfigs configs = TokenRenewalTestConfigs.builder().serviceLevelConfig(true).build(); + final Response response = doTestSetTokenEnabledFlag(configs, false); + validateSetEnabledFlagResponse(response, 403, false, + "Caller (null) not authorized to disable tokens.", TokenResource.ErrorCode.UNAUTHORIZED); + } + + @Test + public void testTokenDisable_Enabled_UnauthorizedCaller() throws Exception { + final String caller = "scott"; + final TokenRenewalTestConfigs configs = TokenRenewalTestConfigs.builder() + .serviceLevelConfig(true) + .caller(createTestSubject(caller)) + .build(); + final Response response = doTestSetTokenEnabledFlag(configs, false); + validateSetEnabledFlagResponse(response, 403, false, + "Caller (" + caller + ") not authorized to disable tokens.", TokenResource.ErrorCode.UNAUTHORIZED); + } + + @Test + public void testTokenDisable_Enabled_OwnToken() throws Exception { + final TokenRenewalTestConfigs configs = TokenRenewalTestConfigs.builder() + .serviceLevelConfig(true) + .caller(createTestSubject(USER_NAME)) + .build(); + final Response response = doTestSetTokenEnabledFlag(configs, false); + validateSuccessfulSetEnabledFlagResponse(response, false); + } + + @Test + public void testTokenDisable_Enabled_WithRenewerWhitelist() throws Exception { + final String caller = "scott"; + final TokenRenewalTestConfigs configs = TokenRenewalTestConfigs.builder() + .serviceLevelConfig(true) + .renewers("tony, dany, steve ," + caller) + .caller(createTestSubject(caller)) + .build(); + final Response response = doTestSetTokenEnabledFlag(configs, false); + validateSuccessfulSetEnabledFlagResponse(response, false); + } + + @Test + public void testTokenDisable_Enabled_WithGroupRenewerWhitelist() throws Exception { + final String caller = "scott"; + final String group = "devOps"; + final TokenRenewalTestConfigs configs = TokenRenewalTestConfigs.builder() + .serviceLevelConfig(true) + .groupRenewers(group) + .caller(createTestSubject(caller, group)) + .build(); + final Response response = doTestSetTokenEnabledFlag(configs, false); + validateSuccessfulSetEnabledFlagResponse(response, false); + } + + @Test + public void testTokenEnable_UnauthorizedCallerRejectedBeforeStateCheck() throws Exception { + final String caller = "scott"; + final TokenRenewalTestConfigs configs = TokenRenewalTestConfigs.builder() + .serviceLevelConfig(true) + .caller(createTestSubject(caller)) + .build(); + final Response response = doTestSetTokenEnabledFlag(configs, true); + validateSetEnabledFlagResponse(response, 403, false, + "Caller (" + caller + ") not authorized to enable tokens.", TokenResource.ErrorCode.UNAUTHORIZED); + } + + @Test + public void testTokenEnable_AlreadyEnabled_OwnerGetsStateCheck() throws Exception { + final TokenRenewalTestConfigs configs = TokenRenewalTestConfigs.builder() + .serviceLevelConfig(true) + .caller(createTestSubject(USER_NAME)) + .build(); + final Response response = doTestSetTokenEnabledFlag(configs, true); + validateSetEnabledFlagResponse(response, 400, false, + "Token is already enabled", TokenResource.ErrorCode.ALREADY_ENABLED); + } + + @Test + public void testTokenDisable_Enabled_ImpersonatorCanDisableCreatedToken() throws Exception { + final Response response = doTestImpersonatedTokenSetEnabledFlag(createTestSubject(USER_NAME), false); + validateSuccessfulSetEnabledFlagResponse(response, false); + } + + @Test + public void testTokenDisable_Enabled_ImpersonatedUserCanDisableOwnToken() throws Exception { + final Response response = doTestImpersonatedTokenSetEnabledFlag(createTestSubject("impersonatedUserName"), false); + validateSuccessfulSetEnabledFlagResponse(response, false); + } + + @Test + public void testTokenDisable_Enabled_UnauthorizedCallerCannotDisableImpersonatedToken() throws Exception { + final String caller = "scott"; + final Response response = doTestImpersonatedTokenSetEnabledFlag(createTestSubject(caller), false); + validateSetEnabledFlagResponse(response, 403, false, + "Caller (" + caller + ") not authorized to disable tokens.", TokenResource.ErrorCode.UNAUTHORIZED); + } + @Test public void testKidJkuClaims() throws Exception { final Map<String, String> contextExpectations = new HashMap<>(); @@ -1700,6 +1800,42 @@ public class TokenServiceResourceTest { return doTestTokenLifecyle(TokenLifecycleOperation.Revoke, configs.isTokenStateServerManaged(), null, configs.getRenewers(), configs.getGroupRenewers(), null, configs.getCaller(), impersonatedUser).getValue(); } + private Response doTestSetTokenEnabledFlag(final TokenRenewalTestConfigs configs, final boolean enable) throws Exception { + final TokenLifecycleOperation operation = enable ? TokenLifecycleOperation.Enable : TokenLifecycleOperation.Disable; + return doTestTokenLifecyle(operation, configs.isTokenStateServerManaged(), null, configs.getRenewers(), configs.getGroupRenewers(), null, configs.getCaller(), null).getValue(); + } + + /** + * Issues a token under an impersonating subject (so the persisted metadata has + * userName=impersonated user and createdBy=impersonator) and then attempts to flip its + * enabled flag as {@code caller}. Mirrors the impersonated-token creation flow in + * {@link #testCreateImpersonatedToken(boolean)}. + */ + private Response doTestImpersonatedTokenSetEnabledFlag(final Subject caller, final boolean enable) throws Exception { + final String impersonatedUser = "impersonatedUserName"; + final Map<String, String> contextExpectations = new HashMap<>(); + contextExpectations.put("knox.token.exp.server-managed", Boolean.TRUE.toString()); + contextExpectations.put(TokenResource.QUERY_PARAMETER_DOAS, impersonatedUser); + contextExpectations.put(AuthFilterUtils.PROXYUSER_PREFIX + "." + USER_NAME + ".users", impersonatedUser); + contextExpectations.put(AuthFilterUtils.PROXYUSER_PREFIX + "." + USER_NAME + ".hosts", "*"); + contextExpectations.put(ContextAttributes.IMPERSONATION_ENABLED_ATTRIBUTE, Boolean.TRUE.toString()); + configureCommonExpectations(contextExpectations, Boolean.TRUE); + + final TokenResource tr = new TokenResource(); + tr.request = request; + tr.context = context; + tr.init(); + + final Subject issuer = createTestSubject(USER_NAME); + issuer.getPrincipals().add(new ImpersonatedPrincipal(impersonatedUser)); + final Response issueResponse = Subject.doAs(issuer, (PrivilegedAction<Response>) () -> tr.doGet()); + assertEquals(200, issueResponse.getStatus()); + final String accessToken = getTagValue(issueResponse.getEntity().toString(), "access_token"); + final String tokenId = TokenUtils.getTokenId(new JWTToken(accessToken)); + + return requestSetTokenEnabledFlag(tr, tokenId, enable, caller); + } + /** * @param operation A TokenLifecycleOperation * @param serviceLevelConfig true, if server-side token state management should be enabled at the service level; @@ -1751,6 +1887,8 @@ public class TokenServiceResourceTest { Response response = switch (operation) { case Renew -> requestTokenRenewal(tr, accessToken, caller); case Revoke -> requestTokenRevocation(tr, accessToken, caller); + case Enable -> requestSetTokenEnabledFlag(tr, TokenUtils.getTokenId(new JWTToken(accessToken)), true, caller); + case Disable -> requestSetTokenEnabledFlag(tr, TokenUtils.getTokenId(new JWTToken(accessToken)), false, caller); }; return new AbstractMap.SimpleEntry<>(tss, response); @@ -1792,6 +1930,11 @@ public class TokenServiceResourceTest { return response; } + private static Response requestSetTokenEnabledFlag(final TokenResource tr, final String tokenId, final boolean enable, final Subject caller) { + final PrivilegedAction<Response> action = () -> enable ? tr.enable(tokenId) : tr.disable(tokenId); + return caller != null ? Subject.doAs(caller, action) : action.run(); + } + private static void validateSuccessfulRenewalResponse(final Response response) throws IOException { validateRenewalResponse(response, 200, true, null, null); } @@ -1801,13 +1944,22 @@ public class TokenServiceResourceTest { final boolean expectedResult, final String expectedMessage, final TokenResource.ErrorCode expectedCode) throws IOException { + validateLifecycleResponse(response, "renewed", expectedStatusCode, expectedResult, expectedMessage, expectedCode); + } + + private static void validateLifecycleResponse(final Response response, + final String resultField, + final int expectedStatusCode, + final boolean expectedResult, + final String expectedMessage, + final TokenResource.ErrorCode expectedCode) throws IOException { assertEquals(expectedStatusCode, response.getStatus()); assertTrue(response.hasEntity()); String responseContent = (String) response.getEntity(); assertNotNull(responseContent); assertFalse(responseContent.isEmpty()); Map<String, Object> json = parseJSONResponse(responseContent); - boolean result = Boolean.valueOf((String)json.get("renewed")); + boolean result = Boolean.parseBoolean((String) json.get(resultField)); assertEquals(expectedResult, result); assertEquals(expectedMessage, json.get("error")); if (expectedCode != null) { @@ -1824,18 +1976,21 @@ public class TokenServiceResourceTest { final boolean expectedResult, final String expectedMessage, final TokenResource.ErrorCode expectedCode) throws IOException { - assertEquals(expectedStatusCode, response.getStatus()); - assertTrue(response.hasEntity()); - String responseContent = (String) response.getEntity(); - assertNotNull(responseContent); - assertFalse(responseContent.isEmpty()); - Map<String, Object> json = parseJSONResponse(responseContent); - boolean result = Boolean.valueOf((String)json.get("revoked")); - assertEquals(expectedResult, result); - assertEquals(expectedMessage, json.get("error")); - if (expectedCode != null) { - assertEquals(expectedCode.toInt(), json.get("code")); - } + validateLifecycleResponse(response, "revoked", expectedStatusCode, expectedResult, expectedMessage, expectedCode); + } + + private static void validateSuccessfulSetEnabledFlagResponse(final Response response, final boolean enable) throws IOException { + validateSetEnabledFlagResponse(response, 200, true, null, null); + final Map<String, Object> json = parseJSONResponse((String) response.getEntity()); + assertEquals(String.valueOf(enable), json.get("isEnabled")); + } + + private static void validateSetEnabledFlagResponse(final Response response, + final int expectedStatusCode, + final boolean expectedResult, + final String expectedMessage, + final TokenResource.ErrorCode expectedCode) throws IOException { + validateLifecycleResponse(response, "setEnabledFlag", expectedStatusCode, expectedResult, expectedMessage, expectedCode); }
