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     */
019    package org.apache.wiki.auth;
020    
021    import java.io.File;
022    import java.io.FileNotFoundException;
023    import java.io.FileOutputStream;
024    import java.io.IOException;
025    import java.io.InputStream;
026    import java.io.OutputStream;
027    import java.net.MalformedURLException;
028    import java.net.URL;
029    import java.security.Principal;
030    import java.util.Collections;
031    import java.util.HashMap;
032    import java.util.HashSet;
033    import java.util.Map;
034    import java.util.Properties;
035    import java.util.Set;
036    
037    import javax.security.auth.Subject;
038    import javax.security.auth.callback.CallbackHandler;
039    import javax.security.auth.login.LoginException;
040    import javax.security.auth.spi.LoginModule;
041    import javax.servlet.http.HttpServletRequest;
042    import javax.servlet.http.HttpSession;
043    
044    import org.apache.commons.io.IOUtils;
045    import org.apache.log4j.Logger;
046    import org.apache.wiki.WikiEngine;
047    import org.apache.wiki.WikiSession;
048    import org.apache.wiki.api.exceptions.WikiException;
049    import org.apache.wiki.auth.authorize.Role;
050    import org.apache.wiki.auth.authorize.WebAuthorizer;
051    import org.apache.wiki.auth.authorize.WebContainerAuthorizer;
052    import org.apache.wiki.auth.login.AnonymousLoginModule;
053    import org.apache.wiki.auth.login.CookieAssertionLoginModule;
054    import org.apache.wiki.auth.login.CookieAuthenticationLoginModule;
055    import org.apache.wiki.auth.login.UserDatabaseLoginModule;
056    import org.apache.wiki.auth.login.WebContainerCallbackHandler;
057    import org.apache.wiki.auth.login.WebContainerLoginModule;
058    import org.apache.wiki.auth.login.WikiCallbackHandler;
059    import org.apache.wiki.event.WikiEventListener;
060    import org.apache.wiki.event.WikiEventManager;
061    import org.apache.wiki.event.WikiSecurityEvent;
062    import org.apache.wiki.util.TextUtil;
063    import 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     */
078    public 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
104         */
105        public  static final String                PROP_SECURITY       = "jspwiki.security";
106    
107        /** Value specifying that the user wants to use the container-managed security, just like in JSPWiki 2.2. */
108        public static final String                SECURITY_OFF      = "off";
109    
110        /** Value specifying that the user wants to use the built-in JAAS-based system */
111        public static final String                SECURITY_JAAS     = "jaas";
112    
113        /** Whether logins should be throttled to limit brute-forcing attempts. Defaults to true. */
114        public static final String                 PROP_LOGIN_THROTTLING = "jspwiki.login.throttling";
115    
116        protected static final Logger              log                 = Logger.getLogger( AuthenticationManager.class );
117    
118        /** Prefix for LoginModule options key/value pairs. */
119        protected static final String                 PREFIX_LOGIN_MODULE_OPTIONS = "jspwiki.loginModule.options.";
120    
121        /** If this jspwiki.properties property is <code>true</code>, allow cookies to be used to assert identities. */
122        protected static final String                 PROP_ALLOW_COOKIE_ASSERTIONS = "jspwiki.cookieAssertions";
123    
124        /** The {@link javax.security.auth.spi.LoginModule} to use for custom authentication. */
125        protected static final String                 PROP_LOGIN_MODULE = "jspwiki.loginModule.class";
126        
127        /** Empty Map passed to JAAS {@link #doJAASLogin(Class, CallbackHandler, Map)} method. */
128        protected static final Map<String,String> EMPTY_MAP = Collections.unmodifiableMap( new HashMap<String,String>() );
129        
130        /** Class (of type LoginModule) to use for custom authentication. */
131        protected Class<? extends LoginModule> m_loginModuleClass = UserDatabaseLoginModule.class;
132        
133        /** Options passed to {@link javax.security.auth.spi.LoginModule#initialize(Subject, CallbackHandler, Map, Map)}; 
134         * initialized by {@link #initialize(WikiEngine, Properties)}. */
135        protected Map<String,String> m_loginModuleOptions = new HashMap<String,String>();
136    
137        /** Just to provide compatibility with the old versions.  The same
138         *  as SECURITY_OFF.
139         *
140         *  @deprecated use {@link #SECURITY_OFF} instead
141         */
142        protected static final String             SECURITY_CONTAINER = "container";
143    
144        /** The default {@link javax.security.auth.spi.LoginModule} class name to use for custom authentication. */
145        private static final String                 DEFAULT_LOGIN_MODULE = "org.apache.wiki.auth.login.UserDatabaseLoginModule";
146        
147        /** Empty principal set. */
148        private static final Set<Principal> NO_PRINCIPALS = new HashSet<Principal>();
149    
150        /** Static Boolean for lazily-initializing the "allows assertions" flag */
151        private boolean                     m_allowsCookieAssertions  = true;
152    
153        private boolean                     m_throttleLogins = true;
154    
155        /** Static Boolean for lazily-initializing the "allows cookie authentication" flag */
156        private boolean                     m_allowsCookieAuthentication = false;
157    
158        private WikiEngine                         m_engine            = null;
159        
160        /** If true, logs the IP address of the editor */
161        private boolean                            m_storeIPAddress    = true;
162    
163        private boolean               m_useJAAS = true;
164    
165        /** Keeps a list of the usernames who have attempted a login recently. */
166        
167        private TimedCounterList<String> m_lastLoginAttempts = new TimedCounterList<String>();
168        
169        /**
170         * Creates an AuthenticationManager instance for the given WikiEngine and
171         * the specified set of properties. All initialization for the modules is
172         * done here.
173         * @param engine the wiki engine
174         * @param props the properties used to initialize the wiki engine
175         * @throws WikiException if the AuthenticationManager cannot be initialized
176         */
177        @SuppressWarnings("unchecked")
178        public void initialize( WikiEngine engine, Properties props ) throws WikiException
179        {
180            m_engine = engine;
181            m_storeIPAddress = TextUtil.getBooleanProperty( props, PROP_STOREIPADDRESS, m_storeIPAddress );
182    
183            // Should J2SE policies be used for authorization?
184            m_useJAAS = SECURITY_JAAS.equals(props.getProperty( PROP_SECURITY, SECURITY_JAAS ));
185            
186            // Should we allow cookies for assertions? (default: yes)
187            m_allowsCookieAssertions = TextUtil.getBooleanProperty( props,
188                                                                  PROP_ALLOW_COOKIE_ASSERTIONS,
189                                                                  true );
190            
191            // Should we allow cookies for authentication? (default: no)
192            m_allowsCookieAuthentication = TextUtil.getBooleanProperty( props,
193                                                                        PROP_ALLOW_COOKIE_AUTH,
194                                                                        false );
195            
196            // Should we throttle logins? (default: yes)
197            m_throttleLogins = TextUtil.getBooleanProperty( props,
198                                                            PROP_LOGIN_THROTTLING,
199                                                            true );
200    
201            // Look up the LoginModule class
202            String loginModuleClassName = TextUtil.getStringProperty( props, PROP_LOGIN_MODULE, DEFAULT_LOGIN_MODULE );
203            try
204            {
205                m_loginModuleClass = (Class<? extends LoginModule>) Class.forName( loginModuleClassName );
206            }
207            catch (ClassNotFoundException e)
208            {
209                e.printStackTrace();
210                throw new WikiException( "Could not instantiate LoginModule class.", e );
211            }
212            
213            // Initialize the LoginModule options
214            initLoginModuleOptions( props );
215        }
216    
217        /**
218         * Returns true if this WikiEngine uses container-managed authentication.
219         * This method is used primarily for cosmetic purposes in the JSP tier, and
220         * performs no meaningful security function per se. Delegates to
221         * {@link org.apache.wiki.auth.authorize.WebContainerAuthorizer#isContainerAuthorized()},
222         * if used as the external authorizer; otherwise, returns <code>false</code>.
223         * @return <code>true</code> if the wiki's authentication is managed by
224         *         the container, <code>false</code> otherwise
225         */
226        public boolean isContainerAuthenticated()
227        {
228            if( !m_useJAAS ) return true;
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            // Ok, the absolute path didn't work; try other methods
632    
633            URL path = null;
634            
635            if( engine.getServletContext() != null )
636            {
637                OutputStream os = null;
638                InputStream is = null;
639                try
640                {
641                    log.info( "looking for /" + name + " on classpath" );
642                    //  create a tmp file of the policy loaded as an InputStream and return the URL to it
643                    //  
644                    is = AuthenticationManager.class.getResourceAsStream( "/" + name );
645                    if( is == null ) {
646                        throw new FileNotFoundException( name + " not found" );
647                    }
648                    File tmpFile = File.createTempFile( "temp." + name, "" );
649                    tmpFile.deleteOnExit();
650    
651                    os = new FileOutputStream(tmpFile);
652    
653                    byte[] buff = new byte[1024];
654                    int bytes = 0;
655                    while ((bytes = is.read(buff)) != -1) {
656                        os.write(buff, 0, bytes);
657                    }
658    
659                    path = tmpFile.toURI().toURL();
660                }
661                catch( MalformedURLException e )
662                {
663                    // This should never happen unless I screw up
664                    log.fatal( "Your code is b0rked.  You are a bad person.", e );
665                }
666                catch (IOException e)
667                {
668                   log.error( "failed to load security policy from file " + name + ",stacktrace follows", e );
669                }
670                finally 
671                {
672                    IOUtils.closeQuietly( is );
673                    IOUtils.closeQuietly( os );
674                }
675            }
676            return path;
677        }
678    
679        /**
680         * Returns the first Principal in a set that isn't a {@link org.apache.wiki.auth.authorize.Role} or
681         * {@link org.apache.wiki.auth.GroupPrincipal}.
682         * @param principals the principal set
683         * @return the login principal
684         */
685        protected Principal getLoginPrincipal(Set<Principal> principals)
686        {
687            for (Principal principal: principals )
688            {
689                if ( isUserPrincipal( principal ) )
690                {
691                    return principal;
692                }
693            }
694            return null;
695        }
696    
697        // events processing .......................................................
698    
699        /**
700         * Registers a WikiEventListener with this instance.
701         * This is a convenience method.
702         * @param listener the event listener
703         */
704        public synchronized void addWikiEventListener( WikiEventListener listener )
705        {
706            WikiEventManager.addWikiEventListener( this, listener );
707        }
708    
709        /**
710         * Un-registers a WikiEventListener with this instance.
711         * This is a convenience method.
712         * @param listener the event listener
713         */
714        public synchronized void removeWikiEventListener( WikiEventListener listener )
715        {
716            WikiEventManager.removeWikiEventListener( this, listener );
717        }
718    
719        /**
720         *  Fires a WikiSecurityEvent of the provided type, Principal and target Object
721         *  to all registered listeners.
722         *
723         * @see org.apache.wiki.event.WikiSecurityEvent
724         * @param type       the event type to be fired
725         * @param principal  the subject of the event, which may be <code>null</code>
726         * @param target     the changed Object, which may be <code>null</code>
727         */
728        protected void fireEvent( int type, Principal principal, Object target )
729        {
730            if ( WikiEventManager.isListening(this) )
731            {
732                WikiEventManager.fireEvent(this,new WikiSecurityEvent(this,type,principal,target));
733            }
734        }
735        
736        /**
737         * Initializes the options Map supplied to the configured LoginModule every time it is invoked.
738         * The properties and values extracted from
739         * <code>jspwiki.properties</code> are of the form
740         * <code>jspwiki.loginModule.options.<var>param</var> = <var>value</var>, where
741         * <var>param</var> is the key name, and <var>value</var> is the value.
742         * @param props the properties used to initialize JSPWiki
743         * @throws IllegalArgumentException if any of the keys are duplicated
744         */
745        private void initLoginModuleOptions(Properties props)
746        {
747            for ( Object key : props.keySet() )
748            {
749                String propName = key.toString();
750                if ( propName.startsWith( PREFIX_LOGIN_MODULE_OPTIONS ) )
751                {
752                    // Extract the option name and value
753                    String optionKey = propName.substring( PREFIX_LOGIN_MODULE_OPTIONS.length() ).trim();
754                    if ( optionKey.length() > 0 )
755                    {
756                        String optionValue = props.getProperty( propName );
757                        
758                        // Make sure the key is unique before stashing the key/value pair
759                        if ( m_loginModuleOptions.containsKey( optionKey ) )
760                        {
761                            throw new IllegalArgumentException( "JAAS LoginModule key " + propName + " cannot be specified twice!" );
762                        }
763                        m_loginModuleOptions.put( optionKey, optionValue );
764                    }
765                }
766            }
767        }
768        
769        /**
770         * After successful login, this method is called to inject authorized role Principals into the WikiSession.
771         * To determine which roles should be injected, the configured Authorizer
772         * is queried for the roles it knows about by calling  {@link org.apache.wiki.auth.Authorizer#getRoles()}.
773         * Then, each role returned by the authorizer is tested by calling {@link org.apache.wiki.auth.Authorizer#isUserInRole(WikiSession, Principal)}.
774         * If this check fails, and the Authorizer is of type WebAuthorizer, the role is checked again by calling
775         * {@link org.apache.wiki.auth.authorize.WebAuthorizer#isUserInRole(javax.servlet.http.HttpServletRequest, Principal)}).
776         * Any roles that pass the test are injected into the Subject by firing appropriate authentication events.
777         * @param session the user's current WikiSession
778         * @param authorizer the WikiEngine's configured Authorizer
779         * @param request the user's HTTP session, which may be <code>null</code>
780         */
781        private void injectAuthorizerRoles( WikiSession session, Authorizer authorizer, HttpServletRequest request )
782        {
783            // Test each role the authorizer knows about
784            for ( Principal role : authorizer.getRoles() )
785            {
786                // Test the Authorizer
787                if ( authorizer.isUserInRole( session, role ) )
788                {
789                    fireEvent( WikiSecurityEvent.PRINCIPAL_ADD, role, session );
790                    if ( log.isDebugEnabled() )
791                    {
792                        log.debug("Added authorizer role " + role.getName() + "." );
793                    }
794                }
795                
796                // If web authorizer, test the request.isInRole() method also
797                else if ( request != null && authorizer instanceof WebAuthorizer )
798                {
799                    WebAuthorizer wa = (WebAuthorizer)authorizer;
800                    if ( wa.isUserInRole( request, role ) )
801                    {
802                        fireEvent( WikiSecurityEvent.PRINCIPAL_ADD, role, session );
803                        if ( log.isDebugEnabled() )
804                        {
805                            log.debug("Added container role " + role.getName() + "." );
806                        }
807                    }
808                }
809            }
810        }
811    
812    }