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