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.user; 020 021import java.io.IOException; 022import java.io.Serializable; 023import java.security.Principal; 024import java.sql.Connection; 025import java.sql.DatabaseMetaData; 026import java.sql.PreparedStatement; 027import java.sql.ResultSet; 028import java.sql.SQLException; 029import java.sql.Timestamp; 030import java.util.Date; 031import java.util.HashSet; 032import java.util.Map; 033import java.util.Properties; 034import java.util.Set; 035 036import javax.naming.Context; 037import javax.naming.InitialContext; 038import javax.naming.NamingException; 039import javax.sql.DataSource; 040 041import org.apache.commons.lang.StringUtils; 042import org.apache.wiki.WikiEngine; 043import org.apache.wiki.api.exceptions.NoRequiredPropertyException; 044import org.apache.wiki.auth.NoSuchPrincipalException; 045import org.apache.wiki.auth.WikiPrincipal; 046import org.apache.wiki.auth.WikiSecurityException; 047import org.apache.wiki.util.Serializer; 048 049/** 050 * <p> 051 * Implementation of UserDatabase that persists {@link DefaultUserProfile} 052 * objects to a JDBC DataSource, as might typically be provided by a web 053 * container. This implementation looks up the JDBC DataSource using JNDI. The 054 * JNDI name of the datasource, backing table and mapped columns used by this 055 * class can be overridden by adding settings in <code>jspwiki.properties</code>. 056 * </p> 057 * <p> 058 * Configurable properties are these: 059 * </p> 060 * <table> 061 * <tr> <thead> 062 * <th>Property</th> 063 * <th>Default</th> 064 * <th>Definition</th> 065 * <thead> </tr> 066 * <tr> 067 * <td><code>jspwiki.userdatabase.datasource</code></td> 068 * <td><code>jdbc/UserDatabase</code></td> 069 * <td>The JNDI name of the DataSource</td> 070 * </tr> 071 * <tr> 072 * <td><code>jspwiki.userdatabase.table</code></td> 073 * <td><code>users</code></td> 074 * <td>The table that stores the user profiles</td> 075 * </tr> 076 * <tr> 077 * <td><code>jspwiki.userdatabase.attributes</code></td> 078 * <td><code>attributes</code></td> 079 * <td>The CLOB column containing the profile's custom attributes, stored as key/value strings, each separated by newline.</td> 080 * </tr> 081 * <tr> 082 * <td><code>jspwiki.userdatabase.created</code></td> 083 * <td><code>created</code></td> 084 * <td>The column containing the profile's creation timestamp</td> 085 * </tr> 086 * <tr> 087 * <td><code>jspwiki.userdatabase.email</code></td> 088 * <td><code>email</code></td> 089 * <td>The column containing the user's e-mail address</td> 090 * </tr> 091 * <tr> 092 * <td><code>jspwiki.userdatabase.fullName</code></td> 093 * <td><code>full_name</code></td> 094 * <td>The column containing the user's full name</td> 095 * </tr> 096 * <tr> 097 * <td><code>jspwiki.userdatabase.loginName</code></td> 098 * <td><code>login_name</code></td> 099 * <td>The column containing the user's login id</td> 100 * </tr> 101 * <tr> 102 * <td><code>jspwiki.userdatabase.password</code></td> 103 * <td><code>password</code></td> 104 * <td>The column containing the user's password</td> 105 * </tr> 106 * <tr> 107 * <td><code>jspwiki.userdatabase.modified</code></td> 108 * <td><code>modified</code></td> 109 * <td>The column containing the profile's last-modified timestamp</td> 110 * </tr> 111 * <tr> 112 * <td><code>jspwiki.userdatabase.uid</code></td> 113 * <td><code>uid</code></td> 114 * <td>The column containing the profile's unique identifier, as a long integer</td> 115 * </tr> 116 * <tr> 117 * <td><code>jspwiki.userdatabase.wikiName</code></td> 118 * <td><code>wiki_name</code></td> 119 * <td>The column containing the user's wiki name</td> 120 * </tr> 121 * <tr> 122 * <td><code>jspwiki.userdatabase.lockExpiry</code></td> 123 * <td><code>lock_expiry</code></td> 124 * <td>The column containing the date/time when the profile, if locked, should be unlocked.</td> 125 * </tr> 126 * <tr> 127 * <td><code>jspwiki.userdatabase.roleTable</code></td> 128 * <td><code>roles</code></td> 129 * <td>The table that stores user roles. When a new user is created, a new 130 * record is inserted containing user's initial role. The table will have an ID 131 * column whose name and values correspond to the contents of the user table's 132 * login name column. It will also contain a role column (see next row).</td> 133 * </tr> 134 * <tr> 135 * <td><code>jspwiki.userdatabase.role</code></td> 136 * <td><code>role</code></td> 137 * <td>The column in the role table that stores user roles. When a new user is 138 * created, this column will be populated with the value 139 * <code>Authenticated</code>. Once created, JDBCUserDatabase does not use 140 * this column again; it is provided strictly for the convenience of 141 * container-managed authentication services.</td> 142 * </tr> 143 * </table> 144 * <p> 145 * This class hashes passwords using SHA-1. All of the underying SQL commands 146 * used by this class are implemented using prepared statements, so it is immune 147 * to SQL injection attacks. 148 * </p> 149 * <p> 150 * This class is typically used in conjunction with a web container's JNDI 151 * resource factory. For example, Tomcat provides a basic 152 * JNDI factory for registering DataSources. To give JSPWiki access to the JNDI 153 * resource named by <code></code>, you would declare the datasource resource 154 * similar to this: 155 * </p> 156 * <blockquote><code><Context ...><br/> 157 * ...<br/> 158 * <Resource name="jdbc/UserDatabase" auth="Container"<br/> 159 * type="javax.sql.DataSource" username="dbusername" password="dbpassword"<br/> 160 * driverClassName="org.hsql.jdbcDriver" url="jdbc:HypersonicSQL:database"<br/> 161 * maxActive="8" maxIdle="4"/><br/> 162 * ...<br/> 163 * </Context></code></blockquote> 164 * <p> 165 * To configure JSPWiki to use JDBC support, first create a database 166 * with a structure similar to that provided by the HSQL and PostgreSQL 167 * scripts in src/main/config/db. If you have different table or column 168 * names you can either alias them with a database view and have JSPWiki 169 * use the views, or alter the WEB-INF/jspwiki.properties file: the 170 * jspwiki.userdatabase.* and jspwiki.groupdatabase.* properties change the 171 * names of the tables and columns that JSPWiki uses. 172 * </p> 173 * <p> 174 * A JNDI datasource (named jdbc/UserDatabase by default but can be configured 175 * in the jspwiki.properties file) will need to be created in your servlet container. 176 * JDBC driver JARs should be added, e.g. in Tomcat's <code>lib</code> 177 * directory. For more Tomcat JNDI configuration examples, see <a 178 * href="http://tomcat.apache.org/tomcat-7.0-doc/jndi-resources-howto.html"> 179 * http://tomcat.apache.org/tomcat-7.0-doc/jndi-resources-howto.html</a>. 180 * Once done, restart JSPWiki in the servlet container for it to read the 181 * new properties and switch to JDBC authentication. 182 * </p> 183 * <p> 184 * JDBCUserDatabase commits changes as transactions if the back-end database 185 * supports them. If the database supports transactions, user profile changes 186 * are saved to permanent storage only when the {@link #commit()} method is 187 * called. If the database does <em>not</em> support transactions, then 188 * changes are made immediately (during the {@link #save(UserProfile)} method), 189 * and the {@linkplain #commit()} method no-ops. Thus, callers should always 190 * call the {@linkplain #commit()} method after saving a profile to guarantee 191 * that changes are applied. 192 * </p> 193 * 194 * @since 2.3 195 */ 196public class JDBCUserDatabase extends AbstractUserDatabase 197{ 198 199 private static final String NOTHING = ""; 200 201 public static final String DEFAULT_DB_ATTRIBUTES = "attributes"; 202 203 public static final String DEFAULT_DB_CREATED = "created"; 204 205 public static final String DEFAULT_DB_EMAIL = "email"; 206 207 public static final String DEFAULT_DB_FULL_NAME = "full_name"; 208 209 public static final String DEFAULT_DB_JNDI_NAME = "jdbc/UserDatabase"; 210 211 public static final String DEFAULT_DB_LOCK_EXPIRY = "lock_expiry"; 212 213 public static final String DEFAULT_DB_MODIFIED = "modified"; 214 215 public static final String DEFAULT_DB_ROLE = "role"; 216 217 public static final String DEFAULT_DB_ROLE_TABLE = "roles"; 218 219 public static final String DEFAULT_DB_TABLE = "users"; 220 221 public static final String DEFAULT_DB_LOGIN_NAME = "login_name"; 222 223 public static final String DEFAULT_DB_PASSWORD = "password"; 224 225 public static final String DEFAULT_DB_UID = "uid"; 226 227 public static final String DEFAULT_DB_WIKI_NAME = "wiki_name"; 228 229 public static final String PROP_DB_ATTRIBUTES = "jspwiki.userdatabase.attributes"; 230 231 public static final String PROP_DB_CREATED = "jspwiki.userdatabase.created"; 232 233 public static final String PROP_DB_EMAIL = "jspwiki.userdatabase.email"; 234 235 public static final String PROP_DB_FULL_NAME = "jspwiki.userdatabase.fullName"; 236 237 public static final String PROP_DB_DATASOURCE = "jspwiki.userdatabase.datasource"; 238 239 public static final String PROP_DB_LOCK_EXPIRY = "jspwiki.userdatabase.lockExpiry"; 240 241 public static final String PROP_DB_LOGIN_NAME = "jspwiki.userdatabase.loginName"; 242 243 public static final String PROP_DB_MODIFIED = "jspwiki.userdatabase.modified"; 244 245 public static final String PROP_DB_PASSWORD = "jspwiki.userdatabase.password"; 246 247 public static final String PROP_DB_UID = "jspwiki.userdatabase.uid"; 248 249 public static final String PROP_DB_ROLE = "jspwiki.userdatabase.role"; 250 251 public static final String PROP_DB_ROLE_TABLE = "jspwiki.userdatabase.roleTable"; 252 253 public static final String PROP_DB_TABLE = "jspwiki.userdatabase.table"; 254 255 public static final String PROP_DB_WIKI_NAME = "jspwiki.userdatabase.wikiName"; 256 257 private DataSource m_ds = null; 258 259 private String m_deleteUserByLoginName = null; 260 261 private String m_deleteRoleByLoginName = null; 262 263 private String m_findByEmail = null; 264 265 private String m_findByFullName = null; 266 267 private String m_findByLoginName = null; 268 269 private String m_findByUid = null; 270 271 private String m_findByWikiName = null; 272 273 private String m_renameProfile = null; 274 275 private String m_renameRoles = null; 276 277 private String m_updateProfile = null; 278 279 private String m_findAll = null; 280 281 private String m_findRoles = null; 282 283 private String m_insertProfile = null; 284 285 private String m_insertRole = null; 286 287 private String m_attributes = null; 288 289 private String m_email = null; 290 291 private String m_fullName = null; 292 293 private String m_lockExpiry = null; 294 295 private String m_loginName = null; 296 297 private String m_password = null; 298 299 private String m_uid = null; 300 301 private String m_wikiName = null; 302 303 private String m_created = null; 304 305 private String m_modified = null; 306 307 private boolean m_supportsCommits = false; 308 309 /** 310 * Looks up and deletes the first {@link UserProfile} in the user database 311 * that matches a profile having a given login name. If the user database 312 * does not contain a user with a matching attribute, throws a 313 * {@link NoSuchPrincipalException}. This method is intended to be atomic; 314 * results cannot be partially committed. If the commit fails, it should 315 * roll back its state appropriately. Implementing classes that persist to 316 * the file system may wish to make this method <code>synchronized</code>. 317 * 318 * @param loginName the login name of the user profile that shall be deleted 319 */ 320 public void deleteByLoginName( String loginName ) throws NoSuchPrincipalException, WikiSecurityException 321 { 322 // Get the existing user; if not found, throws NoSuchPrincipalException 323 findByLoginName( loginName ); 324 Connection conn = null; 325 326 try 327 { 328 // Open the database connection 329 conn = m_ds.getConnection(); 330 if( m_supportsCommits ) 331 { 332 conn.setAutoCommit( false ); 333 } 334 335 PreparedStatement ps; 336 // Delete user record 337 ps = conn.prepareStatement( m_deleteUserByLoginName ); 338 ps.setString( 1, loginName ); 339 ps.execute(); 340 ps.close(); 341 342 // Delete role record 343 ps = conn.prepareStatement( m_deleteRoleByLoginName ); 344 ps.setString( 1, loginName ); 345 ps.execute(); 346 ps.close(); 347 348 // Commit and close connection 349 if( m_supportsCommits ) 350 { 351 conn.commit(); 352 } 353 } 354 catch( SQLException e ) 355 { 356 throw new WikiSecurityException( e.getMessage(), e ); 357 } 358 finally 359 { 360 try 361 { 362 if( conn != null ) conn.close(); 363 } 364 catch( Exception e ) 365 { 366 } 367 } 368 } 369 370 /** 371 * @see org.apache.wiki.auth.user.UserDatabase#findByEmail(java.lang.String) 372 */ 373 public UserProfile findByEmail( String index ) throws NoSuchPrincipalException 374 { 375 return findByPreparedStatement( m_findByEmail, index ); 376 } 377 378 /** 379 * @see org.apache.wiki.auth.user.UserDatabase#findByFullName(java.lang.String) 380 */ 381 public UserProfile findByFullName( String index ) throws NoSuchPrincipalException 382 { 383 return findByPreparedStatement( m_findByFullName, index ); 384 } 385 386 /** 387 * @see org.apache.wiki.auth.user.UserDatabase#findByLoginName(java.lang.String) 388 */ 389 public UserProfile findByLoginName( String index ) throws NoSuchPrincipalException 390 { 391 return findByPreparedStatement( m_findByLoginName, index ); 392 } 393 394 /** 395 * @see org.apache.wiki.auth.user.UserDatabase#findByWikiName(String) 396 */ 397 public UserProfile findByUid( String uid ) throws NoSuchPrincipalException 398 { 399 return findByPreparedStatement( m_findByUid, uid ); 400 } 401 402 /** 403 * @see org.apache.wiki.auth.user.UserDatabase#findByWikiName(String) 404 */ 405 public UserProfile findByWikiName( String index ) throws NoSuchPrincipalException 406 { 407 return findByPreparedStatement( m_findByWikiName, index ); 408 } 409 410 /** 411 * Returns all WikiNames that are stored in the UserDatabase as an array of 412 * WikiPrincipal objects. If the database does not contain any profiles, 413 * this method will return a zero-length array. 414 * 415 * @return the WikiNames 416 */ 417 public Principal[] getWikiNames() throws WikiSecurityException 418 { 419 Set<Principal> principals = new HashSet<Principal>(); 420 Connection conn = null; 421 try 422 { 423 conn = m_ds.getConnection(); 424 PreparedStatement ps = conn.prepareStatement( m_findAll ); 425 ResultSet rs = ps.executeQuery(); 426 while ( rs.next() ) 427 { 428 String wikiName = rs.getString( m_wikiName ); 429 if( wikiName == null ) 430 { 431 log.warn( "Detected null wiki name in XMLUserDataBase. Check your user database." ); 432 } 433 else 434 { 435 Principal principal = new WikiPrincipal( wikiName, WikiPrincipal.WIKI_NAME ); 436 principals.add( principal ); 437 } 438 } 439 ps.close(); 440 } 441 catch( SQLException e ) 442 { 443 throw new WikiSecurityException( e.getMessage(), e ); 444 } 445 finally 446 { 447 try 448 { 449 if( conn != null ) conn.close(); 450 } 451 catch( Exception e ) 452 { 453 } 454 } 455 456 return principals.toArray( new Principal[principals.size()] ); 457 } 458 459 /** 460 * @see org.apache.wiki.auth.user.UserDatabase#initialize(org.apache.wiki.WikiEngine, 461 * java.util.Properties) 462 */ 463 public void initialize( WikiEngine engine, Properties props ) throws NoRequiredPropertyException, WikiSecurityException 464 { 465 String userTable; 466 String role; 467 String roleTable; 468 469 String jndiName = props.getProperty( PROP_DB_DATASOURCE, DEFAULT_DB_JNDI_NAME ); 470 try 471 { 472 Context initCtx = new InitialContext(); 473 Context ctx = (Context) initCtx.lookup( "java:comp/env" ); 474 m_ds = (DataSource) ctx.lookup( jndiName ); 475 476 // Prepare the SQL selectors 477 userTable = props.getProperty( PROP_DB_TABLE, DEFAULT_DB_TABLE ); 478 m_email = props.getProperty( PROP_DB_EMAIL, DEFAULT_DB_EMAIL ); 479 m_fullName = props.getProperty( PROP_DB_FULL_NAME, DEFAULT_DB_FULL_NAME ); 480 m_lockExpiry = props.getProperty( PROP_DB_LOCK_EXPIRY, DEFAULT_DB_LOCK_EXPIRY ); 481 m_loginName = props.getProperty( PROP_DB_LOGIN_NAME, DEFAULT_DB_LOGIN_NAME ); 482 m_password = props.getProperty( PROP_DB_PASSWORD, DEFAULT_DB_PASSWORD ); 483 m_uid = props.getProperty( PROP_DB_UID, DEFAULT_DB_UID ); 484 m_wikiName = props.getProperty( PROP_DB_WIKI_NAME, DEFAULT_DB_WIKI_NAME ); 485 m_created = props.getProperty( PROP_DB_CREATED, DEFAULT_DB_CREATED ); 486 m_modified = props.getProperty( PROP_DB_MODIFIED, DEFAULT_DB_MODIFIED ); 487 m_attributes = props.getProperty( PROP_DB_ATTRIBUTES, DEFAULT_DB_ATTRIBUTES ); 488 489 m_findAll = "SELECT * FROM " + userTable; 490 m_findByEmail = "SELECT * FROM " + userTable + " WHERE " + m_email + "=?"; 491 m_findByFullName = "SELECT * FROM " + userTable + " WHERE " + m_fullName + "=?"; 492 m_findByLoginName = "SELECT * FROM " + userTable + " WHERE " + m_loginName + "=?"; 493 m_findByUid = "SELECT * FROM " + userTable + " WHERE " + m_uid + "=?"; 494 m_findByWikiName = "SELECT * FROM " + userTable + " WHERE " + m_wikiName + "=?"; 495 496 // The user insert SQL prepared statement 497 m_insertProfile = "INSERT INTO " + userTable + " (" 498 + m_uid + "," 499 + m_email + "," 500 + m_fullName + "," 501 + m_password + "," 502 + m_wikiName + "," 503 + m_modified + "," 504 + m_loginName + "," 505 + m_attributes + "," 506 + m_created 507 + ") VALUES (?,?,?,?,?,?,?,?,?)"; 508 509 // The user update SQL prepared statement 510 m_updateProfile = "UPDATE " + userTable + " SET " 511 + m_uid + "=?," 512 + m_email + "=?," 513 + m_fullName + "=?," 514 + m_password + "=?," 515 + m_wikiName + "=?," 516 + m_modified + "=?," 517 + m_loginName + "=?," 518 + m_attributes + "=?," 519 + m_lockExpiry + "=? " 520 + "WHERE " + m_loginName + "=?"; 521 522 // Prepare the role insert SQL 523 roleTable = props.getProperty( PROP_DB_ROLE_TABLE, DEFAULT_DB_ROLE_TABLE ); 524 role = props.getProperty( PROP_DB_ROLE, DEFAULT_DB_ROLE ); 525 m_insertRole = "INSERT INTO " + roleTable + " (" + m_loginName + "," + role + ") VALUES (?,?)"; 526 m_findRoles = "SELECT * FROM " + roleTable + " WHERE " + m_loginName + "=?"; 527 528 // Prepare the user delete SQL 529 m_deleteUserByLoginName = "DELETE FROM " + userTable + " WHERE " + m_loginName + "=?"; 530 531 // Prepare the role delete SQL 532 m_deleteRoleByLoginName = "DELETE FROM " + roleTable + " WHERE " + m_loginName + "=?"; 533 534 // Prepare the rename user/roles SQL 535 m_renameProfile = "UPDATE " + userTable + " SET " + m_loginName + "=?," + m_modified + "=? WHERE " + m_loginName 536 + "=?"; 537 m_renameRoles = "UPDATE " + roleTable + " SET " + m_loginName + "=? WHERE " + m_loginName + "=?"; 538 } 539 catch( NamingException e ) 540 { 541 log.error( "JDBCUserDatabase initialization error: " + e.getMessage() ); 542 throw new NoRequiredPropertyException( PROP_DB_DATASOURCE, "JDBCUserDatabase initialization error: " + e.getMessage() ); 543 } 544 545 // Test connection by doing a quickie select 546 Connection conn = null; 547 try 548 { 549 conn = m_ds.getConnection(); 550 PreparedStatement ps = conn.prepareStatement( m_findAll ); 551 ps.close(); 552 } 553 catch( SQLException e ) 554 { 555 log.error( "DB connectivity error: " + e.getMessage() ); 556 throw new WikiSecurityException("DB connectivity error: " + e.getMessage(), e ); 557 } 558 finally 559 { 560 try 561 { 562 if( conn != null ) conn.close(); 563 } 564 catch( Exception e ) 565 { 566 } 567 } 568 log.info( "JDBCUserDatabase initialized from JNDI DataSource: " + jndiName ); 569 570 // Determine if the datasource supports commits 571 try 572 { 573 conn = m_ds.getConnection(); 574 DatabaseMetaData dmd = conn.getMetaData(); 575 if( dmd.supportsTransactions() ) 576 { 577 m_supportsCommits = true; 578 conn.setAutoCommit( false ); 579 log.info( "JDBCUserDatabase supports transactions. Good; we will use them." ); 580 } 581 } 582 catch( SQLException e ) 583 { 584 log.warn( "JDBCUserDatabase warning: user database doesn't seem to support transactions. Reason: " + e.getMessage() ); 585 } 586 finally 587 { 588 try 589 { 590 if( conn != null ) conn.close(); 591 } 592 catch( Exception e ) 593 { 594 } 595 } 596 } 597 598 /** 599 * @see org.apache.wiki.auth.user.UserDatabase#rename(String, String) 600 */ 601 public void rename( String loginName, String newName ) 602 throws NoSuchPrincipalException, 603 DuplicateUserException, 604 WikiSecurityException 605 { 606 // Get the existing user; if not found, throws NoSuchPrincipalException 607 UserProfile profile = findByLoginName( loginName ); 608 609 // Get user with the proposed name; if found, it's a collision 610 try 611 { 612 UserProfile otherProfile = findByLoginName( newName ); 613 if( otherProfile != null ) 614 { 615 throw new DuplicateUserException( "security.error.cannot.rename", newName ); 616 } 617 } 618 catch( NoSuchPrincipalException e ) 619 { 620 // Good! That means it's safe to save using the new name 621 } 622 623 Connection conn = null; 624 try 625 { 626 // Open the database connection 627 conn = m_ds.getConnection(); 628 if( m_supportsCommits ) 629 { 630 conn.setAutoCommit( false ); 631 } 632 633 Timestamp ts = new Timestamp( System.currentTimeMillis() ); 634 Date modDate = new Date( ts.getTime() ); 635 636 // Change the login ID for the user record 637 PreparedStatement ps = conn.prepareStatement( m_renameProfile ); 638 ps.setString( 1, newName ); 639 ps.setTimestamp( 2, ts ); 640 ps.setString( 3, loginName ); 641 ps.execute(); 642 ps.close(); 643 644 // Change the login ID for the role records 645 ps = conn.prepareStatement( m_renameRoles ); 646 ps.setString( 1, newName ); 647 ps.setString( 2, loginName ); 648 ps.execute(); 649 ps.close(); 650 651 // Set the profile name and mod time 652 profile.setLoginName( newName ); 653 profile.setLastModified( modDate ); 654 655 // Commit and close connection 656 if( m_supportsCommits ) 657 { 658 conn.commit(); 659 } 660 } 661 catch( SQLException e ) 662 { 663 throw new WikiSecurityException( e.getMessage(), e ); 664 } 665 finally 666 { 667 try 668 { 669 if( conn != null ) conn.close(); 670 } 671 catch( Exception e ) 672 { 673 } 674 } 675 } 676 677 /** 678 * @see org.apache.wiki.auth.user.UserDatabase#save(org.apache.wiki.auth.user.UserProfile) 679 */ 680 public void save( UserProfile profile ) throws WikiSecurityException 681 { 682 String initialRole = "Authenticated"; 683 684 // Figure out which prepared statement to use & execute it 685 String loginName = profile.getLoginName(); 686 PreparedStatement ps = null; 687 UserProfile existingProfile = null; 688 689 try 690 { 691 existingProfile = findByLoginName( loginName ); 692 } 693 catch( NoSuchPrincipalException e ) 694 { 695 // Existing profile will be null 696 } 697 698 // Get a clean password from the passed profile. 699 // Blank password is the same as null, which means we re-use the 700 // existing one. 701 String password = profile.getPassword(); 702 String existingPassword = (existingProfile == null) ? null : existingProfile.getPassword(); 703 if( NOTHING.equals( password ) ) 704 { 705 password = null; 706 } 707 if( password == null ) 708 { 709 password = existingPassword; 710 } 711 712 // If password changed, hash it before we save 713 if( !StringUtils.equals( password, existingPassword ) ) 714 { 715 password = getHash( password ); 716 } 717 718 Connection conn = null; 719 try 720 { 721 // Open the database connection 722 conn = m_ds.getConnection(); 723 if( m_supportsCommits ) 724 { 725 conn.setAutoCommit( false ); 726 } 727 728 Timestamp ts = new Timestamp( System.currentTimeMillis() ); 729 Date modDate = new Date( ts.getTime() ); 730 java.sql.Date lockExpiry = profile.getLockExpiry() == null ? null : new java.sql.Date( profile.getLockExpiry().getTime() ); 731 if( existingProfile == null ) 732 { 733 // User is new: insert new user record 734 ps = conn.prepareStatement( m_insertProfile ); 735 ps.setString( 1, profile.getUid() ); 736 ps.setString( 2, profile.getEmail() ); 737 ps.setString( 3, profile.getFullname() ); 738 ps.setString( 4, password ); 739 ps.setString( 5, profile.getWikiName() ); 740 ps.setTimestamp( 6, ts ); 741 ps.setString( 7, profile.getLoginName() ); 742 try 743 { 744 ps.setString( 8, Serializer.serializeToBase64( profile.getAttributes() ) ); 745 } 746 catch ( IOException e ) 747 { 748 throw new WikiSecurityException( "Could not save user profile attribute. Reason: " + e.getMessage(), e ); 749 } 750 ps.setTimestamp( 9, ts ); 751 ps.execute(); 752 ps.close(); 753 754 // Insert new role record 755 ps = conn.prepareStatement( m_findRoles ); 756 ps.setString( 1, profile.getLoginName() ); 757 ResultSet rs = ps.executeQuery(); 758 int roles = 0; 759 while ( rs.next() ) 760 { 761 roles++; 762 } 763 ps.close(); 764 if( roles == 0 ) 765 { 766 ps = conn.prepareStatement( m_insertRole ); 767 ps.setString( 1, profile.getLoginName() ); 768 ps.setString( 2, initialRole ); 769 ps.execute(); 770 ps.close(); 771 } 772 773 // Set the profile creation time 774 profile.setCreated( modDate ); 775 } 776 else 777 { 778 // User exists: modify existing record 779 ps = conn.prepareStatement( m_updateProfile ); 780 ps.setString( 1, profile.getUid() ); 781 ps.setString( 2, profile.getEmail() ); 782 ps.setString( 3, profile.getFullname() ); 783 ps.setString( 4, password ); 784 ps.setString( 5, profile.getWikiName() ); 785 ps.setTimestamp( 6, ts ); 786 ps.setString( 7, profile.getLoginName() ); 787 try 788 { 789 ps.setString( 8, Serializer.serializeToBase64( profile.getAttributes() ) ); 790 } 791 catch ( IOException e ) 792 { 793 throw new WikiSecurityException( "Could not save user profile attribute. Reason: " + e.getMessage(), e ); 794 } 795 ps.setDate( 9, lockExpiry ); 796 ps.setString( 10, profile.getLoginName() ); 797 ps.execute(); 798 ps.close(); 799 } 800 // Set the profile mod time 801 profile.setLastModified( modDate ); 802 803 // Commit and close connection 804 if( m_supportsCommits ) 805 { 806 conn.commit(); 807 } 808 } 809 catch( SQLException e ) 810 { 811 throw new WikiSecurityException( e.getMessage(), e ); 812 } 813 finally 814 { 815 try 816 { 817 if( conn != null ) conn.close(); 818 } 819 catch( Exception e ) 820 { 821 } 822 } 823 } 824 825 /** 826 * Private method that returns the first {@link UserProfile} matching a 827 * named column's value. This method will also set the UID if it has not yet been set. 828 * @param sql the SQL statement that should be prepared; it must have one parameter 829 * to set (either a String or a Long) 830 * @param index the value to match 831 * @return the resolved UserProfile 832 * @throws SQLException 833 */ 834 private UserProfile findByPreparedStatement( String sql, Object index ) throws NoSuchPrincipalException 835 { 836 UserProfile profile = null; 837 boolean found = false; 838 boolean unique = true; 839 Connection conn = null; 840 try 841 { 842 // Open the database connection 843 conn = m_ds.getConnection(); 844 if( m_supportsCommits ) 845 { 846 conn.setAutoCommit( false ); 847 } 848 849 PreparedStatement ps = conn.prepareStatement( sql ); 850 851 // Set the parameter to search by 852 if ( index instanceof String ) 853 { 854 ps.setString( 1, (String)index ); 855 } 856 else if ( index instanceof Long ) 857 { 858 ps.setLong( 1, ( (Long)index).longValue() ); 859 } 860 else 861 { 862 throw new IllegalArgumentException( "Index type not recognized!" ); 863 } 864 865 // Go and get the record! 866 ResultSet rs = ps.executeQuery(); 867 while ( rs.next() ) 868 { 869 if( profile != null ) 870 { 871 unique = false; 872 break; 873 } 874 profile = newProfile(); 875 876 // Fetch the basic user attributes 877 profile.setUid( rs.getString( m_uid ) ); 878 if ( profile.getUid() == null ) 879 { 880 profile.setUid( generateUid( this ) ); 881 } 882 profile.setCreated( rs.getTimestamp( m_created ) ); 883 profile.setEmail( rs.getString( m_email ) ); 884 profile.setFullname( rs.getString( m_fullName ) ); 885 profile.setLastModified( rs.getTimestamp( m_modified ) ); 886 Date lockExpiry = rs.getDate( m_lockExpiry ); 887 profile.setLockExpiry( rs.wasNull() ? null : lockExpiry ); 888 profile.setLoginName( rs.getString( m_loginName ) ); 889 profile.setPassword( rs.getString( m_password ) ); 890 891 // Fetch the user attributes 892 String rawAttributes = rs.getString( m_attributes ); 893 if ( rawAttributes != null ) 894 { 895 try 896 { 897 Map<String,? extends Serializable> attributes = Serializer.deserializeFromBase64( rawAttributes ); 898 profile.getAttributes().putAll( attributes ); 899 } 900 catch ( IOException e ) 901 { 902 log.error( "Could not parse user profile attributes!", e ); 903 } 904 } 905 found = true; 906 } 907 ps.close(); 908 } 909 catch( SQLException e ) 910 { 911 throw new NoSuchPrincipalException( e.getMessage() ); 912 } 913 finally 914 { 915 try 916 { 917 if( conn != null ) conn.close(); 918 } 919 catch( Exception e ) 920 { 921 } 922 } 923 924 if( !found ) 925 { 926 throw new NoSuchPrincipalException( "Could not find profile in database!" ); 927 } 928 if( !unique ) 929 { 930 throw new NoSuchPrincipalException( "More than one profile in database!" ); 931 } 932 return profile; 933 934 } 935 936}