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