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