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