Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

KNOX-2713 - Allowing end-users to customize 'user limit exceeded' action when creating Knox tokens #543

Merged
merged 1 commit into from Mar 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -117,6 +118,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 @@ -654,9 +664,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
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);
}
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 @@ -984,12 +985,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 @@ -1023,16 +1024,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 @@ -1041,15 +1058,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 @@ -1395,6 +1412,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