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