/**
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at the
* <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Initial code contributed and copyrighted by<br>
* frentix GmbH, http://www.frentix.com
* <p>
*/
package org.olat.core.util.session;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.olat.core.commons.persistence.DB;
import org.olat.core.gui.control.Disposable;
import org.olat.core.gui.control.Event;
import org.olat.core.helpers.Settings;
import org.olat.core.id.Identity;
import org.olat.core.id.IdentityEnvironment;
import org.olat.core.id.OLATResourceable;
import org.olat.core.id.Roles;
import org.olat.core.id.context.HistoryManager;
import org.olat.core.logging.AssertException;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.logging.activity.CoreLoggingResourceable;
import org.olat.core.logging.activity.OlatLoggingAction;
import org.olat.core.logging.activity.ThreadLocalUserActivityLogger;
import org.olat.core.logging.activity.ThreadLocalUserActivityLoggerInstaller;
import org.olat.core.logging.activity.UserActivityLoggerImpl;
import org.olat.core.util.SessionInfo;
import org.olat.core.util.SignOnOffEvent;
import org.olat.core.util.UserSession;
import org.olat.core.util.cache.CacheWrapper;
import org.olat.core.util.coordinate.CoordinatorManager;
import org.olat.core.util.event.GenericEventListener;
import org.olat.core.util.prefs.Preferences;
import org.olat.core.util.resource.OresHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
*
* Initial date: 15.11.2012<br>
* @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
*
*/
@Service
public class UserSessionManager implements GenericEventListener {
private static final OLog log = Tracing.createLoggerFor(UserSessionManager.class);
private static final String USERSESSIONKEY = UserSession.class.getName();
public static final OLATResourceable ORES_USERSESSION = OresHelper.createOLATResourceableType(UserSession.class);
public static final String STORE_KEY_KILLED_EXISTING_SESSION = "killedExistingSession";
//clusterNOK cache ??
private static final Set<UserSession> authUserSessions = ConcurrentHashMap.newKeySet();
private static final Set<Long> userNameToIdentity = ConcurrentHashMap.newKeySet();
private static final Set<Long> authUsersNamesOtherNodes = ConcurrentHashMap.newKeySet();
private static final AtomicInteger sessionCountWeb = new AtomicInteger();
private static final AtomicInteger sessionCountRest = new AtomicInteger();
private static final AtomicInteger sessionCountDav = new AtomicInteger();
@Autowired
private DB dbInstance;
@Autowired
private UserSessionModule sessionModule;
@Autowired
private CoordinatorManager coordinator;
@Autowired
private HistoryManager historyManager;
private CacheWrapper<Long,Integer> userSessionCache;
@PostConstruct
public void initBean() {
coordinator.getCoordinator().getEventBus().registerFor(this, null, ORES_USERSESSION);
userSessionCache = coordinator.getCoordinator().getCacher().getCache(UserSessionManager.class.getSimpleName(), "usersession");
}
/**
* @param session
* @return associated user session
*/
public UserSession getUserSession(HttpSession session) {
UserSession us = (UserSession) session.getAttribute(USERSESSIONKEY);
if(us == null) {
synchronized (session) {//o_clusterOK by:fj
us = (UserSession) session.getAttribute(USERSESSIONKEY);
if (us == null) {
us = new UserSession();
session.setAttribute(USERSESSIONKEY, us); // triggers the
// valueBoundEvent -> nothing
// more to do here
}
}
}
//set a possible changed session timeout interval
setHttpSessionTimeout(session, us);
return us;
}
/**
* @param hreq
* @return associated user session
*/
public UserSession getUserSession(HttpServletRequest hreq) {
// get existing or create new session
HttpSession httpSession = hreq.getSession(true);
UserSession usess = getUserSession(httpSession);
return usess;
}
/**
* Return the UserSession of the given request if it is already set or null otherwise
* @param hreq
* @return
*/
public UserSession getUserSessionIfAlreadySet(HttpServletRequest hreq) {
HttpSession session = hreq.getSession(false);
if (session==null) {
return null;
}
UserSession us = (UserSession) session.getAttribute(USERSESSIONKEY);
setHttpSessionTimeout(session, us);
return us;
}
private void setHttpSessionTimeout(HttpSession session, UserSession us) {
if(us == null || session == null) return;
int interval;
if(us.isAuthenticated()) {
if(us.getSessionInfo() != null && (us.getSessionInfo().isREST() || us.getSessionInfo().isWebDAV())) {
interval = 300;
} else {
interval = sessionModule.getSessionTimeoutAuthenticated();
}
} else {
interval = sessionModule.getSessionTimeout();
}
if(interval != session.getMaxInactiveInterval()) {
session.setMaxInactiveInterval(interval);
}
}
/**
* @param userName
* @return the identity or null if no user with userName is currently logged
* on
*/
public boolean isSignedOnIdentity(Long identityKey) {
return userNameToIdentity.contains(identityKey);
}
/**
* @return set of authenticated active user sessions
*/
public Set<UserSession> getAuthenticatedUserSessions() {
return new HashSet<UserSession>(authUserSessions);
}
public int getNumberOfAuthenticatedUserSessions() {
return authUserSessions.size();
}
/**
* This method returns only the number of local sessions.
*
* @return Returns the userSessionsCnt (Web, WebDAV, REST) from this VM
*/
public int getUserSessionsCnt() {
return authUserSessions.size();
}
/**
* @return The number of users currently logged in using the web interface
* (guests and authenticated users).
*/
public int getUserSessionWebCounter() {
return userSessionCache.size();
}
public boolean isOnline(Long identityKey) {
return userSessionCache.containsKey(identityKey);
}
/**
* @return The number of users currently logged in using a WebDAV client.
* Note that currently this only returns the users from this VM as
* the synchronization of user between cluster node is not
* correctly. In the long run we return all users here.
*/
public int getUserSessionDavCounter() {
// clusterNOK ?? return only number of locale sessions ?
return sessionCountDav.get();
}
/**
* @return The number of users currently logged in using the REST API. Note
* that currently this only returns the users from this VM as the
* synchronization of user between cluster node is not correctly. In
* the long run we return all users here.
*/
public int getUserSessionRestCounter() {
// clusterNOK ?? return only number of locale sessions ?
return sessionCountRest.get();
}
/**
* prior to calling this method, all instance vars must be set.
*/
public void signOn(UserSession usess) {
boolean isDebug = log.isDebug();
// Added synchronized to be symmetric with sign off and to
// fix a possible dead-lock see also OLAT-3390
synchronized(usess) {
if(isDebug) log.debug("signOn() START");
if (usess.isAuthenticated()) {
throw new AssertException("sign on: already signed on!");
}
IdentityEnvironment identityEnvironment = usess.getIdentityEnvironment();
Identity identity = identityEnvironment.getIdentity();
if (identity == null) {
throw new AssertException("identity is null in identityEnvironment!");
}
SessionInfo sessionInfo = usess.getSessionInfo();
if (sessionInfo == null) {
throw new AssertException("sessionInfo was null for identity " + identity);
}
usess.setAuthenticated(true);
if (sessionInfo.isWebDAV()) {
// load user prefs
usess.reloadPreferences();
// we're only adding this webdav session to the authUserSessions - not to the userNameToIdentity.
// userNameToIdentity is only needed for IM which can't do anything with a webdav session
authUserSessions.add(usess);
log.audit("Logged on [via webdav]: " + sessionInfo.toString());
} else {
UserSession invalidatedSession = null;
if(isDebug) {
log.debug("signOn() authUsersNamesOtherNodes.contains " + identity.getName() + ": " + authUsersNamesOtherNodes.contains(identity.getKey()));
}
// check if already a session exist for this user
if ( (userNameToIdentity.contains(identity.getKey()) || userSessionCache.containsKey(identity.getKey()) )
&& !sessionInfo.isWebDAV() && !sessionInfo.isREST() && !usess.getRoles().isGuestOnly()) {
log.info("Loggin-process II: User has already a session => signOffAndClear existing session");
invalidatedSession = getUserSessionForGui(identity.getKey());
//remove session to be invalidated
//SIDEEFFECT!! to signOffAndClear
//if invalidatedSession is removed from authUserSessions
//signOffAndClear does not remove the identity.getName().toLowerCase() from the userNameToIdentity
if(invalidatedSession != null) {
authUserSessions.remove(invalidatedSession);
}
}
authUserSessions.add(usess);
// user can choose upercase letters in identity name, but this has no effect on the
// database queries, the login form or the IM account. IM works only with lowercase
// characters -> map stores values as such
if(isDebug) log.debug("signOn() adding to userNameToIdentity: "+identity.getName().toLowerCase());
userNameToIdentity.add(identity.getKey());
userSessionCache.put(identity.getKey(), new Integer(Settings.getNodeId()));
//reload user prefs
usess.reloadPreferences();
log.audit("Logged on: " + sessionInfo.toString());
CoordinatorManager.getInstance().getCoordinator().getEventBus().fireEventToListenersOf(new SignOnOffEvent(identity, true), ORES_USERSESSION);
// THE FOLLOWING CHECK MUST BE PLACED HERE NOT TO PRODUCE A DEAD-LOCK WITH SIGNOFFANDCLEAR
// check if a session from any browser was invalidated (IE has a cookie set per Browserinstance!!)
if (invalidatedSession != null || authUsersNamesOtherNodes.contains(identity.getKey())) {
// put flag killed-existing-session into session-store to show info-message 'only one session for each user' on user-home screen
usess.putEntry(STORE_KEY_KILLED_EXISTING_SESSION, Boolean.TRUE);
if(isDebug) log.debug("signOn() removing from authUsersNamesOtherNodes: "+identity.getName());
authUsersNamesOtherNodes.remove(identity.getKey());
//OLAT-3381 & OLAT-3382
if(invalidatedSession != null) {
signOffAndClear(invalidatedSession);
}
}
if(isDebug) log.debug("signOn() END");
}
// update logged in users counters
if (sessionInfo.isREST()) {
sessionCountRest.incrementAndGet();
} else if (sessionInfo.isWebDAV()) {
sessionCountDav.incrementAndGet();
} else {
sessionCountWeb.incrementAndGet();
}
}
}
/**
* called to make sure the current authenticated user (if there is one at all)
* is cleared and signed off. This method is firing the SignOnOffEvent Multiuserevent.
*/
public void signOffAndClear(UserSession usess) { //o_clusterOK by:fj
internSignOffAndClear(usess);
//commit all changes after sign off, especially commit lock which were
//deleted by dispose methods
dbInstance.commit();
}
private void internSignOffAndClear(UserSession usess) {
boolean isDebug = log.isDebug();
if(isDebug) log.debug("signOffAndClear() START");
signOffAndClearWithout(usess);
// handle safely
try {
if (usess.isAuthenticated()) {
SessionInfo sessionInfo = usess.getSessionInfo();
IdentityEnvironment identityEnvironment = usess.getIdentityEnvironment();
Identity identity = identityEnvironment.getIdentity();
log.audit("Logged off: " + sessionInfo);
CoordinatorManager.getInstance().getCoordinator().getEventBus().fireEventToListenersOf(new SignOnOffEvent(identity, false), ORES_USERSESSION);
if(isDebug) log.debug("signOffAndClear() deregistering usersession from eventbus, id="+sessionInfo);
//fxdiff FXOLAT-231: event on GUI Preferences extern changes
OLATResourceable ores = OresHelper.createOLATResourceableInstance(Preferences.class, identity.getKey());
CoordinatorManager.getInstance().getCoordinator().getEventBus().deregisterFor(usess, ores);
}
} catch (Exception e) {
log.error("exception in signOffAndClear: while sending signonoffevent!", e);
}
// clear all instance variables, set authenticated to false
usess.init();
if(isDebug) log.debug("signOffAndClear() END");
}
/**
* called from signOffAndClear()
* called from event -> MUEvent
* the real work to do during sign off but without sending the multiuserevent
* this is used in case the user logs in to node1 and was logged in on node2 =>
* node2 catches the sign on event and invalidates the user on node2 "silently", e.g.
* without firing an event.
*/
private void signOffAndClearWithout(final UserSession usess) {
boolean isDebug = log.isDebug();
if(isDebug) log.debug("signOffAndClearWithout() START");
final IdentityEnvironment identityEnvironment = usess.getIdentityEnvironment();
final SessionInfo sessionInfo = usess.getSessionInfo();
final Identity ident = identityEnvironment.getIdentity();
if (isDebug) log.debug("UserSession:::logging off: " + sessionInfo);
if(usess.isAuthenticated() && usess.getLastHistoryPoint() != null && !usess.getRoles().isGuestOnly()) {
historyManager.persistHistoryPoint(ident, usess.getLastHistoryPoint());
}
/**
* use not RunnableWithException, as exceptionHandlng is inside the run
*/
Runnable run = new Runnable() {
@Override
public void run() {
Object obj = null;
try {
// do logging
if (ident != null) {
ThreadLocalUserActivityLogger.log(OlatLoggingAction.OLAT_LOGOUT, UserSession.class, CoreLoggingResourceable.wrap(ident));
}
// notify all variables in the store (the values) about the disposal
// if
// Disposable
List<Object> storeList = usess.getStoreValues();
for (Iterator<Object> it_storevals = storeList.iterator(); it_storevals.hasNext();) {
obj = it_storevals.next();
if (obj instanceof Disposable) {
// synchronous, since triggered by tomcat session timeout or user
// click and
// asynchronous, if kicked out by administrator.
// we assume synchronous
// !!!!
// As a reminder, this .dispose() calls dispose on
// DefaultController which is synchronized.
// (Windows/WindowManagerImpl/WindowBackOfficeImpl/BaseChiefController/../
// dispose()
// !!!! was important for bug OLAT-3390
((Disposable) obj).dispose();
}
}
} catch (Exception e) {
String objtostr = "n/a";
try {
objtostr = obj.toString();
} catch (Exception ee) {
// ignore
}
log.error("exception in signOffAndClear: while disposing object:" + objtostr, e);
}
}
};
ThreadLocalUserActivityLoggerInstaller.runWithUserActivityLogger(run, UserActivityLoggerImpl.newLoggerForValueUnbound(usess));
if(authUserSessions.remove(usess)) {
//remove only from identityEnvironment if found in sessions.
//see also SIDEEFFECT!! line in signOn(..)
Identity previousSignedOn = identityEnvironment.getIdentity();
if (previousSignedOn != null && previousSignedOn.getKey() != null) {
if(isDebug) log.debug("signOffAndClearWithout() removing from userNameToIdentity: "+previousSignedOn.getName().toLowerCase());
userNameToIdentity.remove(previousSignedOn.getKey());
userSessionCache.remove(previousSignedOn.getKey());
}
} else if (isDebug) {
log.info("UserSession already removed! for ["+ident+"]");
}
// update logged in users counters
if (sessionInfo != null) {
if (sessionInfo.isREST()) {
sessionCountRest.decrementAndGet();
} else if (sessionInfo.isWebDAV()) {
sessionCountDav.decrementAndGet();
} else {
sessionCountWeb.decrementAndGet();
}
}
if (isDebug) log.debug("signOffAndClearWithout() END");
}
/**
* only for SignOffEvents
* - Usersession keeps book about usernames
* - WindowManager responsible to dispose controller chain
* @see org.olat.core.util.event.GenericEventListener#event(org.olat.core.gui.control.Event)
*/
@Override
public void event(Event event) {
if(event instanceof SignOnOffEvent) {
SignOnOffEvent se = (SignOnOffEvent) event;
processSignOnOffEvent(se);
}
}
private void processSignOnOffEvent(SignOnOffEvent se) {
try {
boolean debug = log.isDebug();
if(debug) log.debug("event() START");
if(debug) log.debug("event() is SignOnOffEvent. isSignOn="+se.isSignOn());
if (!se.isEventOnThisNode()) {
// - signOnOff from other node
// - Single OLAT Instance is never passing by here.
if (se.isSignOn()) {
// it is a logged on event
// -> remember other nodes logged usernames
if(debug) log.debug("event() adding to authUsersNamesOtherNodes: "+se.getIdentityKey());
authUsersNamesOtherNodes.add(se.getIdentityKey());
UserSession usess = getUserSessionForGui(se.getIdentityKey());
if (usess != null && usess.getSessionInfo() != null && se.getIdentityKey().equals(usess.getSessionInfo().getIdentityKey())
&& !usess.getSessionInfo().isWebDAV() && !usess.getRoles().isGuestOnly()) {
// if this listening UserSession instance is from the same user
// and it is not a WebDAV Session, and it is not GuestSession
// => log user off on this node
signOffAndClearWithout(usess);
usess.init();
}
} else {
// it is logged off event
// -> remove from other nodes logged on list.
if(debug) log.debug("event() removing from authUsersNamesOtherNodes: "+se.getIdentityKey());
authUsersNamesOtherNodes.remove(se.getIdentityKey());
}
}
if(debug) log.debug("event() END");
} catch (Exception e) {
log.error("", e);
}
}
/**
* Invalidate all sessions except admin-sessions.
* @return Number of invalidated sessions.
*/
public int invalidateAllSessions() {
log.debug("invalidateAllSessions() START");
int invalidateCounter = 0;
log.audit("All sessions were invalidated by an administrator");
//clusterNOK ?? invalidate only locale sessions ?
Set<UserSession> userSessions = getAuthenticatedUserSessions();
for (UserSession userSession : userSessions) {
Roles userRoles = userSession != null ? userSession.getRoles() : null;
if (userRoles != null && !userRoles.isOLATAdmin()) {
//do not logout administrators
try {
internSignOffAndClear(userSession);
if(userSession.getSessionInfo() != null && userSession.getSessionInfo().getSession() != null) {
userSession.getSessionInfo().getSession().invalidate();
}
invalidateCounter++;
} catch(Exception ex) {
// Session already signed off => do nothing and continues
}
}
}
log.debug("invalidateAllSessions() END");
return invalidateCounter;
}
/**
* Invalidate a given number of oldest (last-click-time) sessions except admin-sessions.
* @param nbrSessions number of sessions whisch will be invalidated
* @return Number of invalidated sessions.
*/
public int invalidateOldestSessions(int nbrSessions) {
int invalidateCounter = 0;
// 1. Copy authUserSessions in sorted TreeMap
// This is the Comparator that will be used to sort the TreeSet:
Comparator<UserSession> sessionComparator = new Comparator<UserSession>() {
@Override
public int compare(UserSession o1, UserSession o2) {
Long long1 = new Long((o1).getSessionInfo().getLastClickTime());
Long long2 = new Long((o2).getSessionInfo().getLastClickTime());
return long1.compareTo(long2);
}
};
// clusterNOK ?? invalidate only locale sessions ?
TreeSet<UserSession> sortedSet = new TreeSet<UserSession>(sessionComparator);
sortedSet.addAll(authUserSessions);
int i = 0;
for (Iterator<UserSession> iterator = sortedSet.iterator(); iterator.hasNext() && i++<nbrSessions;) {
try {
UserSession userSession = iterator.next();
if (!userSession.getRoles().isOLATAdmin() && !userSession.getSessionInfo().isWebDAV()) {
internSignOffAndClear(userSession);
invalidateCounter++;
}
} catch (Throwable th) {
log.warn("Error signOffAndClear ", th);
}
}
return invalidateCounter;
}
/**
* set session timeout on http session -
* @param sessionTimeoutInSec
*/
public void setGlobalSessionTimeout(int sessionTimeoutInSec) {
UserSession[] currentSessions = authUserSessions.toArray(new UserSession[0]);
for(int i=currentSessions.length; i-->0; ) {
try{
SessionInfo sessionInfo = currentSessions[i].getSessionInfo();
if(sessionInfo != null && sessionInfo.getSession() != null) {
sessionInfo.getSession().setMaxInactiveInterval(sessionTimeoutInSec);
}
} catch(Throwable th){
log.error("error setting sesssionTimeout", th);
}
}
}
/**
* Lookup non-webdav, non-REST UserSession for identity key.
* @param identityKey
* @return user-session or null when no session was founded.
*/
private UserSession getUserSessionForGui(Long identityKey) {
UserSession identitySession = null;
if(identityKey != null) {
//do not call from somewhere else then signOffAndClear!!
Optional<UserSession> optionalSession = authUserSessions.stream().filter(userSession -> {
Identity identity = userSession.getIdentity();
if (identity != null && identityKey.equals(identity.getKey())
&& userSession.getSessionInfo() != null
&& !userSession.getSessionInfo().isWebDAV()
&& !userSession.getSessionInfo().isREST()) {
return true;
}
return false;
}).findFirst();
identitySession = optionalSession.isPresent() ? optionalSession.get() : null;
}
return identitySession;
}
}