    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}