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 java.security.AccessControlException;
022import java.security.Principal;
023import java.security.PrivilegedAction;
024import java.util.*;
025
026import javax.security.auth.Subject;
027import javax.servlet.http.HttpServletRequest;
028import javax.servlet.http.HttpSession;
029
030import org.apache.commons.lang.StringUtils;
031import org.apache.log4j.Logger;
032
033import org.apache.wiki.auth.*;
034import org.apache.wiki.auth.authorize.Group;
035import org.apache.wiki.auth.authorize.GroupManager;
036import org.apache.wiki.auth.authorize.Role;
037import org.apache.wiki.auth.user.UserDatabase;
038import org.apache.wiki.auth.user.UserProfile;
039import org.apache.wiki.event.WikiEvent;
040import org.apache.wiki.event.WikiEventListener;
041import org.apache.wiki.event.WikiSecurityEvent;
042
043/**
044 * <p>Represents a long-running wiki session, with an associated user Principal,
045 * user Subject, and authentication status. This class is initialized with
046 * minimal, default-deny values: authentication is set to <code>false</code>,
047 * and the user principal is set to <code>null</code>.</p>
048 * <p>The WikiSession class allows callers to:</p>
049 * <ul>
050 *   <li>Obtain the authentication status of the user via
051 *     {@link #isAnonymous()} and {@link #isAuthenticated()}</li>
052 *   <li>Query the session for Principals representing the
053 *     user's identity via {@link #getLoginPrincipal()},
054 *     {@link #getUserPrincipal()} and {@link #getPrincipals()}</li>
055 *   <li>Store, retrieve and clear UI messages via
056 *     {@link #addMessage(String)}, {@link #getMessages(String)}
057 *     and {@link #clearMessages(String)}</li>
058 * </ul>
059 * <p>To keep track of the Principals each user posseses, each WikiSession
060 * stores a JAAS Subject. Various login processes add or remove Principals
061 * when users authenticate or log out.</p>
062 * <p>WikiSession implements the {@link org.apache.wiki.event.WikiEventListener}
063 * interface and listens for group add/change/delete events fired by
064 * event sources the WikiSession is registered with. Normally,
065 * {@link org.apache.wiki.auth.AuthenticationManager} registers each WikiSession
066 * with the {@link org.apache.wiki.auth.authorize.GroupManager}
067 * so it can catch group events. Thus, when a user is added to a
068 * {@link org.apache.wiki.auth.authorize.Group}, a corresponding
069 * {@link org.apache.wiki.auth.GroupPrincipal} is injected into
070 * the Subject's Principal set. Likewise, when the user is removed from
071 * the Group or the Group is deleted, the GroupPrincipal is removed
072 * from the Subject. The effect that this strategy produces is extremely
073 * beneficial: when someone adds a user to a wiki group, that user
074 * <em>immediately</em> gains the privileges associated with that
075 * group; he or she does not need to re-authenticate.
076 * </p>
077 * <p>In addition to methods for examining individual <code>WikiSession</code>
078 * objects, this class also contains a number of static methods for
079 * managing WikiSessions for an entire wiki. These methods allow callers
080 * to find, query and remove WikiSession objects, and
081 * to obtain a list of the current wiki session users.</p>
082 * <p>WikiSession encloses a protected static class, {@link SessionMonitor},
083 * to keep track of WikiSessions registered with each wiki.</p>
084 */
085public final class WikiSession implements WikiEventListener
086{
087
088    /** An anonymous user's session status. */
089    public static final String  ANONYMOUS             = "anonymous";
090
091    /** An asserted user's session status. */
092    public static final String  ASSERTED              = "asserted";
093
094    /** An authenticated user's session status. */
095    public static final String  AUTHENTICATED         = "authenticated";
096
097    private static final int    ONE                   = 48;
098
099    private static final int    NINE                  = 57;
100
101    private static final int    DOT                   = 46;
102
103    private static final Logger log                   = Logger.getLogger( WikiSession.class );
104
105    private static final String ALL                   = "*";
106
107    private static ThreadLocal<WikiSession> c_guestSession = new ThreadLocal<WikiSession>();
108
109    private final Subject       m_subject             = new Subject();
110
111    private final Map<String,Set<String>> m_messages  = new HashMap<String,Set<String>>();
112
113    /** The WikiEngine that created this session. */
114    private WikiEngine          m_engine              = null;
115
116    private String              m_status              = ANONYMOUS;
117
118    private Principal           m_userPrincipal       = WikiPrincipal.GUEST;
119
120    private Principal           m_loginPrincipal      = WikiPrincipal.GUEST;
121
122    private Locale              m_cachedLocale        = Locale.getDefault();
123
124    /**
125     * Returns <code>true</code> if one of this WikiSession's user Principals
126     * can be shown to belong to a particular wiki group. If the user is
127     * not authenticated, this method will always return <code>false</code>.
128     * @param group the group to test
129     * @return the result
130     */
131    protected boolean isInGroup( Group group )
132    {
133        for ( Principal principal : getPrincipals() )
134        {
135          if ( isAuthenticated() && group.isMember( principal ) )
136          {
137              return true;
138          }
139        }
140        return false;
141    }
142
143    /**
144     * Private constructor to prevent WikiSession from being instantiated
145     * directly.
146     */
147    private WikiSession()
148    {
149    }
150
151    /**
152     * Returns <code>true</code> if the user is considered asserted via
153     * a session cookie; that is, the Subject contains the Principal
154     * Role.ASSERTED.
155     * @return Returns <code>true</code> if the user is asserted
156     */
157    public boolean isAsserted()
158    {
159        return m_subject.getPrincipals().contains( Role.ASSERTED );
160    }
161
162    /**
163     * Returns the authentication status of the user's session. The user is
164     * considered authenticated if the Subject contains the Principal
165     * Role.AUTHENTICATED. If this method determines that an earlier
166     * LoginModule did not inject Role.AUTHENTICATED, it will inject one
167     * if the user is not anonymous <em>and</em> not asserted.
168     * @return Returns <code>true</code> if the user is authenticated
169     */
170    public boolean isAuthenticated()
171    {
172        // If Role.AUTHENTICATED is in principals set, always return true.
173        if ( m_subject.getPrincipals().contains( Role.AUTHENTICATED ) )
174        {
175            return true;
176        }
177
178        // With non-JSPWiki LoginModules, the role may not be there, so
179        // we need to add it if the user really is authenticated.
180        if ( !isAnonymous() && !isAsserted() )
181        {
182            // Inject AUTHENTICATED role
183            m_subject.getPrincipals().add( Role.AUTHENTICATED );
184            return true;
185        }
186
187        return false;
188    }
189
190    /**
191     * <p>Determines whether the current session is anonymous. This will be
192     * true if any of these conditions are true:</p>
193     * <ul>
194     *   <li>The session's Principal set contains
195     *       {@link org.apache.wiki.auth.authorize.Role#ANONYMOUS}</li>
196     *   <li>The session's Principal set contains
197     *       {@link org.apache.wiki.auth.WikiPrincipal#GUEST}</li>
198     *   <li>The Principal returned by {@link #getUserPrincipal()} evaluates
199     *       to an IP address.</li>
200     * </ul>
201     * <p>The criteria above are listed in the order in which they are
202     * evaluated.</p>
203     * @return whether the current user's identity is equivalent to an IP
204     * address
205     */
206    public boolean isAnonymous()
207    {
208        Set<Principal> principals = m_subject.getPrincipals();
209        return principals.contains( Role.ANONYMOUS ) ||
210                 principals.contains( WikiPrincipal.GUEST ) ||
211                 isIPV4Address( getUserPrincipal().getName() );
212    }
213
214    /**
215     * <p> Returns the Principal used to log in to an authenticated session. The
216     * login principal is determined by examining the Subject's Principal set
217     * for PrincipalWrappers or WikiPrincipals with type designator
218     * <code>LOGIN_NAME</code>; the first one found is the login principal.
219     * If one is not found, this method returns the first principal that isn't
220     * of type Role or GroupPrincipal. If neither of these conditions hold, this method returns
221     * {@link org.apache.wiki.auth.WikiPrincipal#GUEST}.
222     * @return the login Principal. If it is a PrincipalWrapper containing an
223     * externally-provided Principal, the object returned is the Principal, not
224     * the wrapper around it.
225     */
226    public Principal getLoginPrincipal()
227    {
228        return m_loginPrincipal;
229    }
230
231    /**
232     * <p>Returns the primary user Principal associated with this session. The
233     * primary user principal is determined as follows:</p> <ol> <li>If the
234     * Subject's Principal set contains WikiPrincipals, the first WikiPrincipal
235     * with type designator <code>WIKI_NAME</code> or (alternatively)
236     * <code>FULL_NAME</code> is the primary Principal.</li>
237     *   <li>For all other cases, the first Principal in the Subject's principal
238     *       collection that that isn't of type Role or GroupPrincipal is the primary.</li>
239     * </ol>
240     * If no primary user Principal is found, this method returns
241     * {@link org.apache.wiki.auth.WikiPrincipal#GUEST}.
242     * @return the primary user Principal
243     */
244    public Principal getUserPrincipal()
245    {
246        return m_userPrincipal;
247    }
248
249    /**
250     *  Returns a cached Locale object for this user.  It's better to use
251     *  WikiContext's corresponding getBundle() method, since that will actually
252     *  react if the user changes the locale in the middle, but if that's not
253     *  available (or, for some reason, you need the speed), this method can
254     *  also be used.  The Locale expires when the WikiSession expires, and
255     *  currently there is no way to reset the Locale.
256     *
257     *  @return A cached Locale object
258     *  @since 2.5.96
259     */
260    public Locale getLocale()
261    {
262        return m_cachedLocale;
263    }
264
265    /**
266     * Adds a message to the generic list of messages associated with the
267     * session. These messages retain their order of insertion and remain until
268     * the {@link #clearMessages()} method is called.
269     * @param message the message to add; if <code>null</code> it is ignored.
270     */
271    public void addMessage(String message)
272    {
273        addMessage( ALL, message );
274    }
275
276
277    /**
278     * Adds a message to the specific set of messages associated with the
279     * session. These messages retain their order of insertion and remain until
280     * the {@link #clearMessages()} method is called.
281     * @param topic the topic to associate the message to;
282     * @param message the message to add
283     */
284    public void addMessage(String topic, String message)
285    {
286        if ( topic == null )
287        {
288            throw new IllegalArgumentException( "addMessage: topic cannot be null." );
289        }
290        Set<String> messages = m_messages.get( topic );
291        if (messages == null )
292        {
293            messages = new LinkedHashSet<String>();
294            m_messages.put( topic, messages );
295        }
296        messages.add( StringUtils.defaultString( message ) );
297    }
298
299    /**
300     * Clears all messages associated with this session.
301     */
302    public void clearMessages()
303    {
304        m_messages.clear();
305    }
306
307    /**
308     * Clears all messages associated with a session topic.
309     * @param topic the topic whose messages should be cleared.
310     */
311    public void clearMessages( String topic )
312    {
313        Set<String> messages = m_messages.get( topic );
314        if ( messages != null )
315        {
316            m_messages.clear();
317        }
318    }
319
320    /**
321     * Returns all generic messages associated with this session.
322     * The messages stored with the session persist throughout the
323     * session unless they have been reset with {@link #clearMessages()}.
324     * @return the current messages.
325     */
326    public String[] getMessages()
327    {
328        return getMessages( ALL );
329    }
330
331    /**
332     * Returns all messages associated with a session topic.
333     * The messages stored with the session persist throughout the
334     * session unless they have been reset with {@link #clearMessages(String)}.
335     * @return the current messages.
336     * @param topic The topic
337     */
338    public String[] getMessages( String topic )
339    {
340        Set<String> messages = m_messages.get( topic );
341        if ( messages == null || messages.size() == 0 )
342        {
343            return new String[0];
344        }
345        return messages.toArray( new String[messages.size()] );
346    }
347
348    /**
349     * Returns all user Principals associated with this session. User principals
350     * are those in the Subject's principal collection that aren't of type Role or
351     * of type GroupPrincipal. This is a defensive copy.
352     * @return Returns the user principal
353     * @see org.apache.wiki.auth.AuthenticationManager#isUserPrincipal(Principal)
354     */
355    public Principal[] getPrincipals()
356    {
357        ArrayList<Principal> principals = new ArrayList<Principal>();
358
359        // Take the first non Role as the main Principal
360        for( Principal principal : m_subject.getPrincipals() )
361        {
362            if ( AuthenticationManager.isUserPrincipal( principal ) )
363            {
364                principals.add( principal );
365            }
366        }
367
368        return principals.toArray( new Principal[principals.size()] );
369    }
370
371    /**
372     * Returns an array of Principal objects that represents the groups and
373     * roles that the user associated with a WikiSession possesses. The array is
374     * built by iterating through the Subject's Principal set and extracting all
375     * Role and GroupPrincipal objects into a list. The list is returned as an
376     * array sorted in the natural order implied by each Principal's
377     * <code>getName</code> method. Note that this method does <em>not</em>
378     * consult the external Authorizer or GroupManager; it relies on the
379     * Principals that have been injected into the user's Subject at login time,
380     * or after group creation/modification/deletion.
381     * @return an array of Principal objects corresponding to the roles the
382     *         Subject possesses
383     */
384    public Principal[] getRoles()
385    {
386        Set<Principal> roles = new HashSet<Principal>();
387
388        // Add all of the Roles possessed by the Subject directly
389        roles.addAll( m_subject.getPrincipals( Role.class ) );
390
391        // Add all of the GroupPrincipals possessed by the Subject directly
392        roles.addAll( m_subject.getPrincipals( GroupPrincipal.class ) );
393
394        // Return a defensive copy
395        Principal[] roleArray = roles.toArray( new Principal[roles.size()] );
396        Arrays.sort( roleArray, WikiPrincipal.COMPARATOR );
397        return roleArray;
398    }
399
400    /**
401     * Removes the wiki session associated with the user's HTTP request
402     * from the cache of wiki sessions, typically as part of a logout
403     * process.
404     * @param engine the wiki engine
405     * @param request the users's HTTP request
406     */
407    public static void removeWikiSession( WikiEngine engine, HttpServletRequest request )
408    {
409        if ( engine == null || request == null )
410        {
411            throw new IllegalArgumentException( "Request or engine cannot be null." );
412        }
413        SessionMonitor monitor = SessionMonitor.getInstance( engine );
414        monitor.remove( request.getSession() );
415    }
416
417    /**
418     * Returns <code>true</code> if the WikiSession's Subject
419     * possess a supplied Principal. This method eliminates the need
420     * to externally request and inspect the JAAS subject.
421     * @param principal the Principal to test
422     * @return the result
423     */
424    public boolean hasPrincipal( Principal principal )
425    {
426        return m_subject.getPrincipals().contains( principal );
427
428    }
429
430    /**
431     * Listens for WikiEvents generated by source objects such as the
432     * GroupManager. This method adds Principals to the private Subject managed
433     * by the WikiSession.
434     * @see org.apache.wiki.event.WikiEventListener#actionPerformed(org.apache.wiki.event.WikiEvent)
435     */
436    public void actionPerformed( WikiEvent event )
437    {
438        if ( event instanceof WikiSecurityEvent )
439        {
440            WikiSecurityEvent e = (WikiSecurityEvent)event;
441            if ( e.getTarget() != null )
442            {
443                switch (e.getType() )
444                {
445                    case WikiSecurityEvent.GROUP_ADD:
446                    {
447                        Group group = (Group)e.getTarget();
448                        if ( isInGroup( group ) )
449                        {
450                            m_subject.getPrincipals().add( group.getPrincipal() );
451                        }
452                        break;
453                    }
454                    case WikiSecurityEvent.GROUP_REMOVE:
455                    {
456                        Group group = (Group)e.getTarget();
457                        if ( m_subject.getPrincipals().contains( group.getPrincipal() ) )
458                        {
459                            m_subject.getPrincipals().remove( group.getPrincipal() );
460                        }
461                        break;
462                    }
463                    case WikiSecurityEvent.GROUP_CLEAR_GROUPS:
464                    {
465                        m_subject.getPrincipals().removeAll( m_subject.getPrincipals( GroupPrincipal.class ) );
466                        break;
467                    }
468                    case WikiSecurityEvent.LOGIN_INITIATED:
469                    {
470                        // Do nothing
471                    }
472                    case WikiSecurityEvent.PRINCIPAL_ADD:
473                    {
474                        WikiSession target = (WikiSession)e.getTarget();
475                        if ( this.equals( target ) && m_status == AUTHENTICATED )
476                        {
477                            Set<Principal> principals = m_subject.getPrincipals();
478                            principals.add( (Principal)e.getPrincipal());
479                        }
480                        break;
481                    }
482                    case WikiSecurityEvent.LOGIN_ANONYMOUS:
483                    {
484                        WikiSession target = (WikiSession)e.getTarget();
485                        if ( this.equals( target ) )
486                        {
487                            m_status = ANONYMOUS;
488                            
489                            // Set the login/user principals and login status
490                            Set<Principal> principals = m_subject.getPrincipals();
491                            m_loginPrincipal = (Principal)e.getPrincipal();
492                            m_userPrincipal = m_loginPrincipal;
493                            
494                            // Add the login principal to the Subject, and set the built-in roles
495                            principals.clear();
496                            principals.add( m_loginPrincipal );
497                            principals.add( Role.ALL );
498                            principals.add( Role.ANONYMOUS );
499                        }
500                        break;
501                    }
502                    case WikiSecurityEvent.LOGIN_ASSERTED:
503                    {
504                        WikiSession target = (WikiSession)e.getTarget();
505                        if ( this.equals( target ) )
506                        {
507                            m_status = ASSERTED;
508                            
509                            // Set the login/user principals and login status
510                            Set<Principal> principals = m_subject.getPrincipals();
511                            m_loginPrincipal = (Principal)e.getPrincipal();
512                            m_userPrincipal = m_loginPrincipal;
513                            
514                            // Add the login principal to the Subject, and set the built-in roles
515                            principals.clear();
516                            principals.add( m_loginPrincipal );
517                            principals.add( Role.ALL );
518                            principals.add( Role.ASSERTED );
519                        }
520                        break;
521                    }
522                    case WikiSecurityEvent.LOGIN_AUTHENTICATED:
523                    {
524                        WikiSession target = (WikiSession)e.getTarget();
525                        if ( this.equals( target ) )
526                        {
527                            m_status = AUTHENTICATED;
528                            
529                            // Set the login/user principals and login status
530                            Set<Principal> principals = m_subject.getPrincipals();
531                            m_loginPrincipal = (Principal)e.getPrincipal();
532                            m_userPrincipal = m_loginPrincipal;
533                            
534                            // Add the login principal to the Subject, and set the built-in roles
535                            principals.clear();
536                            principals.add( m_loginPrincipal );
537                            principals.add( Role.ALL );
538                            principals.add( Role.AUTHENTICATED );
539                            
540                            // Add the user and group principals
541                            injectUserProfilePrincipals();  // Add principals for the user profile
542                            injectGroupPrincipals();  // Inject group principals
543                        }
544                        break;
545                    }
546                    case WikiSecurityEvent.PROFILE_SAVE:
547                    {
548                        WikiSession source = e.getSrc();
549                        if ( this.equals( source ) )
550                        {
551                            injectUserProfilePrincipals();  // Add principals for the user profile
552                            injectGroupPrincipals();  // Inject group principals
553                        }
554                        break;
555                    }
556                    case WikiSecurityEvent.PROFILE_NAME_CHANGED:
557                    {
558                        // Refresh user principals based on new user profile
559                        WikiSession source = e.getSrc();
560                        if ( this.equals( source ) && m_status == AUTHENTICATED )
561                        {
562                            // To prepare for refresh, set the new full name as the primary principal
563                            UserProfile[] profiles = (UserProfile[])e.getTarget();
564                            UserProfile newProfile = profiles[1];
565                            if ( newProfile.getFullname() == null )
566                            {
567                                throw new IllegalStateException( "User profile FullName cannot be null." );
568                            }
569                            
570                            Set<Principal> principals = m_subject.getPrincipals();
571                            m_loginPrincipal = new WikiPrincipal( newProfile.getLoginName() );
572                            
573                            // Add the login principal to the Subject, and set the built-in roles
574                            principals.clear();
575                            principals.add( m_loginPrincipal );
576                            principals.add( Role.ALL );
577                            principals.add( Role.AUTHENTICATED );
578                            
579                            // Add the user and group principals
580                            injectUserProfilePrincipals();  // Add principals for the user profile
581                            injectGroupPrincipals();  // Inject group principals
582                        }
583                        break;
584                    }
585
586                    //
587                    //  No action, if the event is not recognized.
588                    //
589                    default:
590                        break;
591                }
592            }
593        }
594    }
595
596    /**
597     * Invalidates the WikiSession and resets its Subject's
598     * Principals to the equivalent of a "guest session".
599     */
600    public void invalidate()
601    {
602        m_subject.getPrincipals().clear();
603        m_subject.getPrincipals().add( WikiPrincipal.GUEST );
604        m_subject.getPrincipals().add( Role.ANONYMOUS );
605        m_subject.getPrincipals().add( Role.ALL );
606        m_userPrincipal = WikiPrincipal.GUEST;
607        m_loginPrincipal = WikiPrincipal.GUEST;
608    }
609
610    /**
611     * Injects GroupPrincipal objects into the user's Principal set based on the
612     * groups the user belongs to. For Groups, the algorithm first calls the
613     * {@link GroupManager#getRoles()} to obtain the array of GroupPrincipals
614     * the authorizer knows about. Then, the method
615     * {@link GroupManager#isUserInRole(WikiSession, Principal)} is called for
616     * each Principal. If the user is a member of the group, an equivalent
617     * GroupPrincipal is injected into the user's principal set. Existing
618     * GroupPrincipals are flushed and replaced. This method should generally be
619     * called after a user's {@link org.apache.wiki.auth.user.UserProfile} is
620     * saved. If the wiki session is null, or there is no matching user profile,
621     * the method returns silently.
622     */
623    protected void injectGroupPrincipals()
624    {
625        // Flush the existing GroupPrincipals
626        m_subject.getPrincipals().removeAll( m_subject.getPrincipals(GroupPrincipal.class) );
627        
628        // Get the GroupManager and test for each Group
629        GroupManager manager = m_engine.getGroupManager();
630        for ( Principal group : manager.getRoles() )
631        {
632            if ( manager.isUserInRole( this, group ) )
633            {
634                m_subject.getPrincipals().add( group );
635            }
636        }
637    }
638
639    /**
640     * Adds Principal objects to the Subject that correspond to the
641     * logged-in user's profile attributes for the wiki name, full name
642     * and login name. These Principals will be WikiPrincipals, and they
643     * will replace all other WikiPrincipals in the Subject. <em>Note:
644     * this method is never called during anonymous or asserted sessions.</em>
645     */
646    protected void injectUserProfilePrincipals()
647    {
648        // Search for the user profile
649        String searchId = m_loginPrincipal.getName();
650        if ( searchId == null )
651        {
652            // Oh dear, this wasn't an authenticated user after all
653            log.info("Refresh principals failed because WikiSession had no user Principal; maybe not logged in?");
654            return;
655        }
656
657        // Look up the user and go get the new Principals
658        UserDatabase database = m_engine.getUserManager().getUserDatabase();
659        if ( database == null )
660        {
661            throw new IllegalStateException( "User database cannot be null." );
662        }
663        try
664        {
665            UserProfile profile = database.find( searchId );
666            Principal[] principals = database.getPrincipals( profile.getLoginName() );
667            for ( Principal principal : principals )
668            {
669                // Add the Principal to the Subject
670                m_subject.getPrincipals().add( principal );
671                
672                // Set the user principal if needed; we prefer FullName, but the WikiName will also work
673                boolean isFullNamePrincipal = ( principal instanceof WikiPrincipal && ((WikiPrincipal)principal).getType() == WikiPrincipal.FULL_NAME );
674                if ( isFullNamePrincipal )
675                {
676                   m_userPrincipal = principal; 
677                }
678                else if ( !( m_userPrincipal instanceof WikiPrincipal ) )
679                {
680                    m_userPrincipal = principal; 
681                }
682            }
683        }
684        catch ( NoSuchPrincipalException e )
685        {
686            // We will get here if the user has a principal but not a profile
687            // For example, it's a container-managed user who hasn't set up a profile yet
688            log.warn("User profile '" + searchId + "' not found. This is normal for container-auth users who haven't set up a profile yet.");
689        }
690    }
691
692    /**
693     * <p>Returns the status of the wiki session as a text string. Valid values are:</p>
694     * <ul>
695     *   <li>{@link #AUTHENTICATED}</li>
696     *   <li>{@link #ASSERTED}</li>
697     *   <li>{@link #ANONYMOUS}</li>
698     * </ul>
699     * @return the user's session status
700     */
701    public String getStatus()
702    {
703        return m_status;
704    }
705
706    /**
707     * <p>Static factory method that returns the WikiSession object associated with
708     * the current HTTP request. This method looks up the associated HttpSession
709     * in an internal WeakHashMap and attempts to retrieve the WikiSession. If
710     * not found, one is created. This method is guaranteed to always return a
711     * WikiSession, although the authentication status is unpredictable until
712     * the user attempts to log in. If the servlet request parameter is
713     * <code>null</code>, a synthetic {@link #guestSession(WikiEngine)}is returned.</p>
714     * <p>When a session is created, this method attaches a WikiEventListener
715     * to the GroupManager so that changes to groups are detected automatically.</p>
716     * @param engine the wiki engine
717     * @param request the servlet request object
718     * @return the existing (or newly created) wiki session
719     */
720    public static WikiSession getWikiSession( WikiEngine engine, HttpServletRequest request )
721    {
722        // If request is null, return guest session
723        if ( request == null )
724        {
725            if ( log.isDebugEnabled() )
726            {
727                log.debug( "Looking up WikiSession for NULL HttpRequest: returning guestSession()" );
728            }
729            return staticGuestSession( engine );
730        }
731
732        // Look for a WikiSession associated with the user's Http Session
733        // and create one if it isn't there yet.
734        HttpSession session = request.getSession();
735        SessionMonitor monitor = SessionMonitor.getInstance( engine );
736        WikiSession wikiSession = monitor.find( session );
737
738        // Attach reference to wiki engine
739        wikiSession.m_engine = engine;
740
741        wikiSession.m_cachedLocale = request.getLocale();
742
743        return wikiSession;
744    }
745
746    /**
747     * Static factory method that creates a new "guest" session containing a single
748     * user Principal {@link org.apache.wiki.auth.WikiPrincipal#GUEST},
749     * plus the role principals {@link Role#ALL} and
750     * {@link Role#ANONYMOUS}. This method also adds the session as a listener
751     * for GroupManager, AuthenticationManager and UserManager events.
752     * @param engine the wiki engine
753     * @return the guest wiki session
754     */
755    public static WikiSession guestSession( WikiEngine engine )
756    {
757        WikiSession session = new WikiSession();
758        session.m_engine = engine;
759        session.invalidate();
760
761        // Add the session as listener for GroupManager, AuthManager, UserManager events
762        GroupManager groupMgr = engine.getGroupManager();
763        AuthenticationManager authMgr = engine.getAuthenticationManager();
764        UserManager userMgr = engine.getUserManager();
765        groupMgr.addWikiEventListener( session );
766        authMgr.addWikiEventListener( session );
767        userMgr.addWikiEventListener( session );
768
769        return session;
770    }
771
772    /**
773     *  Returns a static guest session, which is available for this
774     *  thread only.  This guest session is used internally whenever
775     *  there is no HttpServletRequest involved, but the request is
776     *  done e.g. when embedding JSPWiki code.
777     *
778     *  @param engine WikiEngine for this session
779     *  @return A static WikiSession which is shared by all in this
780     *          same Thread.
781     */
782    // FIXME: Should really use WeakReferences to clean away unused sessions.
783
784    private static WikiSession staticGuestSession( WikiEngine engine )
785    {
786        WikiSession session = c_guestSession.get();
787
788        if( session == null )
789        {
790            session = guestSession( engine );
791
792            c_guestSession.set( session );
793        }
794
795        return session;
796    }
797
798    /**
799     * Returns the total number of active wiki sessions for a
800     * particular wiki. This method delegates to the wiki's
801     * {@link SessionMonitor#sessions()} method.
802     * @param engine the wiki session
803     * @return the number of sessions
804     */
805    public static int sessions( WikiEngine engine )
806    {
807        SessionMonitor monitor = SessionMonitor.getInstance( engine );
808        return monitor.sessions();
809    }
810
811    /**
812     * Returns Principals representing the current users known
813     * to a particular wiki. Each Principal will correspond to the
814     * value returned by each WikiSession's {@link #getUserPrincipal()}
815     * method. This method delegates to {@link SessionMonitor#userPrincipals()}.
816     * @param engine the wiki engine
817     * @return an array of Principal objects, sorted by name
818     */
819    public static Principal[] userPrincipals( WikiEngine engine )
820    {
821        SessionMonitor monitor = SessionMonitor.getInstance( engine );
822        return monitor.userPrincipals();
823    }
824
825    /**
826     * Wrapper for
827     * {@link javax.security.auth.Subject#doAsPrivileged(Subject, java.security.PrivilegedExceptionAction, java.security.AccessControlContext)}
828     * that executes an action with the privileges posssessed by a
829     * WikiSession's Subject. The action executes with a <code>null</code>
830     * AccessControlContext, which has the effect of running it "cleanly"
831     * without the AccessControlContexts of the caller.
832     * @param session the wiki session
833     * @param action the privileged action
834     * @return the result of the privileged action; may be <code>null</code>
835     * @throws java.security.AccessControlException if the action is not permitted
836     * by the security policy
837     */
838    public static Object doPrivileged( WikiSession session, PrivilegedAction<?> action ) throws AccessControlException
839    {
840        return Subject.doAsPrivileged( session.m_subject, action, null );
841    }
842
843    /**
844     * Verifies whether a String represents an IPv4 address. The algorithm is
845     * extremely efficient and does not allocate any objects.
846     * @param name the address to test
847     * @return the result
848     */
849    protected static boolean isIPV4Address( String name )
850    {
851        if ( name.charAt( 0 ) == DOT || name.charAt( name.length() - 1 ) == DOT )
852        {
853            return false;
854        }
855
856        int[] addr = new int[]
857        { 0, 0, 0, 0 };
858        int currentOctet = 0;
859        for( int i = 0; i < name.length(); i++ )
860        {
861            int ch = name.charAt( i );
862            boolean isDigit = ch >= ONE && ch <= NINE;
863            boolean isDot = ch == DOT;
864            if ( !isDigit && !isDot )
865            {
866                return false;
867            }
868            if ( isDigit )
869            {
870                addr[currentOctet] = 10 * addr[currentOctet] + ( ch - ONE );
871                if ( addr[currentOctet] > 255 )
872                {
873                    return false;
874                }
875            }
876            else if ( name.charAt( i - 1 ) == DOT )
877            {
878                return false;
879            }
880            else
881            {
882                currentOctet++;
883            }
884        }
885        return  currentOctet == 3;
886    }
887
888}