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.auth;
020
021import java.io.File;
022import java.io.FileNotFoundException;
023import java.io.FileOutputStream;
024import java.io.IOException;
025import java.io.InputStream;
026import java.io.OutputStream;
027import java.net.MalformedURLException;
028import java.net.URL;
029import java.security.Principal;
030import java.util.Collections;
031import java.util.HashMap;
032import java.util.HashSet;
033import java.util.Map;
034import java.util.Properties;
035import java.util.Set;
036
037import javax.security.auth.Subject;
038import javax.security.auth.callback.CallbackHandler;
039import javax.security.auth.login.LoginException;
040import javax.security.auth.spi.LoginModule;
041import javax.servlet.http.HttpServletRequest;
042import javax.servlet.http.HttpSession;
043
044import org.apache.commons.io.IOUtils;
045import org.apache.log4j.Logger;
046import org.apache.wiki.WikiEngine;
047import org.apache.wiki.WikiSession;
048import org.apache.wiki.api.exceptions.WikiException;
049import org.apache.wiki.auth.authorize.Role;
050import org.apache.wiki.auth.authorize.WebAuthorizer;
051import org.apache.wiki.auth.authorize.WebContainerAuthorizer;
052import org.apache.wiki.auth.login.AnonymousLoginModule;
053import org.apache.wiki.auth.login.CookieAssertionLoginModule;
054import org.apache.wiki.auth.login.CookieAuthenticationLoginModule;
055import org.apache.wiki.auth.login.UserDatabaseLoginModule;
056import org.apache.wiki.auth.login.WebContainerCallbackHandler;
057import org.apache.wiki.auth.login.WebContainerLoginModule;
058import org.apache.wiki.auth.login.WikiCallbackHandler;
059import org.apache.wiki.event.WikiEventListener;
060import org.apache.wiki.event.WikiEventManager;
061import org.apache.wiki.event.WikiSecurityEvent;
062import org.apache.wiki.util.TextUtil;
063import org.apache.wiki.util.TimedCounterList;
064
065/**
066 * Manages authentication activities for a WikiEngine: user login, logout, and
067 * credential refreshes. This class uses JAAS to determine how users log in.
068 * <p>
069 * The login procedure is protected in addition by a mechanism which prevents
070 * a hacker to try and force-guess passwords by slowing down attempts to log in
071 * into the same account.  Every login attempt is recorded, and stored for a while
072 * (currently ten minutes), and each login attempt during that time incurs a penalty
073 * of 2^login attempts milliseconds - that is, 10 login attempts incur a login penalty of 1.024 seconds.
074 * The delay is currently capped to 20 seconds.
075 * 
076 * @since 2.3
077 */
078public class AuthenticationManager {
079
080    /** How many milliseconds the logins are stored before they're cleaned away. */
081    private static final long LASTLOGINS_CLEANUP_TIME = 10*60*1000L; // Ten minutes
082
083    private static final long MAX_LOGIN_DELAY         = 20*1000L; // 20 seconds
084    
085    /** The name of the built-in cookie assertion module */
086    public static final String                 COOKIE_MODULE       =  CookieAssertionLoginModule.class.getName();
087
088    /** The name of the built-in cookie authentication module */
089    public static final String                 COOKIE_AUTHENTICATION_MODULE =  CookieAuthenticationLoginModule.class.getName();
090
091    /** If this jspwiki.properties property is <code>true</code>, logs the IP address of the editor on saving. */
092    public static final String                 PROP_STOREIPADDRESS = "jspwiki.storeIPAddress";
093    
094    /** If this jspwiki.properties property is <code>true</code>, allow cookies to be used for authentication. */
095    public static final String                 PROP_ALLOW_COOKIE_AUTH = "jspwiki.cookieAuthentication";
096    
097    /**
098     *  This property determines whether we use JSPWiki authentication or not.
099     *  Possible values are AUTH_JAAS or AUTH_CONTAINER.
100     *  <p>
101     *  Setting this is now deprecated - we do not guarantee that it works.
102     *  
103     * @deprecated - to be removed on 2.11.0
104     */
105    @Deprecated
106    public  static final String                PROP_SECURITY       = "jspwiki.security";
107
108    /** Value specifying that the user wants to use the container-managed security, just like in JSPWiki 2.2.
109     * @deprecated - to be removed on 2.11.0
110      */
111    @Deprecated
112    public static final String                SECURITY_OFF      = "off";
113
114    /** Value specifying that the user wants to use the built-in JAAS-based system.
115     * @deprecated - to be removed on 2.11.0
116     */
117    @Deprecated
118    public static final String                SECURITY_JAAS     = "jaas";
119
120    /** Whether logins should be throttled to limit brute-forcing attempts. Defaults to true. */
121    public static final String                 PROP_LOGIN_THROTTLING = "jspwiki.login.throttling";
122
123    protected static final Logger              log                 = Logger.getLogger( AuthenticationManager.class );
124
125    /** Prefix for LoginModule options key/value pairs. */
126    protected static final String                 PREFIX_LOGIN_MODULE_OPTIONS = "jspwiki.loginModule.options.";
127
128    /** If this jspwiki.properties property is <code>true</code>, allow cookies to be used to assert identities. */
129    protected static final String                 PROP_ALLOW_COOKIE_ASSERTIONS = "jspwiki.cookieAssertions";
130
131    /** The {@link javax.security.auth.spi.LoginModule} to use for custom authentication. */
132    protected static final String                 PROP_LOGIN_MODULE = "jspwiki.loginModule.class";
133    
134    /** Empty Map passed to JAAS {@link #doJAASLogin(Class, CallbackHandler, Map)} method. */
135    protected static final Map<String,String> EMPTY_MAP = Collections.unmodifiableMap( new HashMap<String,String>() );
136    
137    /** Class (of type LoginModule) to use for custom authentication. */
138    protected Class<? extends LoginModule> m_loginModuleClass = UserDatabaseLoginModule.class;
139    
140    /** Options passed to {@link javax.security.auth.spi.LoginModule#initialize(Subject, CallbackHandler, Map, Map)}; 
141     * initialized by {@link #initialize(WikiEngine, Properties)}. */
142    protected Map<String,String> m_loginModuleOptions = new HashMap<String,String>();
143
144    /** Just to provide compatibility with the old versions.  The same
145     *  as SECURITY_OFF.
146     *
147     *  @deprecated use {@link #SECURITY_OFF} instead - to be removed on 2.11.0
148     */
149    @Deprecated
150    protected static final String             SECURITY_CONTAINER = "container";
151
152    /** The default {@link javax.security.auth.spi.LoginModule} class name to use for custom authentication. */
153    private static final String                 DEFAULT_LOGIN_MODULE = "org.apache.wiki.auth.login.UserDatabaseLoginModule";
154    
155    /** Empty principal set. */
156    private static final Set<Principal> NO_PRINCIPALS = new HashSet<Principal>();
157
158    /** Static Boolean for lazily-initializing the "allows assertions" flag */
159    private boolean                     m_allowsCookieAssertions  = true;
160
161    private boolean                     m_throttleLogins = true;
162
163    /** Static Boolean for lazily-initializing the "allows cookie authentication" flag */
164    private boolean                     m_allowsCookieAuthentication = false;
165
166    private WikiEngine                         m_engine            = null;
167    
168    /** If true, logs the IP address of the editor */
169    private boolean                            m_storeIPAddress    = true;
170
171    /** Keeps a list of the usernames who have attempted a login recently. */
172    private TimedCounterList<String> m_lastLoginAttempts = new TimedCounterList<String>();
173    
174    /**
175     * Creates an AuthenticationManager instance for the given WikiEngine and
176     * the specified set of properties. All initialization for the modules is
177     * done here.
178     * @param engine the wiki engine
179     * @param props the properties used to initialize the wiki engine
180     * @throws WikiException if the AuthenticationManager cannot be initialized
181     */
182    @SuppressWarnings("unchecked")
183    public void initialize( WikiEngine engine, Properties props ) throws WikiException
184    {
185        m_engine = engine;
186        m_storeIPAddress = TextUtil.getBooleanProperty( props, PROP_STOREIPADDRESS, m_storeIPAddress );
187
188        // Should we allow cookies for assertions? (default: yes)
189        m_allowsCookieAssertions = TextUtil.getBooleanProperty( props,
190                                                              PROP_ALLOW_COOKIE_ASSERTIONS,
191                                                              true );
192        
193        // Should we allow cookies for authentication? (default: no)
194        m_allowsCookieAuthentication = TextUtil.getBooleanProperty( props,
195                                                                    PROP_ALLOW_COOKIE_AUTH,
196                                                                    false );
197        
198        // Should we throttle logins? (default: yes)
199        m_throttleLogins = TextUtil.getBooleanProperty( props,
200                                                        PROP_LOGIN_THROTTLING,
201                                                        true );
202
203        // Look up the LoginModule class
204        String loginModuleClassName = TextUtil.getStringProperty( props, PROP_LOGIN_MODULE, DEFAULT_LOGIN_MODULE );
205        try
206        {
207            m_loginModuleClass = (Class<? extends LoginModule>) Class.forName( loginModuleClassName );
208        }
209        catch (ClassNotFoundException e)
210        {
211            e.printStackTrace();
212            throw new WikiException( "Could not instantiate LoginModule class.", e );
213        }
214        
215        // Initialize the LoginModule options
216        initLoginModuleOptions( props );
217    }
218
219    /**
220     * Returns true if this WikiEngine uses container-managed authentication.
221     * This method is used primarily for cosmetic purposes in the JSP tier, and
222     * performs no meaningful security function per se. Delegates to
223     * {@link org.apache.wiki.auth.authorize.WebContainerAuthorizer#isContainerAuthorized()},
224     * if used as the external authorizer; otherwise, returns <code>false</code>.
225     * @return <code>true</code> if the wiki's authentication is managed by
226     *         the container, <code>false</code> otherwise
227     */
228    public boolean isContainerAuthenticated()
229    {
230        try
231        {
232            Authorizer authorizer = m_engine.getAuthorizationManager().getAuthorizer();
233            if ( authorizer instanceof WebContainerAuthorizer )
234            {
235                 return ( ( WebContainerAuthorizer )authorizer ).isContainerAuthorized();
236            }
237        }
238        catch ( WikiException e )
239        {
240            // It's probably ok to fail silently...
241        }
242        return false;
243    }
244
245    /**
246     * <p>Logs in the user by attempting to populate a WikiSession Subject from
247     * a web servlet request by examining the request
248     *  for the presence of container credentials and user cookies. The processing
249     * logic is as follows:
250     * </p>
251     * <ul>
252     * <li>If the WikiSession had previously been unauthenticated, check to see if
253     * user has subsequently authenticated. To be considered "authenticated,"
254     * the request must supply one of the following (in order of preference):
255     * the container <code>userPrincipal</code>, container <code>remoteUser</code>,
256     * or authentication cookie. If the user is authenticated, this method fires event
257     * {@link org.apache.wiki.event.WikiSecurityEvent#LOGIN_AUTHENTICATED}
258     * with two parameters: a Principal representing the login principal,
259     * and the current WikiSession. In addition, if the authorizer is of type
260     * WebContainerAuthorizer, this method iterates through the container roles returned by
261     * {@link org.apache.wiki.auth.authorize.WebContainerAuthorizer#getRoles()},
262     * tests for membership in each one, and adds those that pass to the Subject's principal set.</li>
263     * <li>If, after checking for authentication, the WikiSession is still Anonymous,
264     * this method next checks to see if the user has "asserted" an identity
265     * by supplying an assertion cookie. If the user is found to be asserted,
266     * this method fires event {@link org.apache.wiki.event.WikiSecurityEvent#LOGIN_ASSERTED}
267     * with two parameters: <code>WikiPrincipal(<em>cookievalue</em>)</code>, and
268     * the current WikiSession.</li>
269     * <li>If, after checking for authenticated and asserted status, the  WikiSession is
270     * <em>still</em> anonymous, this method fires event
271     * {@link org.apache.wiki.event.WikiSecurityEvent#LOGIN_ANONYMOUS} with
272     * two parameters: <code>WikiPrincipal(<em>remoteAddress</em>)</code>,
273     * and the current WikiSession </li>
274     * </ul>
275     * @param request servlet request for this user
276     * @return always returns <code>true</code> (because anonymous login, at least, will always succeed)
277     * @throws org.apache.wiki.auth.WikiSecurityException if the user cannot be logged in for any reason
278     * @since 2.3
279     */
280    public boolean login( HttpServletRequest request ) throws WikiSecurityException
281    {
282        HttpSession httpSession = request.getSession();
283        WikiSession session = SessionMonitor.getInstance(m_engine).find( httpSession );
284        AuthenticationManager authenticationMgr = m_engine.getAuthenticationManager();
285        AuthorizationManager authorizationMgr = m_engine.getAuthorizationManager();
286        CallbackHandler handler = null;
287        Map<String,String> options = EMPTY_MAP;
288
289        // If user not authenticated, check if container logged them in, or if
290        // there's an authentication cookie
291        if ( !session.isAuthenticated() )
292        {
293            // Create a callback handler
294            handler = new WebContainerCallbackHandler( m_engine, request );
295            
296            // Execute the container login module, then (if that fails) the cookie auth module
297            Set<Principal> principals = authenticationMgr.doJAASLogin( WebContainerLoginModule.class, handler, options );
298            if ( principals.size() == 0 && authenticationMgr.allowsCookieAuthentication() )
299            {
300                principals = authenticationMgr.doJAASLogin( CookieAuthenticationLoginModule.class, handler, options );
301            }
302            
303            // If the container logged the user in successfully, tell the WikiSession (and add all of the Principals)
304            if ( principals.size() > 0 )
305            {
306                fireEvent( WikiSecurityEvent.LOGIN_AUTHENTICATED, getLoginPrincipal( principals ), session );
307                for ( Principal principal : principals )
308                {
309                    fireEvent( WikiSecurityEvent.PRINCIPAL_ADD, principal, session );
310                }
311                
312                // Add all appropriate Authorizer roles
313                injectAuthorizerRoles( session, authorizationMgr.getAuthorizer(), request );
314            }
315        }
316
317        // If user still not authenticated, check if assertion cookie was supplied
318        if ( !session.isAuthenticated() && authenticationMgr.allowsCookieAssertions() )
319        {
320            // Execute the cookie assertion login module
321            Set<Principal> principals = authenticationMgr.doJAASLogin( CookieAssertionLoginModule.class, handler, options );
322            if ( principals.size() > 0 )
323            {
324                fireEvent( WikiSecurityEvent.LOGIN_ASSERTED, getLoginPrincipal( principals ), session);
325            }
326        }
327
328        // If user still anonymous, use the remote address
329        if (session.isAnonymous() )
330        {
331            Set<Principal> principals = authenticationMgr.doJAASLogin( AnonymousLoginModule.class, handler, options );
332            if ( principals.size() > 0 )
333            {
334                fireEvent( WikiSecurityEvent.LOGIN_ANONYMOUS, getLoginPrincipal( principals ), session );
335                return true;
336            }
337        }
338        
339        // If by some unusual turn of events the Anonymous login module doesn't work, login failed!
340        return false;
341    }
342    
343    /**
344     * Attempts to perform a WikiSession login for the given username/password
345     * combination using JSPWiki's custom authentication mode. This method is identical to
346     * {@link #login(WikiSession, String, String)}, except that user's HTTP request is not made available
347     * to LoginModules via the {@link org.apache.wiki.auth.login.HttpRequestCallback}.
348     * @param session the current wiki session; may not be <code>null</code>.
349     * @param username The user name. This is a login name, not a WikiName. In
350     *            most cases they are the same, but in some cases, they might
351     *            not be.
352     * @param password the password
353     * @return true, if the username/password is valid
354     * @throws org.apache.wiki.auth.WikiSecurityException if the Authorizer or UserManager cannot be obtained
355     * @deprecated use {@link #login(WikiSession, HttpServletRequest, String, String)} instead
356     */
357    public boolean login( WikiSession session, String username, String password ) throws WikiSecurityException
358    {
359        return login( session, null, username, password );
360    }
361    
362    /**
363     * Attempts to perform a WikiSession login for the given username/password
364     * combination using JSPWiki's custom authentication mode. In order to log in,
365     * the JAAS LoginModule supplied by the WikiEngine property {@link #PROP_LOGIN_MODULE}
366     * will be instantiated, and its
367     * {@link javax.security.auth.spi.LoginModule#initialize(Subject, CallbackHandler, Map, Map)}
368     * method will be invoked. By default, the {@link org.apache.wiki.auth.login.UserDatabaseLoginModule}
369     * class will be used. When the LoginModule's <code>initialize</code> method is invoked,
370     * an options Map populated by properties keys prefixed by {@link #PREFIX_LOGIN_MODULE_OPTIONS}
371     * will be passed as a parameter.
372     * @param session the current wiki session; may not be <code>null</code>.
373     * @param request the user's HTTP request. This parameter may be <code>null</code>, but the configured
374     * LoginModule will not have access to the HTTP request in this case.
375     * @param username The user name. This is a login name, not a WikiName. In
376     *            most cases they are the same, but in some cases, they might
377     *            not be.
378     * @param password the password
379     * @return true, if the username/password is valid
380     * @throws org.apache.wiki.auth.WikiSecurityException if the Authorizer or UserManager cannot be obtained
381     */
382    public boolean login( WikiSession session, HttpServletRequest request, String username, String password ) throws WikiSecurityException
383    {
384        if ( session == null )
385        {
386            log.error( "No wiki session provided, cannot log in." );
387            return false;
388        }
389
390        // Protect against brute-force password guessing if configured to do so
391        if ( m_throttleLogins )
392        {
393            delayLogin(username);
394        }
395        
396        CallbackHandler handler = new WikiCallbackHandler(
397                m_engine,
398                null,
399                username,
400                password );
401        
402        // Execute the user's specified login module
403        Set<Principal> principals = doJAASLogin( m_loginModuleClass, handler, m_loginModuleOptions );
404        if (principals.size() > 0)
405        {
406            fireEvent(WikiSecurityEvent.LOGIN_AUTHENTICATED, getLoginPrincipal( principals ), session );
407            for ( Principal principal : principals )
408            {
409                fireEvent( WikiSecurityEvent.PRINCIPAL_ADD, principal, session );
410            }
411            
412            // Add all appropriate Authorizer roles
413            injectAuthorizerRoles( session, m_engine.getAuthorizationManager().getAuthorizer(), null );
414            
415            return true;
416        }
417        return false;
418    }
419    
420    /**
421     *  This method builds a database of login names that are being attempted, and will try to
422     *  delay if there are too many requests coming in for the same username.
423     *  <p>
424     *  The current algorithm uses 2^loginattempts as the delay in milliseconds, i.e.
425     *  at 10 login attempts it'll add 1.024 seconds to the login.
426     *  
427     *  @param username The username that is being logged in
428     */
429    private void delayLogin( String username )
430    {
431        try
432        {
433            m_lastLoginAttempts.cleanup( LASTLOGINS_CLEANUP_TIME );
434            int count = m_lastLoginAttempts.count( username );
435            
436            long delay = Math.min( 1<<count, MAX_LOGIN_DELAY );
437            log.debug( "Sleeping for "+delay+" ms to allow login." );
438            Thread.sleep( delay );
439            
440            m_lastLoginAttempts.add( username );
441        }
442        catch( InterruptedException e )
443        {
444            // FALLTHROUGH is fine
445        }
446    }
447
448    /**
449     * Logs the user out by retrieving the WikiSession associated with the
450     * HttpServletRequest and unbinding all of the Subject's Principals,
451     * except for {@link Role#ALL}, {@link Role#ANONYMOUS}.
452     * is a cheap-and-cheerful way to do it without invoking JAAS LoginModules.
453     * The logout operation will also flush the JSESSIONID cookie from
454     * the user's browser session, if it was set.
455     * @param request the current HTTP request
456     */
457    public void logout( HttpServletRequest request )
458    {
459        if( request == null )
460        {
461            log.error( "No HTTP reqest provided; cannot log out." );
462            return;
463        }
464
465        HttpSession session = request.getSession();
466        String sid = ( session == null ) ? "(null)" : session.getId();
467        if( log.isDebugEnabled() )
468        {
469            log.debug( "Invalidating WikiSession for session ID=" + sid );
470        }
471        // Retrieve the associated WikiSession and clear the Principal set
472        WikiSession wikiSession = WikiSession.getWikiSession( m_engine, request );
473        Principal originalPrincipal = wikiSession.getLoginPrincipal();
474        wikiSession.invalidate();
475
476        // Remove the wikiSession from the WikiSession cache
477        WikiSession.removeWikiSession( m_engine, request );
478
479        // We need to flush the HTTP session too
480        if ( session != null )
481        {
482            session.invalidate();
483        }
484
485        // Log the event
486        fireEvent( WikiSecurityEvent.LOGOUT, originalPrincipal, null );
487    }
488
489    /**
490     * Determines whether this WikiEngine allows users to assert identities using
491     * cookies instead of passwords. This is determined by inspecting
492     * the WikiEngine property {@link #PROP_ALLOW_COOKIE_ASSERTIONS}.
493     * @return <code>true</code> if cookies are allowed
494     */
495    public boolean allowsCookieAssertions()
496    {
497        return m_allowsCookieAssertions;
498    }
499
500    /**
501     *  Determines whether this WikiEngine allows users to authenticate using
502     *  cookies instead of passwords. This is determined by inspecting
503     * the WikiEngine property {@link #PROP_ALLOW_COOKIE_AUTH}.
504     *  @return <code>true</code> if cookies are allowed for authentication
505     *  @since 2.5.62
506     */
507    public boolean allowsCookieAuthentication()
508    {
509        return m_allowsCookieAuthentication;
510    }
511    
512    /**
513     * Determines whether the supplied Principal is a "role principal".
514     * @param principal the principal to test
515     * @return <code>true</code> if the Principal is of type
516     *         {@link GroupPrincipal} or
517     *         {@link org.apache.wiki.auth.authorize.Role},
518     *         <code>false</code> otherwise
519     */
520    public static boolean isRolePrincipal( Principal principal )
521    {
522        return principal instanceof Role || principal instanceof GroupPrincipal;
523    }
524
525    /**
526     * Determines whether the supplied Principal is a "user principal".
527     * @param principal the principal to test
528     * @return <code>false</code> if the Principal is of type
529     *         {@link GroupPrincipal} or
530     *         {@link org.apache.wiki.auth.authorize.Role},
531     *         <code>true</code> otherwise
532     */
533    public static boolean isUserPrincipal( Principal principal )
534    {
535        return !isRolePrincipal( principal );
536    }
537
538    /**
539     * Instantiates and executes a single JAAS
540     * {@link javax.security.auth.spi.LoginModule}, and returns a Set of
541     * Principals that results from a successful login. The LoginModule is instantiated,
542     * then its {@link javax.security.auth.spi.LoginModule#initialize(Subject, CallbackHandler, Map, Map)}
543     * method is called. The parameters passed to <code>initialize</code> is a 
544     * dummy Subject, an empty shared-state Map, and an options Map the caller supplies.
545     * 
546     * @param clazz
547     *            the LoginModule class to instantiate
548     * @param handler
549     *            the callback handler to supply to the LoginModule
550     * @param options
551     *            a Map of key/value strings for initializing the LoginModule
552     * @return the set of Principals returned by the JAAS method {@link Subject#getPrincipals()}
553     * @throws WikiSecurityException
554     *             if the LoginModule could not be instantiated for any reason
555     */
556    protected Set<Principal> doJAASLogin(Class<? extends LoginModule> clazz, CallbackHandler handler, Map<String,String> options) throws WikiSecurityException
557    {
558        // Instantiate the login module
559        LoginModule loginModule = null;
560        try
561        {
562            loginModule = clazz.newInstance();
563        }
564        catch (InstantiationException e)
565        {
566            throw new WikiSecurityException(e.getMessage(), e );
567        }
568        catch (IllegalAccessException e)
569        {
570            throw new WikiSecurityException(e.getMessage(), e );
571        }
572
573        // Initialize the LoginModule
574        Subject subject = new Subject();
575        loginModule.initialize( subject, handler, EMPTY_MAP, options );
576
577        // Try to log in:
578        boolean loginSucceeded = false;
579        boolean commitSucceeded = false;
580        try
581        {
582            loginSucceeded = loginModule.login();
583            if (loginSucceeded)
584            {
585                commitSucceeded = loginModule.commit();
586            }
587        }
588        catch (LoginException e)
589        {
590            // Login or commit failed! No principal for you!
591        }
592
593        // If we successfully logged in & committed, return all the principals
594        if (loginSucceeded && commitSucceeded)
595        {
596            return subject.getPrincipals();
597        }
598        return NO_PRINCIPALS;
599    }
600
601    /**
602     * Looks up and obtains a configuration file inside the WEB-INF folder of a
603     * wiki webapp.
604     * @param engine the wiki engine
605     * @param name the file to obtain, <em>e.g.</em>, <code>jspwiki.policy</code>
606     * @return the URL to the file
607     */
608    protected static URL findConfigFile( WikiEngine engine, String name )
609    {
610        log.info( "looking for " + name + " inside WEB-INF " );
611        // Try creating an absolute path first
612        File defaultFile = null;
613        if( engine.getRootPath() != null )
614        {
615            defaultFile = new File( engine.getRootPath() + "/WEB-INF/" + name );
616        }
617        if ( defaultFile != null && defaultFile.exists() )
618        {
619            try
620            {
621                return defaultFile.toURI().toURL();
622            }
623            catch ( MalformedURLException e)
624            {
625                // Shouldn't happen, but log it if it does
626                log.warn( "Malformed URL: " + e.getMessage() );
627            }
628
629        }
630
631        
632        // Ok, the absolute path didn't work; try other methods
633
634        URL path = null;
635        
636        if( engine.getServletContext() != null )
637        {
638            OutputStream os = null;
639            InputStream is = null;
640            try
641            {
642                URL url = engine.getServletContext().getResource("/WEB-INF/" + name);
643                if (url != null)
644                {
645                    return url;
646                }
647                
648                log.info( "looking for /" + name + " on classpath" );
649                //  create a tmp file of the policy loaded as an InputStream and return the URL to it
650                //  
651                is = AuthenticationManager.class.getResourceAsStream( "/" + name );
652                if( is == null ) {
653                    throw new FileNotFoundException( name + " not found" );
654                }
655                File tmpFile = File.createTempFile( "temp." + name, "" );
656                tmpFile.deleteOnExit();
657
658                os = new FileOutputStream(tmpFile);
659
660                byte[] buff = new byte[1024];
661                int bytes = 0;
662                while ((bytes = is.read(buff)) != -1) {
663                    os.write(buff, 0, bytes);
664                }
665
666                path = tmpFile.toURI().toURL();
667            }
668            catch( MalformedURLException e )
669            {
670                // This should never happen unless I screw up
671                log.fatal( "Your code is b0rked.  You are a bad person.", e );
672            }
673            catch (IOException e)
674            {
675               log.error( "failed to load security policy from file " + name + ",stacktrace follows", e );
676            }
677            finally 
678            {
679                IOUtils.closeQuietly( is );
680                IOUtils.closeQuietly( os );
681            }
682        }
683        return path;
684    }
685
686    /**
687     * Returns the first Principal in a set that isn't a {@link org.apache.wiki.auth.authorize.Role} or
688     * {@link org.apache.wiki.auth.GroupPrincipal}.
689     * @param principals the principal set
690     * @return the login principal
691     */
692    protected Principal getLoginPrincipal(Set<Principal> principals)
693    {
694        for (Principal principal: principals )
695        {
696            if ( isUserPrincipal( principal ) )
697            {
698                return principal;
699            }
700        }
701        return null;
702    }
703
704    // events processing .......................................................
705
706    /**
707     * Registers a WikiEventListener with this instance.
708     * This is a convenience method.
709     * @param listener the event listener
710     */
711    public synchronized void addWikiEventListener( WikiEventListener listener )
712    {
713        WikiEventManager.addWikiEventListener( this, listener );
714    }
715
716    /**
717     * Un-registers a WikiEventListener with this instance.
718     * This is a convenience method.
719     * @param listener the event listener
720     */
721    public synchronized void removeWikiEventListener( WikiEventListener listener )
722    {
723        WikiEventManager.removeWikiEventListener( this, listener );
724    }
725
726    /**
727     *  Fires a WikiSecurityEvent of the provided type, Principal and target Object
728     *  to all registered listeners.
729     *
730     * @see org.apache.wiki.event.WikiSecurityEvent
731     * @param type       the event type to be fired
732     * @param principal  the subject of the event, which may be <code>null</code>
733     * @param target     the changed Object, which may be <code>null</code>
734     */
735    protected void fireEvent( int type, Principal principal, Object target )
736    {
737        if ( WikiEventManager.isListening(this) )
738        {
739            WikiEventManager.fireEvent(this,new WikiSecurityEvent(this,type,principal,target));
740        }
741    }
742    
743    /**
744     * Initializes the options Map supplied to the configured LoginModule every time it is invoked.
745     * The properties and values extracted from
746     * <code>jspwiki.properties</code> are of the form
747     * <code>jspwiki.loginModule.options.<var>param</var> = <var>value</var>, where
748     * <var>param</var> is the key name, and <var>value</var> is the value.
749     * @param props the properties used to initialize JSPWiki
750     * @throws IllegalArgumentException if any of the keys are duplicated
751     */
752    private void initLoginModuleOptions(Properties props)
753    {
754        for ( Object key : props.keySet() )
755        {
756            String propName = key.toString();
757            if ( propName.startsWith( PREFIX_LOGIN_MODULE_OPTIONS ) )
758            {
759                // Extract the option name and value
760                String optionKey = propName.substring( PREFIX_LOGIN_MODULE_OPTIONS.length() ).trim();
761                if ( optionKey.length() > 0 )
762                {
763                    String optionValue = props.getProperty( propName );
764                    
765                    // Make sure the key is unique before stashing the key/value pair
766                    if ( m_loginModuleOptions.containsKey( optionKey ) )
767                    {
768                        throw new IllegalArgumentException( "JAAS LoginModule key " + propName + " cannot be specified twice!" );
769                    }
770                    m_loginModuleOptions.put( optionKey, optionValue );
771                }
772            }
773        }
774    }
775    
776    /**
777     * After successful login, this method is called to inject authorized role Principals into the WikiSession.
778     * To determine which roles should be injected, the configured Authorizer
779     * is queried for the roles it knows about by calling  {@link org.apache.wiki.auth.Authorizer#getRoles()}.
780     * Then, each role returned by the authorizer is tested by calling {@link org.apache.wiki.auth.Authorizer#isUserInRole(WikiSession, Principal)}.
781     * If this check fails, and the Authorizer is of type WebAuthorizer, the role is checked again by calling
782     * {@link org.apache.wiki.auth.authorize.WebAuthorizer#isUserInRole(javax.servlet.http.HttpServletRequest, Principal)}).
783     * Any roles that pass the test are injected into the Subject by firing appropriate authentication events.
784     * @param session the user's current WikiSession
785     * @param authorizer the WikiEngine's configured Authorizer
786     * @param request the user's HTTP session, which may be <code>null</code>
787     */
788    private void injectAuthorizerRoles( WikiSession session, Authorizer authorizer, HttpServletRequest request )
789    {
790        // Test each role the authorizer knows about
791        for ( Principal role : authorizer.getRoles() )
792        {
793            // Test the Authorizer
794            if ( authorizer.isUserInRole( session, role ) )
795            {
796                fireEvent( WikiSecurityEvent.PRINCIPAL_ADD, role, session );
797                if ( log.isDebugEnabled() )
798                {
799                    log.debug("Added authorizer role " + role.getName() + "." );
800                }
801            }
802            
803            // If web authorizer, test the request.isInRole() method also
804            else if ( request != null && authorizer instanceof WebAuthorizer )
805            {
806                WebAuthorizer wa = (WebAuthorizer)authorizer;
807                if ( wa.isUserInRole( request, role ) )
808                {
809                    fireEvent( WikiSecurityEvent.PRINCIPAL_ADD, role, session );
810                    if ( log.isDebugEnabled() )
811                    {
812                        log.debug("Added container role " + role.getName() + "." );
813                    }
814                }
815            }
816        }
817    }
818
819}