Index: oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenProviderImpl.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenProviderImpl.java (revision 1814497) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenProviderImpl.java (revision ) @@ -100,6 +100,17 @@ private static final Logger log = LoggerFactory.getLogger(TokenProviderImpl.class); /** + * Optional configuration parameter to define the number of token nodes that + * when exceeded will trigger a cleanup of expired tokens upon creation. + */ + static final String PARAM_TOKEN_CLEANUP_THRESHOLD = "tokenCleanupThreshold"; + + /** + * Default value indicating that tokens should never be cleaned up (i.e. backwards compatible behavior). + */ + static final long NO_TOKEN_CLEANUP = 0; + + /** * Default expiration time in ms for login tokens is 2 hours. */ static final long DEFAULT_TOKEN_EXPIRATION = 2 * 3600 * 1000; @@ -226,6 +237,9 @@ tokenInfo = createTokenNode(tokenParent, UUID.randomUUID().toString(), expTime, uuid, id, attributes); root.commit(CommitMarker.asCommitAttributes()); } + + cleanupExpired(tokenParent, creationTime); + return tokenInfo; } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { // error while generating login token @@ -278,6 +292,10 @@ return TreeUtil.getLong(tokenTree, TOKEN_ATTRIBUTE_EXPIRY, defaultValue); } + private static boolean isExpired(long expirationTime, long loginTime) { + return expirationTime < loginTime; + } + private static void setExpirationTime(@Nonnull Tree tree, long time) { Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(time); @@ -433,6 +451,37 @@ return new TokenInfoImpl(tokenNode, token, id, null); } + /** + * Remove expired token nodes if the configured threshold (i.e. number of + * token nodes) is matched/exceeded. By default (i.e. unless configured with + * a value bigger than {@link #NO_TOKEN_CLEANUP}) no cleanup is performed and + * this method returns without looking at the token nodes; this makes this + * addition optional and will not affect existing configurations. + * + * @param parent The token parent node. + * @param currentTime The time to be used for analysing expiration of existing tokens. + */ + private void cleanupExpired(@Nonnull Tree parent, long currentTime) { + long cleanupThreshold = options.getConfigValue(PARAM_TOKEN_CLEANUP_THRESHOLD, NO_TOKEN_CLEANUP); + if (cleanupThreshold > NO_TOKEN_CLEANUP) { + try { + if (parent.getChildrenCount(cleanupThreshold) >= cleanupThreshold) { + for (Tree child : parent.getChildren()) { + if (isExpired(getExpirationTime(child, Long.MIN_VALUE), currentTime)) { + child.remove(); + } + } + } + if (root.hasPendingChanges()) { + root.commit(CommitMarker.asCommitAttributes()); + } + } catch (CommitFailedException e) { + log.debug("Failed to cleanup expired token nodes", e); + root.refresh(); + } + } + } + //-------------------------------------------------------------------------- /** @@ -498,7 +547,7 @@ @Override public boolean isExpired(long loginTime) { - return expirationTime < loginTime; + return TokenProviderImpl.isExpired(expirationTime, loginTime); } @Override Index: oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/token/TokenCleanupTest.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/token/TokenCleanupTest.java (revision ) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/security/authentication/token/TokenCleanupTest.java (revision ) @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.security.authentication.token; + +import javax.annotation.Nonnull; + +import com.google.common.collect.ImmutableMap; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; +import org.apache.jackrabbit.oak.spi.security.authentication.token.TokenConstants; +import org.apache.jackrabbit.oak.spi.security.authentication.token.TokenInfo; +import org.apache.jackrabbit.oak.spi.security.authentication.token.TokenProvider; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class TokenCleanupTest extends AbstractTokenTest { + + private String userId; + + @Override + public void before() throws Exception { + super.before(); + + userId = getTestUser().getID(); + } + + @Override + ConfigurationParameters getTokenConfig() { + return ConfigurationParameters.of(TokenProviderImpl.PARAM_TOKEN_CLEANUP_THRESHOLD, 5); + } + + private void assertTokenNodes(int expectedNumber) throws Exception { + Tree tokenParent = root.getTree(getTestUser().getPath() + '/' + TokenConstants.TOKENS_NODE_NAME); + assertEquals(expectedNumber, tokenParent.getChildrenCount(expectedNumber*2)); + } + + + private void createExpiredTokens(int numberOfTokens) throws Exception { + for (int i = 0; i < numberOfTokens; i++) { + TokenInfo tokenInfo = tokenProvider.createToken(userId, ImmutableMap.of(TokenProvider.PARAM_TOKEN_EXPIRATION, 2)); + // wait until the info created has expired + if (tokenInfo != null) { + waitUntilExpired(tokenInfo); + } + } + } + + private void waitUntilExpired(@Nonnull TokenInfo info) { + long now = System.currentTimeMillis(); + while (!info.isExpired(now)) { + now = waitForSystemTimeIncrement(now); + } + } + + @Test + public void testExpiredBelowThreshold() throws Exception { + createExpiredTokens(4); + assertTokenNodes(4); + } + + @Test + public void testAllExpiredReachingThreshold() throws Exception { + createExpiredTokens(5); + assertTokenNodes(1); + } + + @Test + public void testSomeExpiredReachingThreshold() throws Exception { + createExpiredTokens(3); + tokenProvider.createToken(userId, ImmutableMap.of()); + + assertTokenNodes(4); + + tokenProvider.createToken(userId, ImmutableMap.of()); + assertTokenNodes(2); + } + + @Test + public void testNotExpiredReachingThreshold() throws Exception { + for (int i = 0; i < 10; i++) { + tokenProvider.createToken(userId, ImmutableMap.of()); + } + assertTokenNodes(10); + } +} \ No newline at end of file Index: oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenConfigurationImpl.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenConfigurationImpl.java (revision 1814497) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/security/authentication/token/TokenConfigurationImpl.java (revision ) @@ -82,6 +82,13 @@ boolean tokenRefresh() default true; @AttributeDefinition( + name = "Token Cleanup Threshold", + description = "Setting this option to a value > 0 will trigger a cleanup upon token creation: " + + "if the number of existing token matches/exceeds the " + + "configured value an attempt is made to removed expired tokens.") + long tokenCleanupThreshold() default TokenProviderImpl.NO_TOKEN_CLEANUP; + + @AttributeDefinition( name = "Hash Algorithm", description = "Name of the algorithm to hash the token.") String passwordHashAlgorithm() default PasswordUtil.DEFAULT_ALGORITHM; \ No newline at end of file