Skip to content
Snippets Groups Projects
Commit 7d642803 authored by felmer's avatar felmer
Browse files

LMS-445 introducing DefaultSessionManager in 'authentication' based on the...

LMS-445 introducing DefaultSessionManager in 'authentication' based on the code of ch.systemsx.cisd.lims.webservice.DefaultSessionManager

SVN: 8227
parent 4f75007f
No related branches found
No related tags found
No related merge requests found
/*
* Copyright 2008 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;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.time.DateUtils;
import org.apache.commons.lang.time.DurationFormatUtils;
import org.apache.log4j.Logger;
import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException;
import ch.systemsx.cisd.common.exceptions.InvalidSessionException;
import ch.systemsx.cisd.common.exceptions.UserFailureException;
import ch.systemsx.cisd.common.logging.LogCategory;
import ch.systemsx.cisd.common.logging.LogFactory;
import ch.systemsx.cisd.common.server.IRemoteHostProvider;
import ch.systemsx.cisd.common.utilities.TokenGenerator;
/**
* Default session manager. Needs
* <ul><li>a {@link ISessionFactory} for creating new session objects,
* <li>a {@link ILogMessagePrefixGenerator} for generating log messages which are
* logged by a logger with category {@link LogCategory#AUTH},
* <li>a {@link IAuthenticationService} for authenticating users,
* <li>a {@link IRemoteHostProvider} for providing the remote host of the user client.
* </ul>
*
* @author Franz-Josef Elmer
*/
public class DefaultSessionManager<T extends BasicSession> implements ISessionManager<T>
{
private static final String LOGOUT_PREFIX = "LOGOUT: ";
private static final String LOGIN_PREFIX = "LOGIN: ";
private static final Logger authenticationLog =
LogFactory.getLogger(LogCategory.AUTH, DefaultSessionManager.class);
private static final Logger operationLog =
LogFactory.getLogger(LogCategory.OPERATION, DefaultSessionManager.class);
private static final TokenGenerator tokenGenerator = new TokenGenerator();
private static final class FullSession<S>
{
/** Session data. */
private final S session;
/** The time period of inactivity (in milliseconds) after which the session will expire. */
private final long expirationPeriodMillis;
/** The last time when this session has been used (in milliseconds since 1970-01-01). */
private long lastActiveTime;
FullSession(final S session, final long expirationPeriodMillis)
{
assert session != null : "Undefined session";
assert expirationPeriodMillis >= 0; // == 0 is for unit tests
this.session = session;
this.expirationPeriodMillis = expirationPeriodMillis;
touch();
}
/**
* Returns the session.
*/
public S getSession()
{
return session;
}
/**
* Sets the time of last activity (used to determine whether the session
* {@link #hasExpired()}.
*/
void touch()
{
this.lastActiveTime = System.currentTimeMillis();
}
/**
* Returns <code>true</code> if the session has expired.
*/
boolean hasExpired()
{
return System.currentTimeMillis() - lastActiveTime > expirationPeriodMillis;
}
}
private final ISessionFactory<T> sessionFactory;
private final ILogMessagePrefixGenerator<T> prefixGenerator;
/**
* The map of session tokens to sessions. Access to this data structure needs to be
* synchronized.
*/
private final Map<String, FullSession<T>> sessions = new LinkedHashMap<String, FullSession<T>>();
private final IAuthenticationService authenticationService;
private final IRemoteHostProvider remoteHostProvider;
/** The time after which an inactive session will be expired (in milliseconds). */
private final long sessionExpirationPeriodMillis;
public DefaultSessionManager(final ISessionFactory<T> sessionFactory,
final ILogMessagePrefixGenerator<T> prefixGenerator,
final IAuthenticationService authenticationService,
final IRemoteHostProvider remoteHostProvider, final int sessionExpirationPeriodMinutes)
{
assert sessionFactory != null : "Missing session factory.";
assert prefixGenerator != null : "Missing prefix generator";
assert authenticationService != null : "Missing authentication service.";
assert remoteHostProvider != null : "Missing remote host provider.";
assert sessionExpirationPeriodMinutes >= 0 : "Session experation time has to be a positive value: "
+ sessionExpirationPeriodMinutes; // == 0 is for unit test
this.sessionFactory = sessionFactory;
this.prefixGenerator = prefixGenerator;
this.authenticationService = authenticationService;
this.remoteHostProvider = remoteHostProvider;
sessionExpirationPeriodMillis =
sessionExpirationPeriodMinutes * DateUtils.MILLIS_PER_MINUTE;
operationLog.info(String.format("Authentication service: '%s'", authenticationService
.getClass().getName()));
operationLog.info(String.format("Session expiration period (ms): %d",
sessionExpirationPeriodMillis));
authenticationService.check();
}
private final T createAndStoreSession(final String user, final Principal principal,
final long now)
{
final String sessionToken = user + "-" + tokenGenerator.getNewToken(now);
synchronized (sessions)
{
final T session =
sessionFactory.create(sessionToken, user, principal, getRemoteHost(), now);
final FullSession<T> createdSession =
new FullSession<T>(session, sessionExpirationPeriodMillis);
sessions.put(user, createdSession);
return session;
}
}
private static void checkIfNotBlank(final String object, final String name)
throws UserFailureException
{
if (StringUtils.isBlank(object))
{
throw UserFailureException.fromTemplate("No '%s' specified.", name);
}
}
private boolean isSessionUnavailable(final FullSession<T> session)
{
return session == null || doSessionExpiration(session);
}
private boolean doSessionExpiration(final FullSession<T> session)
{
return session != null && session.hasExpired();
}
private void logAuthenticed(T session)
{
if (operationLog.isInfoEnabled())
{
operationLog.info(LOGIN_PREFIX + "User '" + session.getUserName()
+ "' has been successfully authenticated from host '" + getRemoteHost()
+ "'. Session token: '" + session.getSessionToken() + "'.");
}
String prefix = prefixGenerator.createPrefix(session);
authenticationLog.info(prefix + ": login");
}
private void logFailedAuthentication(String user)
{
operationLog.warn(LOGIN_PREFIX + "User '" + user + "' failed to authenticate from host '"
+ getRemoteHost() + "'.");
logAuthenticationFailure(user);
}
private void logSessionFailure(String user, RuntimeException ex)
{
logAuthenticationFailure(user);
operationLog.error(LOGIN_PREFIX + "Error when trying to authenticate user '" + user + "'.",
ex);
}
private void logAuthenticationFailure(String user)
{
String prefix = prefixGenerator.createPrefix(user, getRemoteHost());
authenticationLog.info(prefix + ": login ...FAILED");
}
private void logSessionExpired(FullSession<T> fullSession)
{
T session = fullSession.getSession();
if (operationLog.isInfoEnabled())
{
operationLog.info(String.format("%sExpiring session '%s' for user '%s' "
+ "after %d minutes of inactivity.", LOGOUT_PREFIX,
session.getSessionToken(), session.getUserName(),
sessionExpirationPeriodMillis / DateUtils.MILLIS_PER_MINUTE));
}
String prefix = prefixGenerator.createPrefix(session);
authenticationLog.info(prefix + ": session_expired [inactive "
+ DurationFormatUtils.formatDurationHMS(sessionExpirationPeriodMillis) + "]");
}
private void logLogout(T session)
{
String prefix = prefixGenerator.createPrefix(session);
authenticationLog.info(prefix + ": logout");
if (operationLog.isInfoEnabled())
{
String user = session.getUserName();
operationLog.info(LOGOUT_PREFIX + "Session '" + session.getSessionToken()
+ "' of user '" + user + "' has been closed from host '" + getRemoteHost()
+ "'.");
}
}
public T getSession(String sessionToken) throws UserFailureException
{
checkIfNotBlank(sessionToken, "sessionToken");
synchronized (sessions)
{
final String user = StringUtils.split(sessionToken, '-')[0];
final FullSession<T> session = sessions.get(user);
if (session == null)
{
final String msg =
"Session token '" + sessionToken + "' is invalid: user is not logged in.";
if (operationLog.isInfoEnabled())
{
operationLog.info(msg);
}
throw new InvalidSessionException(msg);
}
if (sessionToken.equals(session.getSession().getSessionToken()) == false)
{
final String msg = "Session token '" + sessionToken + "' is invalid: wrong token.";
if (operationLog.isInfoEnabled())
{
operationLog.info(msg);
}
throw new InvalidSessionException(msg);
}
if (doSessionExpiration(session))
{
logSessionExpired(session);
sessions.remove(user);
}
if (isSessionUnavailable(session))
{
throw new InvalidSessionException(
"Session no longer available. Please login again.");
}
// This is where we know for sure we have a session.
session.touch();
return session.getSession();
}
}
public String tryToOpenSession(String user, String password)
{
checkIfNotBlank(user, "user");
checkIfNotBlank(password, "password");
try
{
final String applicationToken = authenticationService.authenticateApplication();
if (applicationToken == null)
{
operationLog.error("User '" + user
+ "' failed to authenticate: application not authenticated.");
return null;
}
String sessionToken = null;
final long now = System.currentTimeMillis();
final boolean isAuthenticated =
authenticationService.authenticateUser(applicationToken, user, password);
if (isAuthenticated)
{
try
{
final Principal principal =
authenticationService.getPrincipal(applicationToken, user);
T session = createAndStoreSession(user, principal, now);
sessionToken = session.getSessionToken();
logAuthenticed(session);
} catch (final IllegalArgumentException ex)
{
// getPrincipal() of an authenticated user should not fail, if it does, this
// is an environment failure.
throw new EnvironmentFailureException(ex.getMessage(), ex);
}
} else
{
logFailedAuthentication(user);
}
return sessionToken;
} catch (final RuntimeException ex)
{
logSessionFailure(user, ex);
throw ex;
}
}
public void closeSession(String sessionToken)
{
synchronized (sessions)
{
final T session = getSession(sessionToken);
sessions.remove(session.getUserName());
logLogout(session);
}
}
public String getRemoteHost()
{
return remoteHostProvider.getRemoteHost();
}
}
/*
* Copyright 2008 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;
/**
* Generator of a prefix for authentication log messages. The prefix contains user information.
* Minimum information is user name or ID and remote host (i.e. IP address of the user client
* computer).
*
* @author Franz-Josef Elmer
*/
public interface ILogMessagePrefixGenerator<T extends BasicSession>
{
/**
* Creates a prefix based on the specified session.
*/
public String createPrefix(T session);
/**
* Creates a prefix for specified user and remote host.
*/
public String createPrefix(String user, String remoteHost);
}
...@@ -37,7 +37,7 @@ public interface ISessionManager<T extends BasicSession> extends IRemoteHostProv ...@@ -37,7 +37,7 @@ public interface ISessionManager<T extends BasicSession> extends IRemoteHostProv
* @return A session token that is used afterwards to get the <code>Session</code> object, or * @return A session token that is used afterwards to get the <code>Session</code> object, or
* <code>null</code>, if the user could not be authenticated. * <code>null</code>, if the user could not be authenticated.
*/ */
public String openSession(String user, String password); public String tryToOpenSession(String user, String password);
/** Closes session by removing given <code>sessionToken</code> from active sessions. */ /** Closes session by removing given <code>sessionToken</code> from active sessions. */
public void closeSession(String sessionToken); public void closeSession(String sessionToken);
......
/*
* Copyright 2007 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;
import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.assertTrue;
import static org.testng.AssertJUnit.fail;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Level;
import org.jmock.Expectations;
import org.jmock.Mockery;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException;
import ch.systemsx.cisd.common.exceptions.UserFailureException;
import ch.systemsx.cisd.common.logging.BufferedAppender;
import ch.systemsx.cisd.common.server.IRemoteHostProvider;
import ch.systemsx.cisd.common.utilities.OSUtilities;
/**
* Test cases for the {@link DefaultSessionManager}.
*
* @author Bernd Rinn
*/
public class DefaultSessionManagerTest
{
private static final String REMOTE_HOST = "remote-host";
/** Kind of dummy <code>Principal</code> to point out that the login was successful. */
private static final Principal principal =
new Principal(StringUtils.EMPTY, StringUtils.EMPTY, StringUtils.EMPTY,
StringUtils.EMPTY);
private static final int SESSION_EXPIRATION_PERIOD_MINUTES = 1;
private Mockery context;
private IAuthenticationService authenticationService;
private ISessionFactory<BasicSession> sessionFactory;
private ILogMessagePrefixGenerator<BasicSession> prefixGenerator;
private IRemoteHostProvider remoteHostProvider;
private ISessionManager<BasicSession> sessionManager;
private BufferedAppender logRecorder;
private void assertExceptionMessageForInvalidSessionToken(final UserFailureException ex)
{
final String message = ex.getMessage();
assertTrue(message, message.indexOf("login again") > 0);
}
@SuppressWarnings("unchecked")
@BeforeMethod
public void setUp()
{
context = new Mockery();
sessionFactory = context.mock(ISessionFactory.class);
prefixGenerator = context.mock(ILogMessagePrefixGenerator.class);
authenticationService = context.mock(IAuthenticationService.class);
remoteHostProvider = context.mock(IRemoteHostProvider.class);
context.checking(new Expectations()
{
{
one(authenticationService).check();
}
});
sessionManager = createSessionManager(SESSION_EXPIRATION_PERIOD_MINUTES);
logRecorder = new BufferedAppender("%-5p %c - %m%n", Level.DEBUG);
}
@SuppressWarnings("unchecked")
private ISessionManager<BasicSession> createSessionManager(int sessionExpiration)
{
return new DefaultSessionManager(sessionFactory, prefixGenerator, authenticationService,
remoteHostProvider, sessionExpiration);
}
@AfterMethod
public void tearDown()
{
logRecorder.reset();
// To following line of code should also be called at the end of each test method.
// Otherwise one does not known which test failed.
context.assertIsSatisfied();
}
private void prepareRemoteHostSessionFactoryAndPrefixGenerator(final String user)
{
context.checking(new Expectations()
{
{
allowing(remoteHostProvider).getRemoteHost();
will(returnValue(REMOTE_HOST));
one(sessionFactory)
.create(with(any(String.class)), with(equal(user)),
with(equal(principal)), with(equal(REMOTE_HOST)),
with(any(Long.class)));
BasicSession session =
new BasicSession(user + "-1", user, principal, REMOTE_HOST, 42L);
will(returnValue(session));
atLeast(1).of(prefixGenerator).createPrefix(session);
will(returnValue("[USER:'bla', HOST:'remote-host']"));
}
});
}
@Test
public void testSuccessfulAuthentication()
{
final String applicationToken = "ole";
final String user = "bla";
prepareRemoteHostSessionFactoryAndPrefixGenerator(user);
context.checking(new Expectations()
{
{
one(authenticationService).authenticateUser(applicationToken, user, "blub");
will(returnValue(true));
one(authenticationService).authenticateApplication();
will(returnValue(applicationToken));
one(authenticationService).getPrincipal(applicationToken, user);
will(returnValue(principal));
}
});
final String token = sessionManager.tryToOpenSession("bla", "blub");
assertEquals("bla-1", token);
assertEquals(
"INFO OPERATION.DefaultSessionManager - "
+ "LOGIN: User 'bla' has been successfully authenticated from host 'remote-host'. Session token: '"
+ token + "'." + OSUtilities.LINE_SEPARATOR
+ "INFO AUTH.DefaultSessionManager - [USER:'bla', HOST:'remote-host']: login", logRecorder
.getLogContent());
context.assertIsSatisfied();
}
@Test
public void testFailedAuthentication()
{
final String applicationToken = "ole";
final String user = "bla";
context.checking(new Expectations()
{
{
one(authenticationService).authenticateUser("ole", user, "blub");
will(returnValue(false));
one(authenticationService).authenticateApplication();
will(returnValue(applicationToken));
allowing(remoteHostProvider).getRemoteHost();
will(returnValue(REMOTE_HOST));
one(prefixGenerator).createPrefix(user, REMOTE_HOST);
will(returnValue("[USER:'bla', HOST:'remote-host']"));
}
});
assert null == sessionManager.tryToOpenSession(user, "blub");
assertEquals("WARN OPERATION.DefaultSessionManager - "
+ "LOGIN: User 'bla' failed to authenticate from host 'remote-host'."
+ OSUtilities.LINE_SEPARATOR + "INFO AUTH.DefaultSessionManager - [USER:'bla', HOST:'remote-host']: login ...FAILED",
logRecorder.getLogContent());
context.assertIsSatisfied();
}
@Test
public void testAuthenticationForUnavailableAuthenticationService()
{
final String errorMsg = "I pretend to be not here!";
context.checking(new Expectations()
{
{
one(authenticationService).check();
will(throwException(new EnvironmentFailureException(errorMsg)));
}
});
try
{
createSessionManager(1);
fail("Unavailable authentication service not expected");
} catch (final EnvironmentFailureException e)
{
assertEquals(errorMsg, e.getMessage());
}
context.assertIsSatisfied();
}
@Test
public void testExpirationOfSession()
{
final String applicationToken = "ole";
final String user = "bla";
prepareRemoteHostSessionFactoryAndPrefixGenerator(user);
context.checking(new Expectations()
{
{
one(authenticationService).check();
one(authenticationService).authenticateUser(applicationToken, user, "blub");
will(returnValue(true));
one(authenticationService).getPrincipal(applicationToken, user);
will(returnValue(principal));
one(authenticationService).authenticateApplication();
will(returnValue(applicationToken));
}
});
sessionManager = createSessionManager(0);
final String sessionToken = sessionManager.tryToOpenSession("bla", "blub");
assert sessionToken.length() > 0;
try
{
Thread.sleep(100);
} catch (final InterruptedException ex)
{
// ignored
}
logRecorder.resetLogContent();
try
{
sessionManager.getSession(sessionToken);
fail("UserFailureException expected because session has expired.");
} catch (final UserFailureException e)
{
assertExceptionMessageForInvalidSessionToken(e);
}
assertEquals("INFO OPERATION.DefaultSessionManager - " + "LOGOUT: Expiring session '"
+ sessionToken + "' for user 'bla' after 0 minutes of inactivity."
+ OSUtilities.LINE_SEPARATOR + "INFO AUTH.DefaultSessionManager - [USER:'bla', HOST:'remote-host']: session_expired [inactive 0:00:00.000]", logRecorder
.getLogContent());
context.assertIsSatisfied();
}
@Test
public void testSessionRemoval()
{
final String applicationToken = "ole";
final String user = "bla";
final String password = "blub";
prepareRemoteHostSessionFactoryAndPrefixGenerator(user);
context.checking(new Expectations()
{
{
one(authenticationService).authenticateUser(applicationToken, user, password);
will(returnValue(true));
one(authenticationService).authenticateApplication();
will(returnValue(applicationToken));
one(authenticationService).getPrincipal(applicationToken, user);
will(returnValue(principal));
}
});
final String sessionToken = sessionManager.tryToOpenSession(user, password);
sessionManager.closeSession(sessionToken);
try
{
sessionManager.getSession(user);
fail("UserFailureException expected because session token no longer valid.");
} catch (final UserFailureException ex)
{
final String message = ex.getMessage();
assertTrue(message, message.indexOf("user is not logged in") > 0);
}
context.assertIsSatisfied();
}
}
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