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