From 650835c0d767c0fb59dd6237dd9b31a6d7b9ccd0 Mon Sep 17 00:00:00 2001 From: felmer <franz-josef.elmer@id.ethz.ch> Date: Wed, 4 Sep 2019 15:03:36 +0200 Subject: [PATCH] SSDM-8599: Instead of throwing an exception the oldest session will be expired --- .../authentication/DefaultSessionManager.java | 88 ++++++++++++++----- .../cisd/authentication/ISessionManager.java | 2 + .../DefaultSessionManagerTest.java | 30 ++++--- .../web/server/AbstractClientService.java | 74 ++++++++++++---- .../generic/server/AbstractServer.java | 17 ++++ .../eln-lims/html/js/server/ServerFacade.js | 3 +- 6 files changed, 162 insertions(+), 52 deletions(-) diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/DefaultSessionManager.java b/authentication/source/java/ch/systemsx/cisd/authentication/DefaultSessionManager.java index 41f9aeaaea8..778350bcd3b 100644 --- a/authentication/source/java/ch/systemsx/cisd/authentication/DefaultSessionManager.java +++ b/authentication/source/java/ch/systemsx/cisd/authentication/DefaultSessionManager.java @@ -19,11 +19,15 @@ package ch.systemsx.cisd.authentication; import java.io.File; import java.text.DateFormat; import java.text.SimpleDateFormat; -import java.util.Collection; +import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.Set; import javax.annotation.Resource; @@ -32,6 +36,7 @@ import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DurationFormatUtils; import org.apache.log4j.Logger; +import ch.systemsx.cisd.common.collection.SimpleComparator; import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException; import ch.systemsx.cisd.common.exceptions.InvalidSessionException; import ch.systemsx.cisd.common.exceptions.UserFailureException; @@ -55,6 +60,11 @@ import ch.systemsx.cisd.common.spring.ExposablePropertyPlaceholderConfigurer; */ public class DefaultSessionManager<T extends BasicSession> implements ISessionManager<T> { + private enum SessionClosingReason + { + LOGOUT, SESSION_EXPIRATION, SESSIONS_LIMIT + } + public static final File NO_LOGIN_FILE = new File("./etc/nologin.html"); private static final String LOGOUT_PREFIX = "LOGOUT: "; @@ -143,6 +153,8 @@ public class DefaultSessionManager<T extends BasicSession> implements ISessionMa private final boolean tryEmailAsUserName; + private final Set<ISessionActionListener> listeners = new LinkedHashSet<>(); + public DefaultSessionManager(final ISessionFactory<T> sessionFactory, final ILogMessagePrefixGenerator<T> prefixGenerator, final IAuthenticationService authenticationService, @@ -212,11 +224,11 @@ public class DefaultSessionManager<T extends BasicSession> implements ISessionMa int maxNumberOfSessions = getMaxNumberOfSessionsFor(user); if (maxNumberOfSessions > 0) { - int numberOfOpenSessions = getNumberOfOpenSessionsFor(user); - if (numberOfOpenSessions >= maxNumberOfSessions) + List<FullSession<T>> openSessions = getOpenSessionsFor(user); + while (openSessions.size() >= maxNumberOfSessions) { - throw new UserFailureException(numberOfOpenSessions + " open session(s) but only " - + maxNumberOfSessions + " session(s) are allowed for user " + user + "."); + FullSession<T> session = openSessions.remove(0); + closeSession(session.getSession(), SessionClosingReason.SESSIONS_LIMIT); } } @@ -232,18 +244,25 @@ public class DefaultSessionManager<T extends BasicSession> implements ISessionMa } } - private int getNumberOfOpenSessionsFor(String user) + private List<FullSession<T>> getOpenSessionsFor(String user) { - int count = 0; - Collection<FullSession<T>> values = sessions.values(); - for (FullSession<T> session : values) + List<FullSession<T>> userSessions = new ArrayList<>(); + for (FullSession<T> session : sessions.values()) { if (session.getSession().getUserName().equals(user)) { - count++; + userSessions.add(session); } } - return count; + Collections.sort(userSessions, new SimpleComparator<FullSession<T>, Long>() + { + @Override + public Long evaluate(FullSession<T> session) + { + return session.lastActiveTime; + } + }); + return userSessions; } protected int getMaxNumberOfSessionsFor(String user) @@ -453,6 +472,18 @@ public class DefaultSessionManager<T extends BasicSession> implements ISessionMa } } + private void logLimitedNumberOfSessions(final T session) + { + final String prefix = prefixGenerator.createPrefix(session); + authenticationLog.info(prefix + ": session closed because limit of open session has been reached."); + if (operationLog.isInfoEnabled()) + { + final String user = session.getUserName(); + operationLog.info(LOGOUT_PREFIX + "Session '" + session.getSessionToken() + + "' of user '" + user + "' has been closed because limit of open session has been reached."); + } + } + @Override public boolean isAWellFormedSessionToken(String sessionTokenOrNull) { @@ -540,7 +571,7 @@ public class DefaultSessionManager<T extends BasicSession> implements ISessionMa } if (checkAndTouch && doSessionExpiration(session)) { - closeSession(session.getSession(), false); + closeSession(session.getSession(), SessionClosingReason.SESSION_EXPIRATION); } if (checkAndTouch && isSessionUnavailable(session)) { @@ -615,7 +646,7 @@ public class DefaultSessionManager<T extends BasicSession> implements ISessionMa synchronized (sessions) { final T session = getSession(sessionToken, false); - closeSession(session, true); + closeSession(session, SessionClosingReason.LOGOUT); } } @@ -623,22 +654,32 @@ public class DefaultSessionManager<T extends BasicSession> implements ISessionMa public void expireSession(String sessionToken) throws InvalidSessionException { final T session = getSession(sessionToken, false); - closeSession(session, false); + closeSession(session, SessionClosingReason.SESSION_EXPIRATION); } - private void closeSession(final T session, final boolean regularLogout) + private void closeSession(final T session, SessionClosingReason reason) throws InvalidSessionException { synchronized (sessions) { session.cleanup(); - sessions.remove(session.getSessionToken()); - if (regularLogout) + String sessionToken = session.getSessionToken(); + sessions.remove(sessionToken); + switch (reason) { - logLogout(session); - } else + case LOGOUT: + logLogout(session); + break; + case SESSION_EXPIRATION: + logSessionExpired(session); + break; + case SESSIONS_LIMIT: + logLimitedNumberOfSessions(session); + break; + } + for (ISessionActionListener listener : listeners) { - logSessionExpired(session); + listener.sessionClosed(sessionToken); } } } @@ -659,4 +700,11 @@ public class DefaultSessionManager<T extends BasicSession> implements ISessionMa } return p; } + + @Override + public void addListener(ISessionActionListener listener) + { + listeners.add(listener); + } + } diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/ISessionManager.java b/authentication/source/java/ch/systemsx/cisd/authentication/ISessionManager.java index e08049acc03..30aa716f670 100644 --- a/authentication/source/java/ch/systemsx/cisd/authentication/ISessionManager.java +++ b/authentication/source/java/ch/systemsx/cisd/authentication/ISessionManager.java @@ -75,5 +75,7 @@ public interface ISessionManager<T extends BasicSession> extends IRemoteHostProv * </p> */ public T tryGetSession(final String sessionToken); + + public void addListener(ISessionActionListener listener); } \ No newline at end of file diff --git a/authentication/sourceTest/java/ch/systemsx/cisd/authentication/DefaultSessionManagerTest.java b/authentication/sourceTest/java/ch/systemsx/cisd/authentication/DefaultSessionManagerTest.java index 78ed2c035df..8704bf905bc 100644 --- a/authentication/sourceTest/java/ch/systemsx/cisd/authentication/DefaultSessionManagerTest.java +++ b/authentication/sourceTest/java/ch/systemsx/cisd/authentication/DefaultSessionManagerTest.java @@ -24,6 +24,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Level; @@ -40,6 +41,7 @@ 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.test.RecordingMatcher; import ch.systemsx.cisd.common.test.RetryTen; import ch.systemsx.cisd.common.test.TestReportCleaner; @@ -78,6 +80,8 @@ public class DefaultSessionManagerTest private Sequence sequence; + private ISessionActionListener sessionActionListener; + private void assertExceptionMessageForInvalidSessionToken(final UserFailureException ex) { final String message = ex.getMessage(); @@ -94,6 +98,7 @@ public class DefaultSessionManagerTest authenticationService = context.mock(IAuthenticationService.class); remoteHostProvider = context.mock(IRemoteHostProvider.class); principalProvider = context.mock(IPrincipalProvider.class); + sessionActionListener = context.mock(ISessionActionListener.class); sequence = context.sequence("sequence"); context.checking(new Expectations() { @@ -232,6 +237,7 @@ public class DefaultSessionManagerTest public void testLimitedNumberOfSession() { final String user = "bla"; + RecordingMatcher<BasicSession> recordingSessionMatcher = new RecordingMatcher<BasicSession>(); context.checking(new Expectations() { { @@ -243,7 +249,7 @@ public class DefaultSessionManagerTest exactly(3).of(authenticationService).tryGetAndAuthenticateUser(user, "blub"); will(returnValue(principal)); - for (int i = 0; i < 2; i++) + for (int i = 0; i < 3; i++) { one(sessionFactory).create(with(any(String.class)), with(equal(user)), with(equal(principal)), with(equal(REMOTE_HOST)), @@ -253,26 +259,24 @@ public class DefaultSessionManagerTest will(returnValue(session)); inSequence(sequence); - one(prefixGenerator).createPrefix(session); + one(prefixGenerator).createPrefix(with(recordingSessionMatcher)); will(returnValue("[USER:'" + user + "', HOST:'remote-host']")); } - - one(prefixGenerator).createPrefix("bla", REMOTE_HOST); - will(returnValue("[USER:'bla', HOST:'remote-host']")); + one(prefixGenerator).createPrefix(with(recordingSessionMatcher)); + will(returnValue("[USER:'" + user + "', HOST:'remote-host']")); + + one(sessionActionListener).sessionClosed("bla-1"); } }); sessionManager = createSessionManager(SESSION_EXPIRATION_PERIOD_MINUTES, Collections.singletonMap("bla", 2)); + sessionManager.addListener(sessionActionListener); assertEquals("bla-1", sessionManager.tryToOpenSession("bla", "blub")); assertEquals("bla-2", sessionManager.tryToOpenSession("bla", "blub")); - try - { - assertEquals("bla-3", sessionManager.tryToOpenSession("bla", "blub")); - fail("UserFailureException expected"); - } catch (UserFailureException e) - { - assertEquals("2 open session(s) but only 2 session(s) are allowed for user bla.", e.getMessage()); - } + assertEquals("bla-3", sessionManager.tryToOpenSession("bla", "blub")); + assertEquals("[bla-1, bla-2, bla-1, bla-3]", + recordingSessionMatcher.getRecordedObjects().stream().map(s -> s.getSessionToken()) + .collect(Collectors.toList()).toString()); context.assertIsSatisfied(); } diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/server/AbstractClientService.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/server/AbstractClientService.java index c1d3ce70d52..0976d7a0f46 100644 --- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/server/AbstractClientService.java +++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/server/AbstractClientService.java @@ -36,6 +36,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import ch.rinn.restrictions.Private; +import ch.systemsx.cisd.authentication.ISessionActionListener; import ch.systemsx.cisd.common.exceptions.UserFailureException; import ch.systemsx.cisd.common.logging.LogCategory; import ch.systemsx.cisd.common.logging.LogFactory; @@ -73,8 +74,10 @@ import ch.systemsx.cisd.openbis.generic.client.web.server.translator.ResultSetTr import ch.systemsx.cisd.openbis.generic.client.web.server.translator.UserFailureExceptionTranslator; import ch.systemsx.cisd.openbis.generic.client.web.server.util.TableModelUtils; import ch.systemsx.cisd.openbis.generic.client.web.server.util.XMLPropertyTransformer; +import ch.systemsx.cisd.openbis.generic.server.ComponentNames; import ch.systemsx.cisd.openbis.generic.server.SessionConstants; import ch.systemsx.cisd.openbis.generic.shared.Constants; +import ch.systemsx.cisd.openbis.generic.shared.IOpenBisSessionManager; import ch.systemsx.cisd.openbis.generic.shared.IServer; import ch.systemsx.cisd.openbis.generic.shared.WebClientConfigurationProvider; import ch.systemsx.cisd.openbis.generic.shared.basic.IEntityInformationHolder; @@ -116,6 +119,29 @@ public abstract class AbstractClientService implements IClientService, @Resource(name = ExposablePropertyPlaceholderConfigurer.PROPERTY_CONFIGURER_BEAN_NAME) private ExposablePropertyPlaceholderConfigurer configurer; + @Resource(name = ComponentNames.SESSION_MANAGER) + private IOpenBisSessionManager sessionManager; + + private final Map<String, HttpSession> httpSessionsBySessionToken = new HashMap<>(); + + private ISessionActionListener sessionActionListener = new ISessionActionListener() + { + @Override + public void sessionClosed(String sessionToken) + { + synchronized (httpSessionsBySessionToken) + { + HttpSession httpSession = httpSessionsBySessionToken.remove(sessionToken); + logout(null, true, httpSession, sessionToken); + if (httpSession != null) + { + operationLog.info("Session " + sessionToken + " closed. " + + "httpSessionsBySessionToken.size() = " + httpSessionsBySessionToken.size()); + } + } + } + }; + @Autowired private TableDataCache<String, Object> tableDataCache; @@ -653,6 +679,12 @@ public abstract class AbstractClientService implements IClientService, return null; } final HttpSession httpSession = createHttpSession(); + synchronized (httpSessionsBySessionToken) + { + httpSessionsBySessionToken.put(session.getSessionToken(), httpSession); + operationLog.info("httpSessionsBySessionToken.size() = " + httpSessionsBySessionToken.size()); + } + // Expiration time of httpSession is 10 seconds less than of session final int sessionExpirationTimeInMillis = session.getSessionExpirationTime(); final int sessionExpirationTimeInSeconds = sessionExpirationTimeInMillis / 1000; @@ -772,26 +804,31 @@ public abstract class AbstractClientService implements IClientService, try { final HttpSession httpSession = getHttpSession(); - if (httpSession != null) + String sessionToken = getSessionToken(); + logout(displaySettings, simpleViewMode, httpSession, sessionToken); + } catch (Exception e) + { + operationLog.info("logout exception: " + e); + } + } + + private void logout(DisplaySettings displaySettings, boolean simpleViewMode, final HttpSession httpSession, String sessionToken) + { + if (httpSession != null) + { + httpSession.removeAttribute(SessionConstants.OPENBIS_SESSION_TOKEN_ATTRIBUTE_KEY); + httpSession.removeAttribute(SessionConstants.OPENBIS_SERVER_ATTRIBUTE_KEY); + httpSession.removeAttribute(SessionConstants.OPENBIS_RESULT_SET_MANAGER); + httpSession.removeAttribute(SessionConstants.OPENBIS_EXPORT_MANAGER); + httpSession.invalidate(); + IServer server = getServer(); + if (simpleViewMode == false) { - String sessionToken = getSessionToken(); - httpSession.removeAttribute(SessionConstants.OPENBIS_SESSION_TOKEN_ATTRIBUTE_KEY); - httpSession.removeAttribute(SessionConstants.OPENBIS_SERVER_ATTRIBUTE_KEY); - httpSession.removeAttribute(SessionConstants.OPENBIS_RESULT_SET_MANAGER); - httpSession.removeAttribute(SessionConstants.OPENBIS_EXPORT_MANAGER); - httpSession.invalidate(); - IServer server = getServer(); - if (simpleViewMode == false) - { - // only save settings for "normal" view - int maxEntityVisits = getWebClientConfiguration().getMaxEntityVisits(); - server.saveDisplaySettings(sessionToken, displaySettings, maxEntityVisits); - } - server.logout(sessionToken); + // only save settings for "normal" view + int maxEntityVisits = getWebClientConfiguration().getMaxEntityVisits(); + server.saveDisplaySettings(sessionToken, displaySettings, maxEntityVisits); } - } catch (final UserFailureException e) - { - // Just ignore it because we are logging out anyway. + server.logout(sessionToken); } } @@ -832,5 +869,6 @@ public abstract class AbstractClientService implements IClientService, public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; + sessionManager.addListener(sessionActionListener); } } diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/AbstractServer.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/AbstractServer.java index 5952c837f6b..3ce90cd554e 100644 --- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/AbstractServer.java +++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/AbstractServer.java @@ -29,6 +29,7 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import javax.annotation.PostConstruct; import javax.annotation.Resource; import org.apache.commons.lang3.StringUtils; @@ -39,6 +40,7 @@ import org.springframework.transaction.annotation.Transactional; import ch.ethz.sis.openbis.generic.asapi.v3.IApplicationServerApi; import ch.systemsx.cisd.authentication.DefaultSessionManager; import ch.systemsx.cisd.authentication.IPrincipalProvider; +import ch.systemsx.cisd.authentication.ISessionActionListener; import ch.systemsx.cisd.authentication.ISessionManager; import ch.systemsx.cisd.authentication.Principal; import ch.systemsx.cisd.common.action.IDelegatedActionWithResult; @@ -178,6 +180,15 @@ public abstract class AbstractServer<T> extends AbstractServiceWithLogger<T> imp private IApplicationServerApi v3Api; protected String CISDHelpdeskEmail; + + private ISessionActionListener sessionActionListener = new ISessionActionListener() + { + @Override + public void sessionClosed(String sessionToken) + { + logout(sessionToken); + } + }; protected AbstractServer() { @@ -204,6 +215,12 @@ public abstract class AbstractServer<T> extends AbstractServiceWithLogger<T> imp this.sampleTypeSlaveServerPlugin = sampleTypeSlaveServerPlugin; this.dataSetTypeSlaveServerPlugin = dataSetTypeSlaveServerPlugin; } + + @PostConstruct + public void registerAtSessionManager() + { + sessionManager.addListener(sessionActionListener); + } // For unit tests - in production Spring will inject this object. public void setDisplaySettingsProvider(DisplaySettingsProvider displaySettingsProvider) diff --git a/openbis_standard_technologies/dist/core-plugins/eln-lims/1/as/webapps/eln-lims/html/js/server/ServerFacade.js b/openbis_standard_technologies/dist/core-plugins/eln-lims/1/as/webapps/eln-lims/html/js/server/ServerFacade.js index 96e277604e4..c8bab098eba 100644 --- a/openbis_standard_technologies/dist/core-plugins/eln-lims/1/as/webapps/eln-lims/html/js/server/ServerFacade.js +++ b/openbis_standard_technologies/dist/core-plugins/eln-lims/1/as/webapps/eln-lims/html/js/server/ServerFacade.js @@ -51,7 +51,8 @@ function ServerFacade(openbisServer) { var responseInterceptor = function(response, action){ var isError = false; if(response && response.error) { - if(response.error.message === "Session no longer available. Please login again.") { + if(response.error.message === "Session no longer available. Please login again." + || response.error.message.endsWith("is invalid: user is not logged in.")) { isError = true; Util.showError(response.error.message, function() { location.reload(true); -- GitLab