001/* 002 Licensed to the Apache Software Foundation (ASF) under one 003 or more contributor license agreements. See the NOTICE file 004 distributed with this work for additional information 005 regarding copyright ownership. The ASF licenses this file 006 to you under the Apache License, Version 2.0 (the 007 "License"); you may not use this file except in compliance 008 with the License. You may obtain a copy of the License at 009 010 http://www.apache.org/licenses/LICENSE-2.0 011 012 Unless required by applicable law or agreed to in writing, 013 software distributed under the License is distributed on an 014 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 KIND, either express or implied. See the License for the 016 specific language governing permissions and limitations 017 under the License. 018 */ 019package org.apache.wiki.auth; 020 021import org.apache.logging.log4j.LogManager; 022import org.apache.logging.log4j.Logger; 023import org.apache.wiki.api.core.Engine; 024import org.apache.wiki.api.core.Session; 025import org.apache.wiki.api.spi.Wiki; 026import org.apache.wiki.event.WikiEventListener; 027import org.apache.wiki.event.WikiEventManager; 028import org.apache.wiki.event.WikiSecurityEvent; 029import org.apache.wiki.util.comparators.PrincipalComparator; 030 031import jakarta.servlet.http.HttpServletRequest; 032import jakarta.servlet.http.HttpSession; 033import jakarta.servlet.http.HttpSessionEvent; 034import jakarta.servlet.http.HttpSessionListener; 035import java.security.Principal; 036import java.util.ArrayList; 037import java.util.Arrays; 038import java.util.Collection; 039import java.util.List; 040import java.util.Map; 041import java.util.WeakHashMap; 042import java.util.concurrent.ConcurrentHashMap; 043import java.util.stream.Collectors; 044import org.apache.wiki.security.EventUtil; 045 046/** 047 * <p>Manages Sessions for different Engines.</p> 048 * <p>The Sessions are stored both in the remote user HttpSession and in the SessionMonitor for the Engine. 049 * This class must be configured as a session listener in the web.xml for the wiki web application.</p> 050 */ 051public class SessionMonitor implements HttpSessionListener { 052 053 private static final Logger LOG = LogManager.getLogger( SessionMonitor.class ); 054 055 /** Map with Engines as keys, and SessionMonitors as values. */ 056 private static final ConcurrentHashMap< Engine, SessionMonitor > c_monitors = new ConcurrentHashMap<>(); 057 058 /** Weak hashmap with HttpSessions as keys, and WikiSessions as values. */ 059 private final Map< String, Session > m_sessions = new WeakHashMap<>(); 060 061 private Engine m_engine; 062 063 private final PrincipalComparator m_comparator = new PrincipalComparator(); 064 065 /** 066 * Returns the instance of the SessionMonitor for this wiki. Only one SessionMonitor exists per Engine. 067 * 068 * @param engine the wiki engine 069 * @return the session monitor 070 */ 071 public static SessionMonitor getInstance( final Engine engine ) { 072 if( engine == null ) { 073 throw new IllegalArgumentException( "Engine cannot be null." ); 074 } 075 SessionMonitor monitor = c_monitors.get( engine ); 076 if( monitor == null ) { 077 monitor = new SessionMonitor( engine ); 078 c_monitors.put( engine, monitor ); 079 } 080 081 return monitor; 082 } 083 084 /** Construct the SessionListener */ 085 public SessionMonitor() { 086 } 087 088 private SessionMonitor( final Engine engine ) { 089 m_engine = engine; 090 } 091 092 /** 093 * Just looks for a WikiSession; does not create a new one. 094 * This method may return <code>null</code>, <em>and 095 * callers should check for this value</em>. 096 * 097 * @param session the user's HTTP session 098 * @return the WikiSession, if found 099 */ 100 private Session findSession( final HttpSession session ) { 101 final String sid = ( session == null ) ? "(null)" : session.getId(); 102 return findSession( sid ); 103 } 104 105 /** 106 * Just looks for a WikiSession; does not create a new one. 107 * This method may return <code>null</code>, <em>and 108 * callers should check for this value</em>. 109 * 110 * @param sessionId the user's HTTP session id 111 * @return the WikiSession, if found 112 */ 113 private Session findSession( final String sessionId ) { 114 Session wikiSession = null; 115 final String sid = ( sessionId == null ) ? "(null)" : sessionId; 116 synchronized( m_sessions ){ 117 final Session storedSession = m_sessions.get( sid ); 118 119 // If the weak reference returns a wiki session, return it 120 if( storedSession != null ) { 121 LOG.debug( "Looking up WikiSession for session ID={}... found it", sid ); 122 wikiSession = storedSession; 123 } 124 } 125 126 return wikiSession; 127 } 128 129 /** 130 * <p>Looks up the wiki session associated with a user's Http session and adds it to the session cache. This method will return the 131 * "guest session" as constructed by {@link org.apache.wiki.api.spi.SessionSPI#guest(Engine)} if the HttpSession is not currently 132 * associated with a WikiSession. This method is guaranteed to return a non-<code>null</code> WikiSession.</p> 133 * <p>Internally, the session is stored in a HashMap; keys are the HttpSession objects, while the values are 134 * {@link java.lang.ref.WeakReference}-wrapped WikiSessions.</p> 135 * 136 * @param session the HTTP session 137 * @return the wiki session 138 */ 139 public final Session find( final HttpSession session ) { 140 final Session wikiSession = findSession( session ); 141 final String sid = ( session == null ) ? "(null)" : session.getId(); 142 if( wikiSession == null ) { 143 return createGuestSessionFor( sid ); 144 } 145 146 return wikiSession; 147 } 148 149 /** 150 * <p>Looks up the wiki session associated with a user's Http session and adds it to the session cache. This method will return the 151 * "guest session" as constructed by {@link org.apache.wiki.api.spi.SessionSPI#guest(Engine)} if the HttpSession is not currently 152 * associated with a WikiSession. This method is guaranteed to return a non-<code>null</code> WikiSession.</p> 153 * <p>Internally, the session is stored in a HashMap; keys are the HttpSession objects, while the values are 154 * {@link java.lang.ref.WeakReference}-wrapped WikiSessions.</p> 155 * 156 * @param sessionId the HTTP session 157 * @return the wiki session 158 */ 159 public final Session find( final String sessionId ) { 160 final Session wikiSession = findSession( sessionId ); 161 if( wikiSession == null ) { 162 return createGuestSessionFor( sessionId ); 163 } 164 165 return wikiSession; 166 } 167 168 /** 169 * Creates a new session and stashes it 170 * 171 * @param sessionId id looked for before creating the guest session 172 * @return a new guest session 173 */ 174 private Session createGuestSessionFor( final String sessionId ) { 175 LOG.debug( "Session for session ID={}... not found. Creating guestSession()", sessionId ); 176 final Session wikiSession = Wiki.session().guest( m_engine ); 177 synchronized( m_sessions ) { 178 m_sessions.put( sessionId, wikiSession ); 179 } 180 return wikiSession; 181 } 182 183 /** 184 * Removes the wiki session associated with the user's HttpRequest from the session cache. 185 * 186 * @param request the user's HTTP request 187 */ 188 public final void remove( final HttpServletRequest request ) { 189 if( request == null ) { 190 throw new IllegalArgumentException( "Request cannot be null." ); 191 } 192 remove( request.getSession() ); 193 } 194 195 /** 196 * Removes the wiki session associated with the user's HttpSession from the session cache. 197 * 198 * @param session the user's HTTP session 199 */ 200 public final void remove( final HttpSession session ) { 201 if( session == null ) { 202 throw new IllegalArgumentException( "Session cannot be null." ); 203 } 204 synchronized( m_sessions ) { 205 m_sessions.remove( session.getId() ); 206 } 207 } 208 209 /** 210 * Returns the current number of active wiki sessions. 211 * @return the number of sessions 212 */ 213 public final int sessions() 214 { 215 return userPrincipals().length; 216 } 217 218 /** 219 * <p>Returns the current wiki users as a sorted array of Principal objects. The principals are those returned by 220 * each WikiSession's {@link Session#getUserPrincipal()}'s method.</p> 221 * <p>To obtain the list of current WikiSessions, we iterate through our session Map and obtain the list of values, 222 * which are WikiSessions wrapped in {@link java.lang.ref.WeakReference} objects. Those <code>WeakReference</code>s 223 * whose <code>get()</code> method returns non-<code>null</code> values are valid sessions.</p> 224 * 225 * @return the array of user principals 226 */ 227 public final Principal[] userPrincipals() { 228 final Collection<Principal> principals; 229 synchronized ( m_sessions ) { 230 principals = m_sessions.values().stream().map(Session::getUserPrincipal).collect(Collectors.toList()); 231 } 232 final Principal[] p = principals.toArray( new Principal[0] ); 233 Arrays.sort( p, m_comparator ); 234 return p; 235 } 236 237 /** 238 * Registers a WikiEventListener with this instance. 239 * 240 * @param listener the event listener 241 * @since 2.4.75 242 */ 243 public final synchronized void addWikiEventListener( final WikiEventListener listener ) { 244 WikiEventManager.addWikiEventListener( this, listener ); 245 } 246 247 /** 248 * Un-registers a WikiEventListener with this instance. 249 * 250 * @param listener the event listener 251 * @since 2.4.75 252 */ 253 public final synchronized void removeWikiEventListener( final WikiEventListener listener ) { 254 WikiEventManager.removeWikiEventListener( this, listener ); 255 } 256 257 /** 258 * Fires a WikiSecurityEvent to all registered listeners. 259 * 260 * @param type the event type 261 * @param principal the user principal associated with this session 262 * @param session the wiki session 263 * @since 2.4.75 264 */ 265 protected final void fireEvent( final int type, final Principal principal, final Session session ) { 266 if( WikiEventManager.isListening( this ) ) { 267 WikiEventManager.fireEvent( this, 268 EventUtil.applyFrom(new WikiSecurityEvent( this, type, principal, session ) ) ); 269 } 270 } 271 272 /** 273 * Fires when the web container creates a new HTTP session. 274 * 275 * @param se the HTTP session event 276 */ 277 @Override 278 public void sessionCreated( final HttpSessionEvent se ) { 279 final HttpSession session = se.getSession(); 280 LOG.debug( "Created session: " + session.getId() + "." ); 281 } 282 283 /** 284 * Removes the user's WikiSession from the internal session cache when the web 285 * container destroys an HTTP session. 286 * @param se the HTTP session event 287 */ 288 @Override 289 public void sessionDestroyed( final HttpSessionEvent se ) { 290 final HttpSession session = se.getSession(); 291 for( final SessionMonitor monitor : c_monitors.values() ) { 292 final Session storedSession = monitor.findSession( session ); 293 monitor.remove( session ); 294 LOG.debug( "Removed session " + session.getId() + "." ); 295 if( storedSession != null ) { 296 fireEvent( WikiSecurityEvent.SESSION_EXPIRED, storedSession.getLoginPrincipal(), storedSession ); 297 } 298 } 299 } 300 301 /** 302 * gets a list of other sessions for the same login id for auditing purposes. 303 * 304 * @since 3.0.0 305 * @param name 306 * @return list 307 */ 308 public List<Session> findOtherSessionsByUsername(String name) { 309 List<Session> otherSessions = new ArrayList<>(); 310 synchronized (m_sessions) { 311 312 for (Session m : m_sessions.values()) { 313 if (m.getLoginPrincipal().getName().equals(name)) { 314 otherSessions.add(m); 315 } 316 } 317 } 318 return otherSessions; 319 } 320 321}