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