diff --git a/jspwiki-api/src/main/java/org/apache/wiki/api/core/Session.java b/jspwiki-api/src/main/java/org/apache/wiki/api/core/Session.java index 7a8004a093..5894e2ae12 100644 --- a/jspwiki-api/src/main/java/org/apache/wiki/api/core/Session.java +++ b/jspwiki-api/src/main/java/org/apache/wiki/api/core/Session.java @@ -237,4 +237,25 @@ public interface Session extends WikiEventListener { */ Subject getSubject(); + /** + * Wrapper for {@link Subject#doAsPrivileged(Subject, PrivilegedAction, java.security.AccessControlContext)} + * that executes an action with the privileges possessed by a Session's Subject. The action executes with a null + * AccessControlContext, which has the effect of running it "cleanly" without the AccessControlContexts of the caller. + * + * @param session the wiki session + * @param action the privileged action + * @return the result of the privileged action; may be null + * @throws java.security.AccessControlException if the action is not permitted by the security policy + */ + static Object doPrivileged( final Session session, final PrivilegedAction action ) throws AccessControlException { + return Subject.doAsPrivileged( session.getSubject(), action, null ); + } + + /** + * returns the remote address of the user login session, usually an ip address + * @since 3.0.0 + * @return string + */ + String getRemoteAddress(); + } diff --git a/jspwiki-event/src/main/java/org/apache/wiki/event/WikiSecurityEvent.java b/jspwiki-event/src/main/java/org/apache/wiki/event/WikiSecurityEvent.java index 3f3dfa590d..735c70446f 100644 --- a/jspwiki-event/src/main/java/org/apache/wiki/event/WikiSecurityEvent.java +++ b/jspwiki-event/src/main/java/org/apache/wiki/event/WikiSecurityEvent.java @@ -115,6 +115,8 @@ public final class WikiSecurityEvent extends WikiEvent { /** When a low disk space is encountered . */ public static final int LOW_STORAGE = 55; + /** Login audit alert, multiple concurrent logins from the different ip addresses . */ + public static final int LOGIN_ALERT = 56; /** The security logging service. */ private static final Logger LOG = LogManager.getLogger( "SecurityLog" ); @@ -123,7 +125,7 @@ public final class WikiSecurityEvent extends WikiEvent { private final Object m_target; - private static final int[] ERROR_EVENTS = { LOGIN_FAILED }; + private static final int[] ERROR_EVENTS = { LOGIN_FAILED, LOGIN_ALERT }; private static final int[] WARN_EVENTS = { LOGIN_ACCOUNT_EXPIRED, LOGIN_CREDENTIAL_EXPIRED }; @@ -249,6 +251,7 @@ public String getTypeDescription() { case LOGIN_ACCOUNT_EXPIRED: return "login failed: expired account"; case LOGIN_CREDENTIAL_EXPIRED: return "login failed: credential expired"; case LOGIN_FAILED: return "login failed"; + case LOGIN_ALERT: return "login alert"; case LOGOUT: return "user logged out"; case PRINCIPAL_ADD: return "new principal added"; case SESSION_EXPIRED: return "session expired"; diff --git a/jspwiki-main/src/main/java/org/apache/wiki/WikiSession.java b/jspwiki-main/src/main/java/org/apache/wiki/WikiSession.java index d14fe3de5b..23f5d88bde 100644 --- a/jspwiki-main/src/main/java/org/apache/wiki/WikiSession.java +++ b/jspwiki-main/src/main/java/org/apache/wiki/WikiSession.java @@ -72,7 +72,7 @@ public class WikiSession implements Session { /** The Engine that created this session. */ private Engine m_engine; - + private String remoteAddress; private String antiCsrfToken; private String m_status = ANONYMOUS; @@ -490,7 +490,7 @@ public static Session getWikiSession( final Engine engine, final HttpServletRequ final HttpSession session = request.getSession(); final SessionMonitor monitor = SessionMonitor.getInstance( engine ); final WikiSession wikiSession = ( WikiSession )monitor.find( session ); - + wikiSession.remoteAddress = request.getRemoteAddr(); // Attach reference to wiki engine wikiSession.m_engine = engine; wikiSession.m_cachedLocale = request.getLocale(); @@ -571,4 +571,9 @@ public static Principal[] userPrincipals( final Engine engine ) { return monitor.userPrincipals(); } + @Override + public String getRemoteAddress() { + return remoteAddress; + } + } diff --git a/jspwiki-main/src/main/java/org/apache/wiki/auth/DefaultAuthenticationManager.java b/jspwiki-main/src/main/java/org/apache/wiki/auth/DefaultAuthenticationManager.java index ef5ca423e8..d5b83dbd28 100644 --- a/jspwiki-main/src/main/java/org/apache/wiki/auth/DefaultAuthenticationManager.java +++ b/jspwiki-main/src/main/java/org/apache/wiki/auth/DefaultAuthenticationManager.java @@ -50,6 +50,7 @@ Licensed to the Apache Software Foundation (ASF) under one import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; @@ -193,6 +194,23 @@ public boolean login( final HttpServletRequest request ) throws WikiSecurityExce fireEvent( WikiSecurityEvent.LOGIN_ASSERTED, getLoginPrincipal( principals ), session, request); } } + + if (!session.isAnonymous()) { + final SessionMonitor monitor = SessionMonitor.getInstance(m_engine); + List sessions = monitor.findOtherSessionsByUsername(session.getLoginPrincipal().getName()); + StringBuilder sb = new StringBuilder(); + for (Session s : sessions) { + if (s.getRemoteAddress() != null && !s.getRemoteAddress().equals(request.getRemoteAddr())) { + sb.append(request.getRemoteAddr()).append(","); + } + } + if (sb.length() > 0) { + sb.append(request.getRemoteAddr()); + LOG.warn("AUDIT - New login for login '" + session.getLoginPrincipal().getName() + "' from " + request.getRemoteAddr() + + " however there are already concurrent logins from the following addresses " + sb.toString()); + fireEvent(WikiSecurityEvent.LOGIN_ALERT, session.getLoginPrincipal(), session, request); + } + } // If user still anonymous, use the remote address if( session.isAnonymous() ) { diff --git a/jspwiki-main/src/main/java/org/apache/wiki/auth/SessionMonitor.java b/jspwiki-main/src/main/java/org/apache/wiki/auth/SessionMonitor.java index 8fe7555897..76c1ab6f0d 100644 --- a/jspwiki-main/src/main/java/org/apache/wiki/auth/SessionMonitor.java +++ b/jspwiki-main/src/main/java/org/apache/wiki/auth/SessionMonitor.java @@ -33,8 +33,10 @@ Licensed to the Apache Software Foundation (ASF) under one import jakarta.servlet.http.HttpSessionEvent; import jakarta.servlet.http.HttpSessionListener; import java.security.Principal; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.WeakHashMap; import java.util.concurrent.ConcurrentHashMap; @@ -111,12 +113,14 @@ private Session findSession( final HttpSession session ) { private Session findSession( final String sessionId ) { Session wikiSession = null; final String sid = ( sessionId == null ) ? "(null)" : sessionId; - final Session storedSession = m_sessions.get( sid ); + synchronized( m_sessions ){ + final Session storedSession = m_sessions.get( sid ); - // If the weak reference returns a wiki session, return it - if( storedSession != null ) { - LOG.debug( "Looking up WikiSession for session ID={}... found it", sid ); - wikiSession = storedSession; + // If the weak reference returns a wiki session, return it + if( storedSession != null ) { + LOG.debug( "Looking up WikiSession for session ID={}... found it", sid ); + wikiSession = storedSession; + } } return wikiSession; @@ -294,4 +298,24 @@ public void sessionDestroyed( final HttpSessionEvent se ) { } } + /** + * gets a list of other sessions for the same login id for auditing purposes. + * + * @since 3.0.0 + * @param name + * @return list + */ + public List findOtherSessionsByUsername(String name) { + List otherSessions = new ArrayList<>(); + synchronized (m_sessions) { + + for (Session m : m_sessions.values()) { + if (m.getLoginPrincipal().getName().equals(name)) { + otherSessions.add(m); + } + } + } + return otherSessions; + } + }