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}