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 }