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