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