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);
   }
 
 

Reply via email to