    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;
    020
    021import org.apache.commons.lang3.StringUtils;
    022import org.apache.logging.log4j.LogManager;
    023import org.apache.logging.log4j.Logger;
    024import org.apache.wiki.api.core.Engine;
    025import org.apache.wiki.api.core.Session;
    026import org.apache.wiki.auth.AuthenticationManager;
    027import org.apache.wiki.auth.GroupPrincipal;
    028import org.apache.wiki.auth.NoSuchPrincipalException;
    029import org.apache.wiki.auth.SessionMonitor;
    030import org.apache.wiki.auth.UserManager;
    031import org.apache.wiki.auth.WikiPrincipal;
    032import org.apache.wiki.auth.authorize.Group;
    033import org.apache.wiki.auth.authorize.GroupManager;
    034import org.apache.wiki.auth.authorize.Role;
    035import org.apache.wiki.auth.user.UserDatabase;
    036import org.apache.wiki.auth.user.UserProfile;
    037import org.apache.wiki.event.WikiEvent;
    038import org.apache.wiki.event.WikiSecurityEvent;
    039import org.apache.wiki.util.HttpUtil;
    040
    041import javax.security.auth.Subject;
    042import jakarta.servlet.http.HttpServletRequest;
    043import jakarta.servlet.http.HttpSession;
    044import java.net.http.HttpRequest;
    045import java.security.Principal;
    046import java.util.Arrays;
    047import java.util.HashSet;
    048import java.util.LinkedHashSet;
    049import java.util.Locale;
    050import java.util.Map;
    051import java.util.Set;
    052import java.util.UUID;
    053import java.util.concurrent.ConcurrentHashMap;
    054
    055
    056/**
    057 * <p>Default implementation for {@link Session}.</p>
    058 * <p>In addition to methods for examining individual <code>WikiSession</code> objects, this class also contains a number of static
    059 * methods for managing WikiSessions for an entire wiki. These methods allow callers to find, query and remove WikiSession objects, and
    060 * to obtain a list of the current wiki session users.</p>
    061 */
    062public class WikiSession implements Session {
    063
    064    private static final Logger LOG = LogManager.getLogger( WikiSession.class );
    065
    066    private static final String ALL = "*";
    067
    068    private static final ThreadLocal< Session > c_guestSession = new ThreadLocal<>();
    069
    070    private final Subject m_subject = new Subject();
    071
    072    private final Map< String, Set< String > > m_messages  = new ConcurrentHashMap<>();
    073
    074    /** The Engine that created this session. */
    075    private Engine m_engine;
    076    private String remoteAddress;
    077    private String antiCsrfToken;
    078    private String m_status            = ANONYMOUS;
    079
    080    private Principal m_userPrincipal  = WikiPrincipal.GUEST;
    081
    082    private Principal m_loginPrincipal = WikiPrincipal.GUEST;
    083
    084    private Locale m_cachedLocale      = Locale.getDefault();
    085
    086    /**
    087     * Returns <code>true</code> if one of this WikiSession's user Principals can be shown to belong to a particular wiki group. If
    088     * the user is not authenticated, this method will always return <code>false</code>.
    089     *
    090     * @param group the group to test
    091     * @return the result
    092     */
    093    protected boolean isInGroup( final Group group ) {
    094        return Arrays.stream(getPrincipals()).anyMatch(principal -> isAuthenticated() && group.isMember(principal));
    095    }
    096
    097    /**
    098     * Private constructor to prevent WikiSession from being instantiated directly.
    099     */
    100    private WikiSession() {
    101    }
    102
    103    /** {@inheritDoc} */
    104    @Override
    105    public boolean isAsserted() {
    106        return m_subject.getPrincipals().contains( Role.ASSERTED );
    107    }
    108
    109    /** {@inheritDoc} */
    110    @Override
    111    public boolean isAuthenticated() {
    112        // If Role.AUTHENTICATED is in principals set, always return true.
    113        if ( m_subject.getPrincipals().contains( Role.AUTHENTICATED ) ) {
    114            return true;
    115        }
    116
    117        // With non-JSPWiki LoginModules, the role may not be there, so we need to add it if the user really is authenticated.
    118        if ( !isAnonymous() && !isAsserted() ) {
    119            m_subject.getPrincipals().add( Role.AUTHENTICATED );
    120            return true;
    121        }
    122
    123        return false;
    124    }
    125
    126    /** {@inheritDoc} */
    127    @Override
    128    public boolean isAnonymous() {
    129        final Set< Principal > principals = m_subject.getPrincipals();
    130        return principals.contains( Role.ANONYMOUS ) ||
    131               principals.contains( WikiPrincipal.GUEST ) ||
    132               HttpUtil.isIPV4Address( getUserPrincipal().getName() );
    133    }
    134
    135    /** {@inheritDoc} */
    136    @Override
    137    public Principal getLoginPrincipal() {
    138        return m_loginPrincipal;
    139    }
    140
    141    /** {@inheritDoc} */
    142    @Override
    143    public Principal getUserPrincipal() {
    144        return m_userPrincipal;
    145    }
    146
    147    /** {@inheritDoc} */
    148    @Override
    149    public String antiCsrfToken() {
    150        return antiCsrfToken;
    151    }
    152
    153    /** {@inheritDoc} */
    154    @Override
    155    public Locale getLocale() {
    156        return m_cachedLocale;
    157    }
    158
    159    /** {@inheritDoc} */
    160    @Override
    161    public void addMessage( final String message ) {
    162        addMessage( ALL, message );
    163    }
    164
    165    /** {@inheritDoc} */
    166    @Override
    167    public void addMessage( final String topic, final String message ) {
    168        if ( topic == null ) {
    169            throw new IllegalArgumentException( "addMessage: topic cannot be null." );
    170        }
    171        final Set< String > messages = m_messages.computeIfAbsent( topic, k -> new LinkedHashSet<>() );
    172        messages.add( StringUtils.defaultString( message ) );
    173    }
    174
    175    /** {@inheritDoc} */
    176    @Override
    177    public void clearMessages() {
    178        m_messages.clear();
    179    }
    180
    181    /** {@inheritDoc} */
    182    @Override
    183    public void clearMessages( final String topic ) {
    184        final Set< String > messages = m_messages.get( topic );
    185        if ( messages != null ) {
    186            m_messages.clear();
    187        }
    188    }
    189
    190    /** {@inheritDoc} */
    191    @Override
    192    public String[] getMessages() {
    193        return getMessages( ALL );
    194    }
    195
    196    /** {@inheritDoc} */
    197    @Override
    198    public String[] getMessages( final String topic ) {
    199        final Set< String > messages = m_messages.get( topic );
    200        if( messages == null || messages.isEmpty()) {
    201            return new String[ 0 ];
    202        }
    203        return messages.toArray( new String[0] );
    204    }
    205
    206    /** {@inheritDoc} */
    207    @Override
    208    public Principal[] getPrincipals() {
    209
    210        // Take the first non Role as the main Principal
    211
    212        return m_subject.getPrincipals().stream().filter(AuthenticationManager::isUserPrincipal).toArray(Principal[]::new);
    213    }
    214
    215    /** {@inheritDoc} */
    216    @Override
    217    public Principal[] getRoles() {
    218        final Set< Principal > roles = new HashSet<>();
    219
    220        // Add all the Roles possessed by the Subject directly
    221        roles.addAll( m_subject.getPrincipals( Role.class ) );
    222
    223        // Add all the GroupPrincipals possessed by the Subject directly
    224        roles.addAll(m_subject.getPrincipals(GroupPrincipal.class));
    225
    226
    227        // Return a defensive copy
    228        final Principal[] roleArray = roles.toArray( new Principal[0] );
    229        Arrays.sort( roleArray, WikiPrincipal.COMPARATOR );
    230        return roleArray;
    231    }
    232
    233    /** {@inheritDoc} */
    234    @Override
    235    public boolean hasPrincipal( final Principal principal ) {
    236        return m_subject.getPrincipals().contains( principal );
    237    }
    238
    239    /**
    240     * Listens for WikiEvents generated by source objects such as the GroupManager, UserManager or AuthenticationManager. This method adds
    241     * Principals to the private Subject managed by the WikiSession.
    242     *
    243     * @see org.apache.wiki.event.WikiEventListener#actionPerformed(WikiEvent)
    244     */
    245    @Override
    246    public void actionPerformed( final WikiEvent event ) {
    247        if ( event instanceof WikiSecurityEvent ) {
    248            final WikiSecurityEvent e = (WikiSecurityEvent)event;
    249            if ( e.getTarget() != null ) {
    250                switch( e.getType() ) {
    251                case WikiSecurityEvent.GROUP_ADD:
    252                    final Group groupAdd = ( Group )e.getTarget();
    253                    if( isInGroup( groupAdd ) ) {
    254                        m_subject.getPrincipals().add( groupAdd.getPrincipal() );
    255                    }
    256                    break;
    257                case WikiSecurityEvent.GROUP_REMOVE:
    258                    final Group group = ( Group )e.getTarget();
    259                    m_subject.getPrincipals().remove( group.getPrincipal() );
    260                    break;
    261                case WikiSecurityEvent.GROUP_CLEAR_GROUPS:
    262                    m_subject.getPrincipals().removeAll( m_subject.getPrincipals( GroupPrincipal.class ) );
    263                    break;
    264                case WikiSecurityEvent.LOGIN_INITIATED:
    265                    // Do nothing
    266                    break;
    267                case WikiSecurityEvent.PRINCIPAL_ADD:
    268                    final WikiSession targetPA = ( WikiSession )e.getTarget();
    269                    if( this.equals( targetPA ) && m_status.equals( AUTHENTICATED ) ) {
    270                        final Set< Principal > principals = m_subject.getPrincipals();
    271                        principals.add( ( Principal )e.getPrincipal() );
    272                    }
    273                    break;
    274                case WikiSecurityEvent.LOGIN_ANONYMOUS:
    275                    final WikiSession targetLAN = ( WikiSession )e.getTarget();
    276                    if( this.equals( targetLAN ) ) {
    277                        m_status = ANONYMOUS;
    278
    279                        // Set the login/user principals and login status
    280                        final Set< Principal > principals = m_subject.getPrincipals();
    281                        m_loginPrincipal = ( Principal )e.getPrincipal();
    282                        m_userPrincipal = m_loginPrincipal;
    283
    284                        // Add the login principal to the Subject, and set the built-in roles
    285                        principals.clear();
    286                        principals.add( m_loginPrincipal );
    287                        principals.add( Role.ALL );
    288                        principals.add( Role.ANONYMOUS );
    289                    }
    290                    break;
    291                case WikiSecurityEvent.LOGIN_ASSERTED:
    292                    final WikiSession targetLAS = ( WikiSession )e.getTarget();
    293                    if( this.equals( targetLAS ) ) {
    294                        m_status = ASSERTED;
    295
    296                        // Set the login/user principals and login status
    297                        final Set< Principal > principals = m_subject.getPrincipals();
    298                        m_loginPrincipal = ( Principal )e.getPrincipal();
    299                        m_userPrincipal = m_loginPrincipal;
    300
    301                        // Add the login principal to the Subject, and set the built-in roles
    302                        principals.clear();
    303                        principals.add( m_loginPrincipal );
    304                        principals.add( Role.ALL );
    305                        principals.add( Role.ASSERTED );
    306                    }
    307                    break;
    308                case WikiSecurityEvent.LOGIN_AUTHENTICATED:
    309                    final WikiSession targetLAU = ( WikiSession )e.getTarget();
    310                    if( this.equals( targetLAU ) ) {
    311                        m_status = AUTHENTICATED;
    312
    313                        // Set the login/user principals and login status
    314                        final Set< Principal > principals = m_subject.getPrincipals();
    315                        m_loginPrincipal = ( Principal )e.getPrincipal();
    316                        m_userPrincipal = m_loginPrincipal;
    317
    318                        // Add the login principal to the Subject, and set the built-in roles
    319                        principals.clear();
    320                        principals.add( m_loginPrincipal );
    321                        principals.add( Role.ALL );
    322                        principals.add( Role.AUTHENTICATED );
    323
    324                        // Add the user and group principals
    325                        injectUserProfilePrincipals();  // Add principals for the user profile
    326                        injectGroupPrincipals();  // Inject group principals
    327                    }
    328                    break;
    329                case WikiSecurityEvent.PROFILE_SAVE:
    330                    final WikiSession sourcePS = e.getSrc();
    331                    if( this.equals( sourcePS ) ) {
    332                        injectUserProfilePrincipals();  // Add principals for the user profile
    333                        injectGroupPrincipals();  // Inject group principals
    334                    }
    335                    break;
    336                case WikiSecurityEvent.PROFILE_NAME_CHANGED:
    337                    // Refresh user principals based on new user profile
    338                    final WikiSession sourcePNC = e.getSrc();
    339                    if( this.equals( sourcePNC ) && m_status.equals( AUTHENTICATED ) ) {
    340                        // To prepare for refresh, set the new full name as the primary principal
    341                        final UserProfile[] profiles = ( UserProfile[] )e.getTarget();
    342                        final UserProfile newProfile = profiles[ 1 ];
    343                        if( newProfile.getFullname() == null ) {
    344                            throw new IllegalStateException( "User profile FullName cannot be null." );
    345                        }
    346
    347                        final Set< Principal > principals = m_subject.getPrincipals();
    348                        m_loginPrincipal = new WikiPrincipal( newProfile.getLoginName() );
    349
    350                        // Add the login principal to the Subject, and set the built-in roles
    351                        principals.clear();
    352                        principals.add( m_loginPrincipal );
    353                        principals.add( Role.ALL );
    354                        principals.add( Role.AUTHENTICATED );
    355
    356                        // Add the user and group principals
    357                        injectUserProfilePrincipals();  // Add principals for the user profile
    358                        injectGroupPrincipals();  // Inject group principals
    359                    }
    360                    break;
    361
    362                //  No action, if the event is not recognized.
    363                default:
    364                    break;
    365                }
    366            }
    367        }
    368    }
    369
    370    /** {@inheritDoc} */
    371    @Override
    372    public void invalidate() {
    373        m_subject.getPrincipals().clear();
    374        m_subject.getPrincipals().add( WikiPrincipal.GUEST );
    375        m_subject.getPrincipals().add( Role.ANONYMOUS );
    376        m_subject.getPrincipals().add( Role.ALL );
    377        m_userPrincipal = WikiPrincipal.GUEST;
    378        m_loginPrincipal = WikiPrincipal.GUEST;
    379    }
    380
    381    /**
    382     * Injects GroupPrincipal objects into the user's Principal set based on the groups the user belongs to. For Groups, the algorithm
    383     * first calls the {@link GroupManager#getRoles()} to obtain the array of GroupPrincipals the authorizer knows about. Then, the
    384     * method {@link GroupManager#isUserInRole(Session, Principal)} is called for each Principal. If the user is a member of the
    385     * group, an equivalent GroupPrincipal is injected into the user's principal set. Existing GroupPrincipals are flushed and replaced.
    386     * This method should generally be called after a user's {@link org.apache.wiki.auth.user.UserProfile} is saved. If the wiki session
    387     * is null, or there is no matching user profile, the method returns silently.
    388     */
    389    protected void injectGroupPrincipals() {
    390        // Flush the existing GroupPrincipals
    391        m_subject.getPrincipals().removeAll( m_subject.getPrincipals(GroupPrincipal.class) );
    392
    393        // Get the GroupManager and test for each Group
    394        final GroupManager manager = m_engine.getManager( GroupManager.class );
    395        for( final Principal group : manager.getRoles() ) {
    396            if ( manager.isUserInRole( this, group ) ) {
    397                m_subject.getPrincipals().add( group );
    398            }
    399        }
    400    }
    401
    402    /**
    403     * Adds Principal objects to the Subject that correspond to the logged-in user's profile attributes for the wiki name, full name
    404     * and login name. These Principals will be WikiPrincipals, and they will replace all other WikiPrincipals in the Subject. <em>Note:
    405     * this method is never called during anonymous or asserted sessions.</em>
    406     */
    407    protected void injectUserProfilePrincipals() {
    408        // Search for the user profile
    409        final String searchId = m_loginPrincipal.getName();
    410        if ( searchId == null ) {
    411            // Oh dear, this wasn't an authenticated user after all
    412            LOG.info("Refresh principals failed because WikiSession had no user Principal; maybe not logged in?");
    413            return;
    414        }
    415
    416        // Look up the user and go get the new Principals
    417        final UserDatabase database = m_engine.getManager( UserManager.class ).getUserDatabase();
    418        if( database == null ) {
    419            throw new IllegalStateException( "User database cannot be null." );
    420        }
    421        try {
    422            final UserProfile profile = database.findByLoginName( searchId );
    423            final Principal[] principals = database.getPrincipals( profile.getLoginName() );
    424            for( final Principal principal : principals ) {
    425                // Add the Principal to the Subject
    426                m_subject.getPrincipals().add( principal );
    427
    428                // Set the user principal if needed; we prefer FullName, but the WikiName will also work
    429                final boolean isFullNamePrincipal = ( principal instanceof WikiPrincipal &&
    430                                                      ( ( WikiPrincipal )principal ).getType().equals( WikiPrincipal.FULL_NAME ) );
    431                if (( principal instanceof WikiPrincipal &&
    432                                                      ( ( WikiPrincipal )principal ).getType().equals( WikiPrincipal.LOGIN_NAME )) ){
    433                    m_loginPrincipal = principal;
    434                }
    435
    436
    437                if ( isFullNamePrincipal ) {
    438                   m_userPrincipal = principal;
    439                } else if ( !( m_userPrincipal instanceof WikiPrincipal ) ) {
    440                    m_userPrincipal = principal;
    441                }
    442            }
    443        } catch ( final NoSuchPrincipalException e ) {
    444            // We will get here if the user has a principal but not a profile
    445            // For example, it's a container-managed user who hasn't set up a profile yet
    446            LOG.warn("User profile '" + searchId + "' not found. This is normal for container-auth users who haven't set up a profile yet.");
    447        }
    448    }
    449
    450    /** {@inheritDoc} */
    451    @Override
    452    public String getStatus() {
    453        return m_status;
    454    }
    455
    456    /** {@inheritDoc} */
    457    @Override
    458    public Subject getSubject() {
    459        return m_subject;
    460    }
    461
    462    /**
    463     * Removes the wiki session associated with the user's HTTP request from the cache of wiki sessions, typically as part of a
    464     * logout process.
    465     *
    466     * @param engine the wiki engine
    467     * @param request the user's HTTP request
    468     */
    469    public static void removeWikiSession( final Engine engine, final HttpServletRequest request ) {
    470        if ( engine == null || request == null ) {
    471            throw new IllegalArgumentException( "Request or engine cannot be null." );
    472        }
    473        final SessionMonitor monitor = SessionMonitor.getInstance( engine );
    474        monitor.remove( request.getSession() );
    475        c_guestSession.remove();
    476    }
    477
    478    /**
    479     * <p>Static factory method that returns the Session object associated with the current HTTP request. This method looks up
    480     * the associated HttpSession in an internal WeakHashMap and attempts to retrieve the WikiSession. If not found, one is created.
    481     * This method is guaranteed to always return a Session, although the authentication status is unpredictable until the user
    482     * attempts to log in. If the servlet request parameter is <code>null</code>, a synthetic {@link #guestSession(Engine)} is
    483     * returned.</p>
    484     * <p>When a session is created, this method attaches a WikiEventListener to the GroupManager, UserManager and AuthenticationManager,
    485     * so that changes to users, groups, logins, etc. are detected automatically.</p>
    486     *
    487     * @param engine the engine
    488     * @param request the servlet request object
    489     * @return the existing (or newly created) session
    490     */
    491    public static Session getWikiSession( final Engine engine, final HttpServletRequest request ) {
    492        if ( request == null ) {
    493            LOG.debug( "Looking up WikiSession for NULL HttpRequest: returning guestSession()" );
    494            return staticGuestSession( engine );
    495        }
    496
    497        // Look for a WikiSession associated with the user's Http Session and create one if it isn't there yet.
    498        final HttpSession session = request.getSession();
    499        final SessionMonitor monitor = SessionMonitor.getInstance( engine );
    500        final WikiSession wikiSession = ( WikiSession )monitor.find( session );
    501        wikiSession.remoteAddress = request.getRemoteAddr();
    502        // Attach reference to wiki engine
    503        wikiSession.m_engine = engine;
    504        wikiSession.m_cachedLocale = request.getLocale();
    505
    506        String v = engine.getWikiProperties().getProperty("jspwiki.role.extraRoles", null);
    507        if (v != null) {
    508            String[] extraRoles = v.split("\\,");
    509            for (String s : extraRoles) {
    510                if (request.isUserInRole(s)) {
    511                    wikiSession.m_subject.getPrincipals().add(new GroupPrincipal(s));
    512                }
    513            }
    514        }
    515
    516        return wikiSession;
    517    }
    518
    519    /**
    520     * Static factory method that creates a new "guest" session containing a single user Principal
    521     * {@link org.apache.wiki.auth.WikiPrincipal#GUEST}, plus the role principals {@link Role#ALL} and {@link Role#ANONYMOUS}. This
    522     * method also adds the session as a listener for GroupManager, AuthenticationManager and UserManager events.
    523     *
    524     * @param engine the wiki engine
    525     * @return the guest wiki session
    526     */
    527    public static Session guestSession( final Engine engine ) {
    528        final WikiSession session = new WikiSession();
    529        session.m_engine = engine;
    530        session.invalidate();
    531        session.antiCsrfToken = UUID.randomUUID().toString();
    532
    533        // Add the session as listener for GroupManager, AuthManager, UserManager events
    534        final GroupManager groupMgr = engine.getManager( GroupManager.class );
    535        final AuthenticationManager authMgr = engine.getManager( AuthenticationManager.class );
    536        final UserManager userMgr = engine.getManager( UserManager.class );
    537        groupMgr.addWikiEventListener( session );
    538        authMgr.addWikiEventListener( session );
    539        userMgr.addWikiEventListener( session );
    540
    541        return session;
    542    }
    543
    544    /**
    545     *  Returns a static guest session, which is available for this thread only.  This guest session is used internally whenever
    546     *  there is no HttpServletRequest involved, but the request is done e.g. when embedding JSPWiki code.
    547     *
    548     *  @param engine Engine for this session
    549     *  @return A static WikiSession which is shared by all in this same Thread.
    550     */
    551    // FIXME: Should really use WeakReferences to clean away unused sessions.
    552    private static Session staticGuestSession( final Engine engine ) {
    553        Session session = c_guestSession.get();
    554        if( session == null ) {
    555            session = guestSession( engine );
    556            c_guestSession.set( session );
    557        }
    558
    559        return session;
    560    }
    561
    562    /**
    563     * Returns the total number of active wiki sessions for a particular wiki. This method delegates to the wiki's
    564     * {@link SessionMonitor#sessions()} method.
    565     *
    566     * @param engine the wiki session
    567     * @return the number of sessions
    568     * @deprecated use {@link SessionMonitor#sessions()} instead
    569     * @see SessionMonitor#sessions()
    570     */
    571    @Deprecated
    572    public static int sessions( final Engine engine ) {
    573        final SessionMonitor monitor = SessionMonitor.getInstance( engine );
    574        return monitor.sessions();
    575    }
    576
    577    /**
    578     * Returns Principals representing the current users known to a particular wiki. Each Principal will correspond to the
    579     * value returned by each WikiSession's {@link #getUserPrincipal()} method. This method delegates to
    580     * {@link SessionMonitor#userPrincipals()}.
    581     *
    582     * @param engine the wiki engine
    583     * @return an array of Principal objects, sorted by name
    584     * @deprecated use {@link SessionMonitor#userPrincipals()} instead
    585     * @see SessionMonitor#userPrincipals()
    586     */
    587    @Deprecated
    588    public static Principal[] userPrincipals( final Engine engine ) {
    589        final SessionMonitor monitor = SessionMonitor.getInstance( engine );
    590        return monitor.userPrincipals();
    591    }
    592
    593    @Override
    594    public String getRemoteAddress() {
    595        return remoteAddress;
    596    }
    597
    598}