Skip to content
Snippets Groups Projects
Commit ae2cff5c authored by brinn's avatar brinn
Browse files

Add CachingAuthenticationService with a basic set of unit tests.

SVN: 28149
parent e52f9261
No related branches found
No related tags found
No related merge requests found
/*
* Copyright 2013 ETH Zuerich, CISD
*
* Licensed 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 ch.systemsx.cisd.authentication.file;
import java.io.File;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import ch.systemsx.cisd.authentication.IAuthenticationService;
import ch.systemsx.cisd.authentication.Principal;
import ch.systemsx.cisd.authentication.file.LineBasedUserStore.IUserEntryFactory;
import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException;
import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException;
import ch.systemsx.cisd.common.logging.LogCategory;
import ch.systemsx.cisd.common.logging.LogFactory;
import ch.systemsx.cisd.common.utilities.ITimeProvider;
import ch.systemsx.cisd.common.utilities.SystemTimeProvider;
/**
* An {@link IAuthenticationService} that delegates to another {@link IAuthenticationService} and
* keeps the returned value for user authentication in a local cache which is written out to a
* password cache file which will be used to populate the cache on restart, so the password cache
* survives restarts. Changing the password cache file will change the cache without restart, e.g.
* deleting a line from it will remove the cache entry with immediate effect.
* <p>
* In order to make caching as smooth as possible for regular users of the system, a authentication
* request which is served from the cache under some conditions triggers a re-validation request
* with the delegate authentication service. As re-validation is done asynchronously, it does not
* block the user from working.
* <p>
* Two configurable time periods (in milli-seconds) are relevant:
* <ul>
* <li><code>cacheTimeMillis</code> is the time period after caching that a cache entry is kept.
* Older cache entries are treated as invalid and ignored. The default is 28 hours, which means that
* any user who logs into the system once a day will never have to wait for the delegate
* authentication system to respond as all cache updates are performed in asynchronous
* re-validations.</li>
* <li><code>cacheTimeNoRevalidationMillis</code> is the time period after caching in which
* successful authentication requests do not trigger a re-validation request. The default is 1 hour.
* This feature is meant to reduce the load on the delegate authentication system. Set it to
* <code>cacheTimeMillis</code> to never re-validate or set to 0 to always re-validate.</li>
* </ul>
*
* @author Bernd Rinn
*/
public class CachingAuthenticationService implements IAuthenticationService
{
public final static long ONE_MINUTE = 60 * 1000L;
public final static long ONE_HOUR = 60 * ONE_MINUTE;
private final static long CACHE_TIME_MILLIS_NO_REVALIDATION = ONE_HOUR;
private final static long CACHE_TIME_MILLIS = 28 * ONE_HOUR;
private static final Logger operationLog =
LogFactory.getLogger(LogCategory.OPERATION, CachingAuthenticationService.class);
private enum CacheEntryStatus
{
/** Entry OK without validation. */
OK,
/** Entry OK but should request re-validation. */
OK_REVALIDATE,
/** No entry or entry expired. */
NO_ENTRY,
}
/** A request for validation of a user. */
final class ValidationRequest
{
private final UserCacheEntry user;
private final long queuedAt;
private final String passwordOrNull;
private final boolean authenticated;
ValidationRequest(UserCacheEntry user, String passwordOrNull, boolean authenticated,
long now)
{
this.user = user;
this.passwordOrNull = passwordOrNull;
this.authenticated = authenticated;
this.queuedAt = now;
}
/**
* Returns the user entry to re-validate.
*/
UserCacheEntry getUser()
{
return user;
}
/**
* Returns the password the user supplied, or <code>null</code>, if this is a
* non-authenticating
* validation request.
*/
String tryGetPassword()
{
return passwordOrNull;
}
/**
* Returns <code>true</code> if this request does not include authentication.
*/
boolean withoutAuthentication()
{
return (passwordOrNull == null);
}
/**
* Returns <code>true</code>, if the user has been successfully authenticated from the
* cached entry with the given password.
*/
boolean isAuthenticated()
{
return authenticated;
}
/**
* Returns <code>true</code> if this is a valid request at the time when this method is
* called.
*/
boolean isValid()
{
final UserCacheEntry current = userStore.tryGetUserById(user.getUserId());
// OK, if there is no cache entry.
if (current == null)
{
return true;
}
// Not OK, if the request has no password but the cache entry has one.
if (passwordOrNull == null && current.hasPassword())
{
return false;
}
// OK, if the request has a password but the cache entry has none.
if (passwordOrNull != null && current.hasPassword() == false)
{
return true;
}
// OK, if the time of request queuing is after the time of caching.
return (current.getCachedAt() < queuedAt);
}
}
/**
* The class that performs re-validation of users and their passwords.
*
* @author Bernd Rinn
*/
final class RevalidationRunnable implements Runnable
{
void runOnce() throws InterruptedException
{
final ValidationRequest request = validationQueue.take();
if (request.isValid() == false)
{
return;
}
final String userId = request.getUser().getUserId();
final Principal p =
delegate.tryGetAndAuthenticateUser(userId,
request.tryGetPassword());
// If a user got remove from the delegate system, remove the cache entry.
if (p == null)
{
userStore.removeUser(userId);
if (request.isAuthenticated())
{
operationLog
.warn(String.format("User '%s' has been logged in which is no "
+ "longer a valid user.", userId));
}
return;
}
// Only update the cache if no authentication was requested or if the
// authentication was successful.
if (p.isAuthenticated() || request.withoutAuthentication())
{
userStore.addOrUpdateUser(new UserCacheEntry(p, request
.tryGetPassword(), timeProvider.getTimeInMilliseconds()));
}
if (request.isAuthenticated() && p.isAuthenticated() == false)
{
operationLog.warn(String.format(
"User '%s' has been logged in with an outdated password.",
userId));
}
}
@Override
public void run()
{
while (true)
{
try
{
runOnce();
} catch (Throwable th)
{
operationLog.error(
"Exception in " + CachingAuthenticationService.class.getSimpleName()
+ " Revalidator: ", th);
}
}
}
}
/** A simple interface to authenticate by one type of id. */
interface IAuthenticator
{
Principal tryGetAndAuthenticate(String id, String passwordOrNull);
}
private final IUserStore<UserCacheEntry> userStore;
private final IAuthenticationService delegate;
private final IAuthenticator userIdAuthenticator;
private final IAuthenticator emailAuthenticator;
private final long cacheTimeNoRevalidationMillis;
private final long cacheTimeMillis;
private final BlockingQueue<ValidationRequest> validationQueue;
private final ITimeProvider timeProvider;
CachingAuthenticationService(IAuthenticationService delegate,
String passwordCacheFileName)
{
this(delegate, createUserStore(passwordCacheFileName));
}
CachingAuthenticationService(IAuthenticationService authenticationService,
IUserStore<UserCacheEntry> store)
{
this(authenticationService, store, CACHE_TIME_MILLIS_NO_REVALIDATION,
CACHE_TIME_MILLIS);
}
CachingAuthenticationService(IAuthenticationService delegate,
String passwordCacheFileName,
long cacheTimeNoRevalidationMillis,
long cacheTimeMillis)
{
this(delegate, createUserStore(passwordCacheFileName),
cacheTimeNoRevalidationMillis, cacheTimeMillis);
}
CachingAuthenticationService(IAuthenticationService delegate,
IUserStore<UserCacheEntry> userStore,
long cacheTimeNoRevalidationMillis,
long cacheTimeMillis)
{
this(delegate, userStore, cacheTimeNoRevalidationMillis, cacheTimeMillis, true,
SystemTimeProvider.SYSTEM_TIME_PROVIDER);
}
// For unit tests.
CachingAuthenticationService(IAuthenticationService delegate,
IUserStore<UserCacheEntry> userStore,
long cacheTimeNoRevalidationMillis,
long cacheTimeMillis, boolean startRevalidationThread, ITimeProvider timeProvider)
{
this.delegate = delegate;
this.userIdAuthenticator = new IAuthenticator()
{
@Override
public Principal tryGetAndAuthenticate(String userId, String passwordOrNull)
{
return CachingAuthenticationService.this.delegate.tryGetAndAuthenticateUser(
userId,
passwordOrNull);
}
};
this.emailAuthenticator = new IAuthenticator()
{
@Override
public Principal tryGetAndAuthenticate(String email, String passwordOrNull)
{
return CachingAuthenticationService.this.delegate
.tryGetAndAuthenticateUserByEmail(email, passwordOrNull);
}
};
this.userStore = userStore;
this.cacheTimeNoRevalidationMillis =
Math.min(cacheTimeNoRevalidationMillis, cacheTimeMillis);
this.cacheTimeMillis = cacheTimeMillis;
this.validationQueue = new LinkedBlockingQueue<ValidationRequest>();
this.timeProvider = timeProvider;
if (startRevalidationThread)
{
final Thread t = new Thread(new RevalidationRunnable());
t.setName(getClass().getSimpleName() + " - Validator");
t.setDaemon(true);
t.start();
}
}
static IUserStore<UserCacheEntry> createUserStore(
final String passwordCacheFileName)
{
final ILineStore lineStore =
new FileBasedLineStore(new File(passwordCacheFileName), "Password cache file");
return new LineBasedUserStore<UserCacheEntry>(lineStore,
new IUserEntryFactory<UserCacheEntry>()
{
@Override
public UserCacheEntry create(String line)
{
return new UserCacheEntry(line);
}
});
}
// For unit tests.
BlockingQueue<ValidationRequest> getValidationQueue()
{
return validationQueue;
}
@Override
public boolean authenticateUser(String userId, String password)
{
return Principal.isAuthenticated(tryGetAndAuthenticateUser(userId, password,
userIdAuthenticator, userStore.tryGetUserById(userId)));
}
private Principal tryGetAndAuthenticateUser(String id,
String passwordOrNull, IAuthenticator auth, UserCacheEntry entry)
{
// Note: getStatus() returns NO_ENTRY on entry == null
final boolean requiresAuthentication = StringUtils.isNotEmpty(passwordOrNull);
final long now = timeProvider.getTimeInMilliseconds();
final CacheEntryStatus state =
getStatus(entry, requiresAuthentication, now);
switch (state)
{
case OK:
{
final boolean authenticated = entry.isPasswordCorrect(passwordOrNull);
if (authenticated == false && requiresAuthentication)
{
validationQueue.offer(new ValidationRequest(entry, passwordOrNull, false, now));
}
return toPrincipal(entry, authenticated);
}
case OK_REVALIDATE:
{
final boolean authenticated = entry.isPasswordCorrect(passwordOrNull);
validationQueue.offer(new ValidationRequest(entry, passwordOrNull, authenticated,
now));
return toPrincipal(entry, authenticated);
}
case NO_ENTRY:
{
final Principal p = auth.tryGetAndAuthenticate(id, passwordOrNull);
if (p == null)
{
return null;
}
final UserCacheEntry user =
new UserCacheEntry(p, passwordOrNull, timeProvider.getTimeInMilliseconds());
userStore.addOrUpdateUser(user);
return p;
}
default:
throw new Error("Unknown cache entry state " + state);
}
}
private static Principal toPrincipal(UserEntry userOrNull, boolean authenticated)
{
if (userOrNull == null)
{
return null;
}
final Principal principal = userOrNull.asPrincipal();
principal.setAuthenticated(authenticated);
return principal;
}
private CacheEntryStatus getStatus(UserCacheEntry entry, boolean requirePassword, long now)
{
if (entry == null)
{
return CacheEntryStatus.NO_ENTRY;
}
if (requirePassword && entry.hasPassword() == false)
{
return CacheEntryStatus.NO_ENTRY;
}
final long cachedAt = entry.getCachedAt();
if (cachedAt + cacheTimeNoRevalidationMillis >= now)
{
return CacheEntryStatus.OK;
} else if (cachedAt + cacheTimeMillis >= now)
{
return CacheEntryStatus.OK_REVALIDATE;
} else
{
// Treat an expired entry like a missing entry.
return CacheEntryStatus.NO_ENTRY;
}
}
@Override
@Deprecated
public boolean authenticateUser(String dummyToken, String userId, String password)
{
return authenticateUser(userId, password);
}
@Override
public Principal tryGetAndAuthenticateUser(String userId, String passwordOrNull)
{
return tryGetAndAuthenticateUser(userId, passwordOrNull, userIdAuthenticator,
userStore.tryGetUserById(userId));
}
@Override
@Deprecated
public Principal tryGetAndAuthenticateUser(String dummyToken, String userId,
String passwordOrNull)
{
return tryGetAndAuthenticateUser(userId, passwordOrNull);
}
@Override
public Principal getPrincipal(String userId) throws IllegalArgumentException
{
final Principal principalOrNull =
tryGetAndAuthenticateUser(userId, null, userIdAuthenticator,
userStore.tryGetUserById(userId));
if (principalOrNull == null)
{
throw new IllegalArgumentException("Cannot find user '" + userId + "'.");
}
return principalOrNull;
}
@Override
@Deprecated
public Principal getPrincipal(String dummyToken, String userId) throws IllegalArgumentException
{
return getPrincipal(userId);
}
@Override
public Principal tryGetAndAuthenticateUserByEmail(String email, String passwordOrNull)
{
return tryGetAndAuthenticateUser(email, passwordOrNull, emailAuthenticator,
userStore.tryGetUserByEmail(email));
}
@Override
@Deprecated
public Principal tryGetAndAuthenticateUserByEmail(String dummyToken, String email,
String passwordOrNull)
{
return tryGetAndAuthenticateUserByEmail(email, passwordOrNull);
}
@Override
public boolean supportsAuthenticatingByEmail()
{
return delegate.supportsAuthenticatingByEmail();
}
@Override
public boolean supportsListingByUserId()
{
return delegate.supportsListingByUserId();
}
@Override
public List<Principal> listPrincipalsByUserId(String userIdQuery)
throws IllegalArgumentException
{
return delegate.listPrincipalsByUserId(userIdQuery);
}
@Override
public boolean supportsListingByEmail()
{
return delegate.supportsListingByEmail();
}
@Override
public List<Principal> listPrincipalsByEmail(String emailQuery) throws IllegalArgumentException
{
return delegate.listPrincipalsByEmail(emailQuery);
}
@Override
public boolean supportsListingByLastName()
{
return delegate.supportsListingByLastName();
}
@Override
public List<Principal> listPrincipalsByLastName(String lastNameQuery)
throws IllegalArgumentException
{
return delegate.listPrincipalsByLastName(lastNameQuery);
}
@Override
@Deprecated
public String authenticateApplication()
{
return delegate.authenticateApplication();
}
@Override
@Deprecated
public List<Principal> listPrincipalsByUserId(String dummyToken, String userIdQuery)
throws IllegalArgumentException
{
return delegate.listPrincipalsByUserId(dummyToken, userIdQuery);
}
@Override
@Deprecated
public List<Principal> listPrincipalsByEmail(String dummyToken, String emailQuery)
throws IllegalArgumentException
{
return delegate.listPrincipalsByEmail(dummyToken, emailQuery);
}
@Override
@Deprecated
public List<Principal> listPrincipalsByLastName(String dummyToken, String lastNameQuery)
throws IllegalArgumentException
{
return delegate.listPrincipalsByLastName(dummyToken, lastNameQuery);
}
@Override
public boolean isRemote()
{
return delegate.isRemote();
}
@Override
public void check() throws EnvironmentFailureException, ConfigurationFailureException
{
userStore.check();
delegate.check();
}
}
/*
* Copyright 2013 ETH Zuerich, CISD
*
* Licensed 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 ch.systemsx.cisd.authentication.file;
import org.apache.commons.lang.StringUtils;
import ch.systemsx.cisd.authentication.Principal;
import ch.systemsx.cisd.common.security.PasswordHasher;
/**
* A class representing a user entry in the password cache file.
*
* @author Bernd Rinn
*/
public class UserCacheEntry extends UserEntry
{
// For unit tests.
static final int NUMBER_OF_COLUMNS_IN_PASSWORD_CACHE_FILE = 6;
private static final int CACHED_AT_IDX = 5;
private final long cachedAt;
UserCacheEntry(String passwordFileLine) throws IllegalArgumentException
{
super(split(passwordFileLine, NUMBER_OF_COLUMNS_IN_PASSWORD_CACHE_FILE));
this.cachedAt = Long.parseLong(getElement(CACHED_AT_IDX));
}
UserCacheEntry(Principal p, long cachedAt)
{
this(p, null, cachedAt);
}
UserCacheEntry(Principal p, String passwordOrNull, long cachedAt)
{
super(createPasswordFileEntry(p, passwordOrNull, cachedAt));
this.cachedAt = cachedAt;
}
private static String[] createPasswordFileEntry(Principal p, String passwordOrNull,
long cachedAt)
{
assert p != null;
return new String[]
{
p.getUserId(),
p.getEmail(),
p.getFirstName(),
p.getLastName(),
(p.isAuthenticated() && StringUtils.isNotEmpty(passwordOrNull)) ? PasswordHasher
.computeSaltedHash(passwordOrNull)
: "",
Long.toString(cachedAt),
};
}
/**
* Returns the time stamp when this cache entry was put into the cache.
*/
long getCachedAt()
{
return cachedAt;
}
/**
* @throw {@link UnsupportedOperationException}
*/
@Override
void setEmail(String email) throws UnsupportedOperationException
{
throw new UnsupportedOperationException();
}
/**
* @throw {@link UnsupportedOperationException}
*/
@Override
void setFirstName(String firstName) throws UnsupportedOperationException
{
throw new UnsupportedOperationException();
}
/**
* @throw {@link UnsupportedOperationException}
*/
@Override
void setLastName(String lastName) throws UnsupportedOperationException
{
throw new UnsupportedOperationException();
}
/**
* @throw {@link UnsupportedOperationException}
*/
@Override
void setPassword(String plainPassword) throws UnsupportedOperationException
{
throw new UnsupportedOperationException();
}
}
/*
* Copyright 2013 ETH Zuerich, CISD
*
* Licensed 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 ch.systemsx.cisd.authentication.file;
import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.assertFalse;
import static org.testng.AssertJUnit.assertNotNull;
import static org.testng.AssertJUnit.assertNull;
import static org.testng.AssertJUnit.assertTrue;
import java.io.File;
import java.util.List;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import org.jmock.Expectations;
import org.jmock.Mockery;
import org.testng.annotations.AfterClass;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import ch.systemsx.cisd.authentication.IAuthenticationService;
import ch.systemsx.cisd.authentication.Principal;
import ch.systemsx.cisd.common.filesystem.FileUtilities;
import ch.systemsx.cisd.common.utilities.ITimeProvider;
/**
* Test cases for the {@link CachingAuthenticationService} with invalid login attempts.
*
* @author Bernd Rinn
*/
public class CachingAuthenticationServiceInvalidLoginTests
{
private static final File workingDirectory =
new File("targets/unit-test-wd/CachingAuthenticationServiceInvalidLoginTests");
private static final File PASSWD_FILE = new File(workingDirectory, "passwd");
private static final String PASSWD_FILENAME = PASSWD_FILE.getPath();
private static final long CACHE_TIME_NO_REVAL_MILLIS = 1000L;
private static final long CACHE_TIME_MILLIS = 2000L;
private static final String user = "User";
private static final String firstName = "First Name";
private static final String lastName = "Last Name";
private static final String email = "e@mail";
private static final String invalidPassword = "passw0rd";
private static final String validPassword = "s@crit";
private static final long timeStart = 450L;
private static final long timeCache = 500L;
private static final long time2 = 550L;
private static final long time2Cache = 600L;
private static final long time3 = 650L;
private static final long time4 = 700L;
private static final long time5 = 800L;
private Mockery context;
private IAuthenticationService delegateService;
private ITimeProvider timeProvider;
private CachingAuthenticationService service;
private CachingAuthenticationService.RevalidationRunnable revalidator;
@BeforeClass
public void setUp()
{
FileUtilities.deleteRecursively(workingDirectory);
workingDirectory.mkdirs();
context = new Mockery();
delegateService = context.mock(IAuthenticationService.class);
timeProvider = context.mock(ITimeProvider.class);
service =
new CachingAuthenticationService(delegateService,
CachingAuthenticationService.createUserStore(PASSWD_FILENAME),
CACHE_TIME_NO_REVAL_MILLIS, CACHE_TIME_MILLIS, false, timeProvider);
revalidator = service.new RevalidationRunnable();
}
@AfterMethod
public void tearDown()
{
context.assertIsSatisfied();
}
@AfterClass
public void cleanUp()
{
FileUtilities.deleteRecursively(workingDirectory);
}
@Test
public void testAuthenticateUserFirstTimeUserDoesntExist()
{
PASSWD_FILE.delete();
context.checking(new Expectations()
{
{
one(timeProvider).getTimeInMilliseconds();
will(returnValue(timeStart));
one(delegateService).tryGetAndAuthenticateUser(user, invalidPassword);
will(returnValue(null));
}
});
final Principal p = service.tryGetAndAuthenticateUser(user, invalidPassword);
assertNull(p);
checkCacheEmpty();
assertTrue(service.getValidationQueue().isEmpty());
context.assertIsSatisfied();
}
// Note: subsequent test methods depend on testAuthenticateUserFirstTimeUserDoesntExist() not
// being called after this method, so we force a dependency.
@Test(dependsOnMethods = "testAuthenticateUserFirstTimeUserDoesntExist")
public void testAuthenticateUserFirstTimeInvalidPassword()
{
PASSWD_FILE.delete();
context.checking(new Expectations()
{
{
one(timeProvider).getTimeInMilliseconds();
will(returnValue(timeStart));
one(delegateService).tryGetAndAuthenticateUser(user, invalidPassword);
will(returnValue(new Principal(user, firstName, lastName, email, false)));
one(timeProvider).getTimeInMilliseconds();
will(returnValue(timeCache));
}
});
final Principal p = service.tryGetAndAuthenticateUser(user, invalidPassword);
assertNotNull(p);
checkPrincipalUser(user, firstName, lastName, email, false, p);
checkCache(user, firstName, lastName, email, timeCache, false);
assertTrue(service.getValidationQueue().isEmpty());
context.assertIsSatisfied();
}
@Test(dependsOnMethods = "testAuthenticateUserFirstTimeInvalidPassword")
public void testGetPrincipalAfterCacheWithoutPassword()
{
context.checking(new Expectations()
{
{
one(timeProvider).getTimeInMilliseconds();
will(returnValue(time2));
}
});
final Principal p = service.tryGetAndAuthenticateUser(user, null);
assertNotNull(p);
checkPrincipalUser(user, firstName, lastName, email, false, p);
checkCache(user, firstName, lastName, email, timeCache, false);
assertTrue(service.getValidationQueue().isEmpty());
context.assertIsSatisfied();
}
@Test(dependsOnMethods = "testGetPrincipalAfterCacheWithoutPassword")
public void testAuthenticateUserSecondTimeValidPassword()
{
context.checking(new Expectations()
{
{
one(timeProvider).getTimeInMilliseconds();
will(returnValue(time3));
// Cannot use the cached entry because it doesn't have a valid password hash.
one(delegateService).tryGetAndAuthenticateUser(user, validPassword);
will(returnValue(new Principal(user, firstName, lastName, email, true)));
one(timeProvider).getTimeInMilliseconds();
will(returnValue(time2Cache));
}
});
final Principal p = service.tryGetAndAuthenticateUser(user, validPassword);
assertNotNull(p);
checkPrincipalUser(user, firstName, lastName, email, true, p);
checkCache(user, firstName, lastName, email, time2Cache, true);
assertTrue(service.getValidationQueue().isEmpty());
context.assertIsSatisfied();
}
@Test(dependsOnMethods = "testGetPrincipalAfterCacheWithoutPassword")
public void testAuthenticateUserThirdTimeInvalidPassword() throws InterruptedException
{
context.checking(new Expectations()
{
{
one(timeProvider).getTimeInMilliseconds();
will(returnValue(time4));
}
});
final Principal p = service.tryGetAndAuthenticateUser(user, invalidPassword);
assertNotNull(p);
checkPrincipalUser(user, firstName, lastName, email, false, p);
checkCache(user, firstName, lastName, email, time2Cache, true);
assertFalse(service.getValidationQueue().isEmpty());
// Run validator with request, but the password is invalid so the result will not be cached.
context.checking(new Expectations()
{
{
one(delegateService).tryGetAndAuthenticateUser(user, invalidPassword);
will(returnValue(new Principal(user, firstName, lastName, email, false)));
}
});
revalidator.runOnce();
checkCache(user, firstName, lastName, email, time2Cache, true);
// A valid call is still served from the cache.
context.checking(new Expectations()
{
{
one(timeProvider).getTimeInMilliseconds();
will(returnValue(time5));
}
});
final Principal p2 = service.tryGetAndAuthenticateUser(user, validPassword);
assertNotNull(p2);
checkPrincipalUser(user, firstName, lastName, email, true, p2);
checkCache(user, firstName, lastName, email, time2Cache, true);
context.assertIsSatisfied();
}
@SuppressWarnings("hiding")
private void checkPrincipalUser(final String user, final String firstName,
final String lastName, final String email, boolean authenticated, Principal p)
{
assertNotNull(p);
assertEquals(user, p.getUserId());
assertEquals(firstName, p.getFirstName());
assertEquals(lastName, p.getLastName());
assertEquals(email, p.getEmail());
assertEquals(authenticated, p.isAuthenticated());
}
private void checkCacheEmpty()
{
assertTrue(PASSWD_FILE.length() == 0);
}
@SuppressWarnings("hiding")
private void checkCache(final String user, final String firstName,
final String lastName, final String email, final long timeCache, final boolean withHash)
{
assertTrue(PASSWD_FILE.exists());
assertTrue(PASSWD_FILE.length() > 0);
final List<String> passwdLines = FileUtilities.loadToStringList(PASSWD_FILE, null);
assertEquals(1, passwdLines.size());
final String[] split = StringUtils.splitPreserveAllTokens(passwdLines.get(0), ':');
assertEquals(UserCacheEntry.NUMBER_OF_COLUMNS_IN_PASSWORD_CACHE_FILE, split.length);
assertEquals(user, split[0]);
assertEquals(email, split[1]);
assertEquals(firstName, split[2]);
assertEquals(lastName, split[3]);
if (withHash)
{
assertTrue(split[4], Pattern.matches("[\\w\\/,\\+]+", split[4]));
} else
{
assertEquals("", split[4]);
}
assertEquals(Long.toString(timeCache), split[5]);
}
}
/*
* Copyright 2013 ETH Zuerich, CISD
*
* Licensed 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 ch.systemsx.cisd.authentication.file;
import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.assertFalse;
import static org.testng.AssertJUnit.assertNotNull;
import static org.testng.AssertJUnit.assertNull;
import static org.testng.AssertJUnit.assertTrue;
import java.io.File;
import java.util.List;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import org.jmock.Expectations;
import org.jmock.Mockery;
import org.testng.annotations.AfterClass;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import ch.systemsx.cisd.authentication.IAuthenticationService;
import ch.systemsx.cisd.authentication.Principal;
import ch.systemsx.cisd.common.filesystem.FileUtilities;
import ch.systemsx.cisd.common.utilities.ITimeProvider;
/**
* Test cases for the {@link CachingAuthenticationService} with successful logins.
*
* @author Bernd Rinn
*/
public class CachingAuthenticationServiceSuccessTests
{
private static final File workingDirectory =
new File("targets/unit-test-wd/CachingAuthenticationServiceSuccessTests");
private static final File PASSWD_FILE = new File(workingDirectory, "passwd");
private static final String PASSWD_FILENAME = PASSWD_FILE.getPath();
private static final long CACHE_TIME_NO_REVAL_MILLIS = 1000L;
private static final long CACHE_TIME_MILLIS = 2000L;
private static final String user = "User";
private static final String firstName = "First Name";
private static final String lastName = "Last Name";
private static final String email = "e@mail";
private static final String email2 = "e@mail2";
private static final String email3 = "e@mail3";
private static final String password = "passw0rd";
private static final long timeStart = 450L;
private static final long timeCache = 500L;
private static final long timeSecondRequest = 800L;
private static final long timeThirdRequest = 1200L;
private static final long timeForthRequest = 1750L; // Triggers re-validation
private static final long time2Cache = 1800L; // In re-validation
private static final long timeFifthRequest = 1900L; // That's after re-validation from the
// cache.
private static final long timeSixthRequest = 9950L; // Triggers expiration
private static final long time3Cache = 10000L; // New cache after expiration
private Mockery context;
private IAuthenticationService delegateService;
private ITimeProvider timeProvider;
private CachingAuthenticationService service;
private CachingAuthenticationService.RevalidationRunnable revalidator;
@BeforeClass
public void setUp()
{
FileUtilities.deleteRecursively(workingDirectory);
workingDirectory.mkdirs();
context = new Mockery();
delegateService = context.mock(IAuthenticationService.class);
timeProvider = context.mock(ITimeProvider.class);
service =
new CachingAuthenticationService(delegateService,
CachingAuthenticationService.createUserStore(PASSWD_FILENAME),
CACHE_TIME_NO_REVAL_MILLIS, CACHE_TIME_MILLIS, false, timeProvider);
revalidator = service.new RevalidationRunnable();
}
@AfterMethod
public void tearDown()
{
context.assertIsSatisfied();
}
@AfterClass
public void cleanUp()
{
FileUtilities.deleteRecursively(workingDirectory);
}
@Test
public void testAuthenticateUserFirstTimeSuccess()
{
context.checking(new Expectations()
{
{
one(timeProvider).getTimeInMilliseconds();
will(returnValue(timeStart));
one(delegateService).tryGetAndAuthenticateUser(user, password);
will(returnValue(new Principal(user, firstName, lastName, email, true)));
one(timeProvider).getTimeInMilliseconds();
will(returnValue(timeCache));
}
});
final Principal p = service.tryGetAndAuthenticateUser(user, password);
checkPrincipalHappyCase(user, firstName, lastName, email, p);
checkCacheHappyCase(user, firstName, lastName, email, timeCache);
assertTrue(service.getValidationQueue().isEmpty());
context.assertIsSatisfied();
}
@Test(dependsOnMethods = "testAuthenticateUserFirstTimeSuccess")
public void testAuthenticateUserFromCacheStatusOKSuccess()
{
// Now it has to be answered from the cache.
context.checking(new Expectations()
{
{
one(timeProvider).getTimeInMilliseconds();
will(returnValue(timeSecondRequest));
}
});
final Principal p = service.tryGetAndAuthenticateUser(user, password);
checkPrincipalHappyCase(user, firstName, lastName, email, p);
checkCacheHappyCase(user, firstName, lastName, email, timeCache);
assertTrue(service.getValidationQueue().isEmpty());
context.assertIsSatisfied();
}
@Test(dependsOnMethods = "testAuthenticateUserFromCacheStatusOKSuccess")
public void testAuthenticateUserFromCacheByEmailStatusOKSuccess()
{
// Now it has to be answered from the cache.
context.checking(new Expectations()
{
{
one(timeProvider).getTimeInMilliseconds();
will(returnValue(timeThirdRequest));
}
});
final Principal p = service.tryGetAndAuthenticateUserByEmail(email.toUpperCase(), password);
checkPrincipalHappyCase(user, firstName, lastName, email, p);
checkCacheHappyCase(user, firstName, lastName, email, timeCache);
assertTrue(service.getValidationQueue().isEmpty());
context.assertIsSatisfied();
}
@Test(dependsOnMethods = "testAuthenticateUserFromCacheByEmailStatusOKSuccess")
public void testAuthenticateUserFromCacheStatusOKRevalSuccess() throws InterruptedException
{
// Now it has to be answered from the cache, but re-validation is required.
context.checking(new Expectations()
{
{
one(timeProvider).getTimeInMilliseconds();
will(returnValue(timeForthRequest));
}
});
final Principal p = service.tryGetAndAuthenticateUser(user, password);
checkPrincipalHappyCase(user, firstName, lastName, email, p);
checkCacheHappyCase(user, firstName, lastName, email, timeCache);
assertFalse(service.getValidationQueue().isEmpty());
// Perform revalidation
context.checking(new Expectations()
{
{
one(delegateService).tryGetAndAuthenticateUser(user, password);
will(returnValue(new Principal(user, firstName, lastName, email2, true)));
one(timeProvider).getTimeInMilliseconds();
will(returnValue(time2Cache));
}
});
revalidator.runOnce();
assertTrue(service.getValidationQueue().isEmpty());
checkCacheHappyCase(user, firstName, lastName, email2, time2Cache);
// Now it has to be answered from the cache again.
context.checking(new Expectations()
{
{
one(timeProvider).getTimeInMilliseconds();
will(returnValue(timeFifthRequest));
}
});
final Principal p2 =
service.tryGetAndAuthenticateUserByEmail(email2.toUpperCase(), password);
checkPrincipalHappyCase(user, firstName, lastName, email2, p2);
checkCacheHappyCase(user, firstName, lastName, email2, time2Cache);
assertTrue(service.getValidationQueue().isEmpty());
// Check that email is gone from the cache.
context.checking(new Expectations()
{
{
one(timeProvider).getTimeInMilliseconds();
will(returnValue(timeFifthRequest));
one(delegateService).tryGetAndAuthenticateUserByEmail(email.toUpperCase(),
password);
will(returnValue(null));
}
});
final Principal p3 =
service.tryGetAndAuthenticateUserByEmail(email.toUpperCase(), password);
assertNull(p3);
context.assertIsSatisfied();
}
@Test(dependsOnMethods = "testAuthenticateUserFromCacheStatusOKRevalSuccess")
public void testAuthenticateUserAfterCacheExpiratonSuccess()
{
context.checking(new Expectations()
{
{
one(timeProvider).getTimeInMilliseconds();
will(returnValue(timeSixthRequest));
one(delegateService).tryGetAndAuthenticateUser(user, password);
will(returnValue(new Principal(user, firstName, lastName, email3, true)));
one(timeProvider).getTimeInMilliseconds();
will(returnValue(time3Cache));
}
});
final Principal p = service.tryGetAndAuthenticateUser(user, password);
checkPrincipalHappyCase(user, firstName, lastName, email3, p);
checkCacheHappyCase(user, firstName, lastName, email3, time3Cache);
assertTrue(service.getValidationQueue().isEmpty());
context.assertIsSatisfied();
}
@SuppressWarnings("hiding")
private void checkPrincipalHappyCase(final String user, final String firstName,
final String lastName, final String email, Principal p)
{
assertNotNull(p);
assertEquals(user, p.getUserId());
assertEquals(firstName, p.getFirstName());
assertEquals(lastName, p.getLastName());
assertEquals(email, p.getEmail());
assertTrue(p.isAuthenticated());
}
@SuppressWarnings("hiding")
private void checkCacheHappyCase(final String user, final String firstName,
final String lastName,
final String email, final long timeCache)
{
assertTrue(PASSWD_FILE.exists());
assertTrue(PASSWD_FILE.length() > 0);
final List<String> passwdLines = FileUtilities.loadToStringList(PASSWD_FILE, null);
assertEquals(1, passwdLines.size());
final String[] split = StringUtils.split(passwdLines.get(0), ':');
assertEquals(UserCacheEntry.NUMBER_OF_COLUMNS_IN_PASSWORD_CACHE_FILE, split.length);
assertEquals(user, split[0]);
assertEquals(email, split[1]);
assertEquals(firstName, split[2]);
assertEquals(lastName, split[3]);
assertTrue(split[4], Pattern.matches("[\\w\\/,\\+]+", split[4]));
assertEquals(Long.toString(timeCache), split[5]);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment