This is an automated email from the ASF dual-hosted git repository.
hanicz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git
The following commit(s) were added to refs/heads/master by this push:
new f2126008a KNOX-3410: KnoxToken enable/disable endpoints perform no
caller autho… (#1343)
f2126008a is described below
commit f2126008ab764003e9831e48d30302886be93496
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
---
.../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);
}