Skip to content

Commit

Permalink
CDPD-35800 KNOX-2713 - Allowing end-users to customize 'user limit ex…
Browse files Browse the repository at this point in the history
…ceeded' action when creating Knox tokens (apache#543)

Change-Id: Ice1dbf9fe14026a013bfe965cf198a0cea91f01e
  • Loading branch information
smolnar82 authored and Sandor Molnar committed Mar 11, 2022
1 parent 0619dfc commit e955cba
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.UUID;

Expand Down Expand Up @@ -116,6 +117,7 @@ public class TokenResource {
private static final String TSS_MAXIMUM_LIFETIME_TEXT = "maximumLifetimeText";
private static final String LIFESPAN_INPUT_ENABLED_PARAM = "knox.token.lifespan.input.enabled";
private static final String LIFESPAN_INPUT_ENABLED_TEXT = "lifespanInputEnabled";
static final String KNOX_TOKEN_USER_LIMIT_EXCEEDED_ACTION = "knox.token.user.limit.exceeded.action";
private static final long TOKEN_TTL_DEFAULT = 30000L;
static final String TOKEN_API_PATH = "knoxtoken/api/v1";
static final String RESOURCE_PATH = TOKEN_API_PATH + "/token";
Expand Down Expand Up @@ -150,6 +152,9 @@ public class TokenResource {

private int tokenLimitPerUser;

enum UserLimitExceededAction {REMOVE_OLDEST, RETURN_ERROR};
private UserLimitExceededAction userLimitExceededAction = UserLimitExceededAction.RETURN_ERROR;

private List<String> allowedRenewers;

@Context
Expand Down Expand Up @@ -246,6 +251,11 @@ public void init() throws AliasServiceException, ServiceLifecycleException, KeyL
tokenMAC = new TokenMAC(gatewayConfig.getKnoxTokenHashAlgorithm(), aliasService.getPasswordFromAliasForGateway(TokenMAC.KNOX_TOKEN_HASH_KEY_ALIAS_NAME));

tokenLimitPerUser = gatewayConfig.getMaximumNumberOfTokensPerUser();
final String userLimitExceededActionParam = context.getInitParameter(KNOX_TOKEN_USER_LIMIT_EXCEEDED_ACTION);
if (userLimitExceededActionParam != null) {
userLimitExceededAction = UserLimitExceededAction.valueOf(userLimitExceededActionParam);
log.generalInfoMessage("Configured Knox Token user limit exceeded action = " + userLimitExceededAction.name());
}

String renewIntervalValue = context.getInitParameter(TOKEN_EXP_RENEWAL_INTERVAL);
if (renewIntervalValue != null && !renewIntervalValue.isEmpty()) {
Expand Down Expand Up @@ -661,9 +671,17 @@ private Response getAuthenticationToken() {

if (tokenStateService != null) {
if (tokenLimitPerUser != -1) { // if -1 => unlimited tokens for all users
if (tokenStateService.getTokens(p.getName()).size() >= tokenLimitPerUser) {
final Collection<KnoxToken> userTokens = tokenStateService.getTokens(p.getName());
if (userTokens.size() >= tokenLimitPerUser) {
log.tokenLimitExceeded(p.getName());
return Response.status(Response.Status.FORBIDDEN).entity("{ \"Unable to get token - token limit exceeded.\" }").build();
if (UserLimitExceededAction.RETURN_ERROR == userLimitExceededAction) {
return Response.status(Response.Status.FORBIDDEN).entity("{ \"Unable to get token - token limit exceeded.\" }").build();
} else {
// userTokens is an ordered collection (by issue time) -> the first element is the oldest one
final String oldestTokenId = userTokens.iterator().next().getTokenId();
log.generalInfoMessage(String.format(Locale.getDefault(), "Revoking %s's oldest token %s ...", p.getName(), Tokens.getTokenIDDisplayText(oldestTokenId)));
revoke(oldestTokenId);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,7 @@ void invalidToken(String topologyName,

@Message( level = MessageLevel.ERROR, text = "Unable to get token for user {0}: token limit exceeded")
void tokenLimitExceeded(String userName);

@Message( level = MessageLevel.INFO, text = "{0}")
void generalInfoMessage(String message);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import static org.apache.knox.gateway.config.impl.GatewayConfigImpl.KNOX_TOKEN_USER_LIMIT;
import static org.apache.knox.gateway.config.impl.GatewayConfigImpl.KNOX_TOKEN_USER_LIMIT_DEFAULT;
import static org.apache.knox.gateway.service.knoxtoken.TokenResource.KNOX_TOKEN_USER_LIMIT_EXCEEDED_ACTION;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
Expand Down Expand Up @@ -979,12 +980,12 @@ private void testGettingTokenWithConfiguredTTL(String lifespan) throws Exception

@Test
public void testConfiguredTokenLimitPerUser() throws Exception {
testLimitingTokensPerUser(String.valueOf(KNOX_TOKEN_USER_LIMIT_DEFAULT), KNOX_TOKEN_USER_LIMIT_DEFAULT);
testLimitingTokensPerUser(KNOX_TOKEN_USER_LIMIT_DEFAULT, KNOX_TOKEN_USER_LIMIT_DEFAULT);
}

@Test
public void testUnlimitedTokensPerUser() throws Exception {
testLimitingTokensPerUser(String.valueOf("-1"), 100);
testLimitingTokensPerUser(-1, 100);
}

@Test
Expand Down Expand Up @@ -1018,16 +1019,32 @@ public void testTokenLimitChangeAfterAlreadyHavingTokens() throws Exception {
@Test
public void testTokenLimitPerUserExceeded() throws Exception {
try {
testLimitingTokensPerUser(String.valueOf("10"), 11);
testLimitingTokensPerUser(10, 11);
fail("Exception should have been thrown");
} catch (Exception e) {
assertTrue(e.getMessage().contains("Unable to get token - token limit exceeded."));
}
}

private void testLimitingTokensPerUser(String configuredLimit, int numberOfTokens) throws Exception {
@Test
public void testTokenLimitPerUserExceededShouldRevokeOldestToken() throws Exception {
try {
testLimitingTokensPerUser(10, 11, true);
} catch (Exception e) {
fail("Exception should NOT have been thrown");
}
}

private void testLimitingTokensPerUser(int configuredLimit, int numberOfTokens) throws Exception {
testLimitingTokensPerUser(configuredLimit, numberOfTokens, false);
}

private void testLimitingTokensPerUser(int configuredLimit, int numberOfTokens, boolean revokeOldestToken) throws Exception {
final Map<String, String> contextExpectations = new HashMap<>();
contextExpectations.put(KNOX_TOKEN_USER_LIMIT, configuredLimit);
contextExpectations.put(KNOX_TOKEN_USER_LIMIT, String.valueOf(configuredLimit));
if (revokeOldestToken) {
contextExpectations.put(KNOX_TOKEN_USER_LIMIT_EXCEEDED_ACTION, TokenResource.UserLimitExceededAction.REMOVE_OLDEST.name());
}
configureCommonExpectations(contextExpectations, Boolean.TRUE);

final TokenResource tr = new TokenResource();
Expand All @@ -1036,15 +1053,15 @@ private void testLimitingTokensPerUser(String configuredLimit, int numberOfToken
tr.init();

for (int i = 0; i < numberOfTokens; i++) {
final Response getTokenResponse = tr.doGet();
final Response getTokenResponse = Subject.doAs(createTestSubject(USER_NAME), (PrivilegedAction<Response>) () -> tr.doGet());
if (getTokenResponse.getStatus() != Response.Status.OK.getStatusCode()) {
throw new Exception(getTokenResponse.getEntity().toString());
}
}
final Response getKnoxTokensResponse = tr.getUserTokens(USER_NAME);
final Collection<String> tokens = ((Map<String, Collection<String>>) JsonUtils.getObjectFromJsonString(getKnoxTokensResponse.getEntity().toString()))
.get("tokens");
assertEquals(tokens.size(), numberOfTokens);
assertEquals(tokens.size(), revokeOldestToken ? configuredLimit : numberOfTokens);
}

/**
Expand Down Expand Up @@ -1386,6 +1403,10 @@ public void revokeToken(JWTToken token) {

@Override
public void revokeToken(String tokenId) {
issueTimes.remove(tokenId);
expirationData.remove(tokenId);
maxLifetimes.remove(tokenId);
tokenMetadata.remove(tokenId);
}

@Override
Expand Down

0 comments on commit e955cba

Please sign in to comment.