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