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 private static final String NOTHING = ""; 199 200 public static final String DEFAULT_DB_ATTRIBUTES = "attributes"; 201 202 public static final String DEFAULT_DB_CREATED = "created"; 203 204 public static final String DEFAULT_DB_EMAIL = "email"; 205 206 public static final String DEFAULT_DB_FULL_NAME = "full_name"; 207 208 public static final String DEFAULT_DB_JNDI_NAME = "jdbc/UserDatabase"; 209 210 public static final String DEFAULT_DB_LOCK_EXPIRY = "lock_expiry"; 211 212 public static final String DEFAULT_DB_MODIFIED = "modified"; 213 214 public static final String DEFAULT_DB_ROLE = "role"; 215 216 public static final String DEFAULT_DB_ROLE_TABLE = "roles"; 217 218 public static final String DEFAULT_DB_TABLE = "users"; 219 220 public static final String DEFAULT_DB_LOGIN_NAME = "login_name"; 221 222 public static final String DEFAULT_DB_PASSWORD = "password"; 223 224 public static final String DEFAULT_DB_UID = "uid"; 225 226 public static final String DEFAULT_DB_WIKI_NAME = "wiki_name"; 227 228 public static final String PROP_DB_ATTRIBUTES = "jspwiki.userdatabase.attributes"; 229 230 public static final String PROP_DB_CREATED = "jspwiki.userdatabase.created"; 231 232 public static final String PROP_DB_EMAIL = "jspwiki.userdatabase.email"; 233 234 public static final String PROP_DB_FULL_NAME = "jspwiki.userdatabase.fullName"; 235 236 public static final String PROP_DB_DATASOURCE = "jspwiki.userdatabase.datasource"; 237 238 public static final String PROP_DB_LOCK_EXPIRY = "jspwiki.userdatabase.lockExpiry"; 239 240 public static final String PROP_DB_LOGIN_NAME = "jspwiki.userdatabase.loginName"; 241 242 public static final String PROP_DB_MODIFIED = "jspwiki.userdatabase.modified"; 243 244 public static final String PROP_DB_PASSWORD = "jspwiki.userdatabase.password"; 245 246 public static final String PROP_DB_UID = "jspwiki.userdatabase.uid"; 247 248 public static final String PROP_DB_ROLE = "jspwiki.userdatabase.role"; 249 250 public static final String PROP_DB_ROLE_TABLE = "jspwiki.userdatabase.roleTable"; 251 252 public static final String PROP_DB_TABLE = "jspwiki.userdatabase.table"; 253 254 public static final String PROP_DB_WIKI_NAME = "jspwiki.userdatabase.wikiName"; 255 256 private DataSource m_ds = null; 257 258 private String m_deleteUserByLoginName = null; 259 260 private String m_deleteRoleByLoginName = null; 261 262 private String m_findByEmail = null; 263 264 private String m_findByFullName = null; 265 266 private String m_findByLoginName = null; 267 268 private String m_findByUid = null; 269 270 private String m_findByWikiName = null; 271 272 private String m_renameProfile = null; 273 274 private String m_renameRoles = null; 275 276 private String m_updateProfile = null; 277 278 private String m_findAll = null; 279 280 private String m_findRoles = null; 281 282 private String m_insertProfile = null; 283 284 private String m_insertRole = null; 285 286 private String m_attributes = null; 287 288 private String m_email = null; 289 290 private String m_fullName = null; 291 292 private String m_lockExpiry = null; 293 294 private String m_loginName = null; 295 296 private String m_password = null; 297 298 private String m_uid = null; 299 300 private String m_wikiName = null; 301 302 private String m_created = null; 303 304 private String m_modified = null; 305 306 private boolean m_supportsCommits = false; 307 308 /** 309 * Looks up and deletes the first {@link UserProfile} in the user database 310 * that matches a profile having a given login name. If the user database 311 * does not contain a user with a matching attribute, throws a 312 * {@link NoSuchPrincipalException}. This method is intended to be atomic; 313 * results cannot be partially committed. If the commit fails, it should 314 * roll back its state appropriately. Implementing classes that persist to 315 * the file system may wish to make this method <code>synchronized</code>. 316 * 317 * @param loginName the login name of the user profile that shall be deleted 318 */ 319 @Override 320 public void deleteByLoginName( String loginName ) throws NoSuchPrincipalException, WikiSecurityException { 321 // Get the existing user; if not found, throws NoSuchPrincipalException 322 findByLoginName( loginName ); 323 324 try( Connection conn = m_ds.getConnection() ; 325 PreparedStatement ps1 = conn.prepareStatement( m_deleteUserByLoginName ); 326 PreparedStatement ps2 = conn.prepareStatement( m_deleteRoleByLoginName ) ) 327 { 328 // Open the database connection 329 if( m_supportsCommits ) { 330 conn.setAutoCommit( false ); 331 } 332 333 // Delete user record 334 ps1.setString( 1, loginName ); 335 ps1.execute(); 336 337 // Delete role record 338 ps2.setString( 1, loginName ); 339 ps2.execute(); 340 341 // Commit and close connection 342 if( m_supportsCommits ) { 343 conn.commit(); 344 } 345 } catch( SQLException e ) { 346 throw new WikiSecurityException( e.getMessage(), e ); 347 } 348 } 349 350 /** 351 * @see org.apache.wiki.auth.user.UserDatabase#findByEmail(java.lang.String) 352 */ 353 @Override 354 public UserProfile findByEmail( String index ) throws NoSuchPrincipalException { 355 return findByPreparedStatement( m_findByEmail, index ); 356 } 357 358 /** 359 * @see org.apache.wiki.auth.user.UserDatabase#findByFullName(java.lang.String) 360 */ 361 @Override 362 public UserProfile findByFullName( String index ) throws NoSuchPrincipalException { 363 return findByPreparedStatement( m_findByFullName, index ); 364 } 365 366 /** 367 * @see org.apache.wiki.auth.user.UserDatabase#findByLoginName(java.lang.String) 368 */ 369 @Override 370 public UserProfile findByLoginName( String index ) throws NoSuchPrincipalException { 371 return findByPreparedStatement( m_findByLoginName, index ); 372 } 373 374 /** 375 * @see org.apache.wiki.auth.user.UserDatabase#findByWikiName(String) 376 */ 377 @Override 378 public UserProfile findByUid( String uid ) throws NoSuchPrincipalException { 379 return findByPreparedStatement( m_findByUid, uid ); 380 } 381 382 /** 383 * @see org.apache.wiki.auth.user.UserDatabase#findByWikiName(String) 384 */ 385 @Override 386 public UserProfile findByWikiName( String index ) throws NoSuchPrincipalException { 387 return findByPreparedStatement( m_findByWikiName, index ); 388 } 389 390 /** 391 * Returns all WikiNames that are stored in the UserDatabase as an array of 392 * WikiPrincipal objects. If the database does not contain any profiles, 393 * this method will return a zero-length array. 394 * 395 * @return the WikiNames 396 */ 397 @Override 398 public Principal[] getWikiNames() throws WikiSecurityException { 399 Set<Principal> principals = new HashSet<>(); 400 try( Connection conn = m_ds.getConnection(); 401 PreparedStatement ps = conn.prepareStatement( m_findAll ); 402 ResultSet rs = ps.executeQuery() ) 403 { 404 while ( rs.next() ) { 405 String wikiName = rs.getString( m_wikiName ); 406 if( wikiName == null ) { 407 log.warn( "Detected null wiki name in XMLUserDataBase. Check your user database." ); 408 } else { 409 Principal principal = new WikiPrincipal( wikiName, WikiPrincipal.WIKI_NAME ); 410 principals.add( principal ); 411 } 412 } 413 } catch( SQLException e ) { 414 throw new WikiSecurityException( e.getMessage(), e ); 415 } 416 417 return principals.toArray( new Principal[principals.size()] ); 418 } 419 420 /** 421 * @see org.apache.wiki.auth.user.UserDatabase#initialize(org.apache.wiki.WikiEngine, 422 * java.util.Properties) 423 */ 424 @Override 425 public void initialize( WikiEngine engine, Properties props ) throws NoRequiredPropertyException, WikiSecurityException { 426 String jndiName = props.getProperty( PROP_DB_DATASOURCE, DEFAULT_DB_JNDI_NAME ); 427 try { 428 Context initCtx = new InitialContext(); 429 Context ctx = (Context) initCtx.lookup( "java:comp/env" ); 430 m_ds = (DataSource) ctx.lookup( jndiName ); 431 432 // Prepare the SQL selectors 433 String userTable = props.getProperty( PROP_DB_TABLE, DEFAULT_DB_TABLE ); 434 m_email = props.getProperty( PROP_DB_EMAIL, DEFAULT_DB_EMAIL ); 435 m_fullName = props.getProperty( PROP_DB_FULL_NAME, DEFAULT_DB_FULL_NAME ); 436 m_lockExpiry = props.getProperty( PROP_DB_LOCK_EXPIRY, DEFAULT_DB_LOCK_EXPIRY ); 437 m_loginName = props.getProperty( PROP_DB_LOGIN_NAME, DEFAULT_DB_LOGIN_NAME ); 438 m_password = props.getProperty( PROP_DB_PASSWORD, DEFAULT_DB_PASSWORD ); 439 m_uid = props.getProperty( PROP_DB_UID, DEFAULT_DB_UID ); 440 m_wikiName = props.getProperty( PROP_DB_WIKI_NAME, DEFAULT_DB_WIKI_NAME ); 441 m_created = props.getProperty( PROP_DB_CREATED, DEFAULT_DB_CREATED ); 442 m_modified = props.getProperty( PROP_DB_MODIFIED, DEFAULT_DB_MODIFIED ); 443 m_attributes = props.getProperty( PROP_DB_ATTRIBUTES, DEFAULT_DB_ATTRIBUTES ); 444 445 m_findAll = "SELECT * FROM " + userTable; 446 m_findByEmail = "SELECT * FROM " + userTable + " WHERE " + m_email + "=?"; 447 m_findByFullName = "SELECT * FROM " + userTable + " WHERE " + m_fullName + "=?"; 448 m_findByLoginName = "SELECT * FROM " + userTable + " WHERE " + m_loginName + "=?"; 449 m_findByUid = "SELECT * FROM " + userTable + " WHERE " + m_uid + "=?"; 450 m_findByWikiName = "SELECT * FROM " + userTable + " WHERE " + m_wikiName + "=?"; 451 452 // The user insert SQL prepared statement 453 m_insertProfile = "INSERT INTO " + userTable + " (" 454 + m_uid + "," 455 + m_email + "," 456 + m_fullName + "," 457 + m_password + "," 458 + m_wikiName + "," 459 + m_modified + "," 460 + m_loginName + "," 461 + m_attributes + "," 462 + m_created 463 + ") VALUES (?,?,?,?,?,?,?,?,?)"; 464 465 // The user update SQL prepared statement 466 m_updateProfile = "UPDATE " + userTable + " SET " 467 + m_uid + "=?," 468 + m_email + "=?," 469 + m_fullName + "=?," 470 + m_password + "=?," 471 + m_wikiName + "=?," 472 + m_modified + "=?," 473 + m_loginName + "=?," 474 + m_attributes + "=?," 475 + m_lockExpiry + "=? " 476 + "WHERE " + m_loginName + "=?"; 477 478 // Prepare the role insert SQL 479 String roleTable = props.getProperty( PROP_DB_ROLE_TABLE, DEFAULT_DB_ROLE_TABLE ); 480 String role = props.getProperty( PROP_DB_ROLE, DEFAULT_DB_ROLE ); 481 m_insertRole = "INSERT INTO " + roleTable + " (" + m_loginName + "," + role + ") VALUES (?,?)"; 482 m_findRoles = "SELECT * FROM " + roleTable + " WHERE " + m_loginName + "=?"; 483 484 // Prepare the user delete SQL 485 m_deleteUserByLoginName = "DELETE FROM " + userTable + " WHERE " + m_loginName + "=?"; 486 487 // Prepare the role delete SQL 488 m_deleteRoleByLoginName = "DELETE FROM " + roleTable + " WHERE " + m_loginName + "=?"; 489 490 // Prepare the rename user/roles SQL 491 m_renameProfile = "UPDATE " + userTable + " SET " + m_loginName + "=?," + m_modified + "=? WHERE " + m_loginName 492 + "=?"; 493 m_renameRoles = "UPDATE " + roleTable + " SET " + m_loginName + "=? WHERE " + m_loginName + "=?"; 494 } catch( NamingException e ) { 495 log.error( "JDBCUserDatabase initialization error: " + e.getMessage() ); 496 throw new NoRequiredPropertyException( PROP_DB_DATASOURCE, "JDBCUserDatabase initialization error: " + e.getMessage() ); 497 } 498 499 // Test connection by doing a quickie select 500 try( Connection conn = m_ds.getConnection(); PreparedStatement ps = conn.prepareStatement( m_findAll ) ) { 501 } catch( SQLException e ) { 502 log.error( "DB connectivity error: " + e.getMessage() ); 503 throw new WikiSecurityException("DB connectivity error: " + e.getMessage(), e ); 504 } 505 log.info( "JDBCUserDatabase initialized from JNDI DataSource: " + jndiName ); 506 507 // Determine if the datasource supports commits 508 try( Connection conn = m_ds.getConnection() ) { 509 DatabaseMetaData dmd = conn.getMetaData(); 510 if( dmd.supportsTransactions() ) { 511 m_supportsCommits = true; 512 conn.setAutoCommit( false ); 513 log.info( "JDBCUserDatabase supports transactions. Good; we will use them." ); 514 } 515 } catch( SQLException e ) { 516 log.warn( "JDBCUserDatabase warning: user database doesn't seem to support transactions. Reason: " + e.getMessage() ); 517 } 518 } 519 520 /** 521 * @see org.apache.wiki.auth.user.UserDatabase#rename(String, String) 522 */ 523 @Override 524 public void rename( String loginName, String newName ) throws NoSuchPrincipalException, DuplicateUserException, WikiSecurityException { 525 // Get the existing user; if not found, throws NoSuchPrincipalException 526 UserProfile profile = findByLoginName( loginName ); 527 528 // Get user with the proposed name; if found, it's a collision 529 try { 530 UserProfile otherProfile = findByLoginName( newName ); 531 if( otherProfile != null ) { 532 throw new DuplicateUserException( "security.error.cannot.rename", newName ); 533 } 534 } catch( NoSuchPrincipalException e ) { 535 // Good! That means it's safe to save using the new name 536 } 537 538 try( Connection conn = m_ds.getConnection(); 539 PreparedStatement ps1 = conn.prepareStatement( m_renameProfile ); 540 PreparedStatement ps2 = conn.prepareStatement( m_renameRoles ) ) 541 { 542 if( m_supportsCommits ) { 543 conn.setAutoCommit( false ); 544 } 545 546 Timestamp ts = new Timestamp( System.currentTimeMillis() ); 547 Date modDate = new Date( ts.getTime() ); 548 549 // Change the login ID for the user record 550 ps1.setString( 1, newName ); 551 ps1.setTimestamp( 2, ts ); 552 ps1.setString( 3, loginName ); 553 ps1.execute(); 554 555 // Change the login ID for the role records 556 ps2.setString( 1, newName ); 557 ps2.setString( 2, loginName ); 558 ps2.execute(); 559 560 // Set the profile name and mod time 561 profile.setLoginName( newName ); 562 profile.setLastModified( modDate ); 563 564 // Commit and close connection 565 if( m_supportsCommits ) { 566 conn.commit(); 567 } 568 } catch( SQLException e ) { 569 throw new WikiSecurityException( e.getMessage(), e ); 570 } 571 } 572 573 /** 574 * @see org.apache.wiki.auth.user.UserDatabase#save(org.apache.wiki.auth.user.UserProfile) 575 */ 576 @Override 577 public void save( UserProfile profile ) throws WikiSecurityException { 578 String initialRole = "Authenticated"; 579 580 // Figure out which prepared statement to use & execute it 581 String loginName = profile.getLoginName(); 582 UserProfile existingProfile = null; 583 584 try { 585 existingProfile = findByLoginName( loginName ); 586 } catch( NoSuchPrincipalException e ) { 587 // Existing profile will be null 588 } 589 590 // Get a clean password from the passed profile. 591 // Blank password is the same as null, which means we re-use the existing one. 592 String password = profile.getPassword(); 593 String existingPassword = (existingProfile == null) ? null : existingProfile.getPassword(); 594 if( NOTHING.equals( password ) ) { 595 password = null; 596 } 597 if( password == null ) { 598 password = existingPassword; 599 } 600 601 // If password changed, hash it before we save 602 if( !StringUtils.equals( password, existingPassword ) ) { 603 password = getHash( password ); 604 } 605 606 try( Connection conn = m_ds.getConnection(); 607 PreparedStatement ps1 = conn.prepareStatement( m_insertProfile ); 608 PreparedStatement ps2 = conn.prepareStatement( m_findRoles ); 609 PreparedStatement ps3 = conn.prepareStatement( m_insertRole ); 610 PreparedStatement ps4 = conn.prepareStatement( m_updateProfile ) ) 611 { 612 if( m_supportsCommits ) { 613 conn.setAutoCommit( false ); 614 } 615 616 Timestamp ts = new Timestamp( System.currentTimeMillis() ); 617 Date modDate = new Date( ts.getTime() ); 618 java.sql.Date lockExpiry = profile.getLockExpiry() == null ? null : new java.sql.Date( profile.getLockExpiry().getTime() ); 619 if( existingProfile == null ) 620 { 621 // User is new: insert new user record 622 ps1.setString( 1, profile.getUid() ); 623 ps1.setString( 2, profile.getEmail() ); 624 ps1.setString( 3, profile.getFullname() ); 625 ps1.setString( 4, password ); 626 ps1.setString( 5, profile.getWikiName() ); 627 ps1.setTimestamp( 6, ts ); 628 ps1.setString( 7, profile.getLoginName() ); 629 try { 630 ps1.setString( 8, Serializer.serializeToBase64( profile.getAttributes() ) ); 631 } catch ( IOException e ) { 632 throw new WikiSecurityException( "Could not save user profile attribute. Reason: " + e.getMessage(), e ); 633 } 634 ps1.setTimestamp( 9, ts ); 635 ps1.execute(); 636 637 // Insert new role record 638 ps2.setString( 1, profile.getLoginName() ); 639 int roles = 0; 640 try ( ResultSet rs = ps2.executeQuery() ) { 641 while ( rs.next() ) { 642 roles++; 643 } 644 } 645 646 if( roles == 0 ) { 647 ps3.setString( 1, profile.getLoginName() ); 648 ps3.setString( 2, initialRole ); 649 ps3.execute(); 650 } 651 652 // Set the profile creation time 653 profile.setCreated( modDate ); 654 } else { 655 // User exists: modify existing record 656 ps4.setString( 1, profile.getUid() ); 657 ps4.setString( 2, profile.getEmail() ); 658 ps4.setString( 3, profile.getFullname() ); 659 ps4.setString( 4, password ); 660 ps4.setString( 5, profile.getWikiName() ); 661 ps4.setTimestamp( 6, ts ); 662 ps4.setString( 7, profile.getLoginName() ); 663 try 664 { 665 ps4.setString( 8, Serializer.serializeToBase64( profile.getAttributes() ) ); 666 } 667 catch ( IOException e ) 668 { 669 throw new WikiSecurityException( "Could not save user profile attribute. Reason: " + e.getMessage(), e ); 670 } 671 ps4.setDate( 9, lockExpiry ); 672 ps4.setString( 10, profile.getLoginName() ); 673 ps4.execute(); 674 } 675 // Set the profile mod time 676 profile.setLastModified( modDate ); 677 678 // Commit and close connection 679 if( m_supportsCommits ) { 680 conn.commit(); 681 } 682 } 683 catch( SQLException e ) 684 { 685 throw new WikiSecurityException( e.getMessage(), e ); 686 } 687 } 688 689 /** 690 * Private method that returns the first {@link UserProfile} matching a 691 * named column's value. This method will also set the UID if it has not yet been set. 692 * @param sql the SQL statement that should be prepared; it must have one parameter 693 * to set (either a String or a Long) 694 * @param index the value to match 695 * @return the resolved UserProfile 696 * @throws SQLException 697 */ 698 private UserProfile findByPreparedStatement( String sql, Object index ) throws NoSuchPrincipalException 699 { 700 UserProfile profile = null; 701 boolean found = false; 702 boolean unique = true; 703 try( Connection conn = m_ds.getConnection(); PreparedStatement ps = conn.prepareStatement( sql ) ) { 704 if( m_supportsCommits ) { 705 conn.setAutoCommit( false ); 706 } 707 708 // Set the parameter to search by 709 if ( index instanceof String ) { 710 ps.setString( 1, (String)index ); 711 } else if ( index instanceof Long ) { 712 ps.setLong( 1, ( (Long)index).longValue() ); 713 } else { 714 throw new IllegalArgumentException( "Index type not recognized!" ); 715 } 716 717 // Go and get the record! 718 try ( ResultSet rs = ps.executeQuery() ) { 719 while ( rs.next() ) { 720 if( profile != null ) { 721 unique = false; 722 break; 723 } 724 profile = newProfile(); 725 726 // Fetch the basic user attributes 727 profile.setUid( rs.getString( m_uid ) ); 728 if ( profile.getUid() == null ) { 729 profile.setUid( generateUid( this ) ); 730 } 731 profile.setCreated( rs.getTimestamp( m_created ) ); 732 profile.setEmail( rs.getString( m_email ) ); 733 profile.setFullname( rs.getString( m_fullName ) ); 734 profile.setLastModified( rs.getTimestamp( m_modified ) ); 735 Date lockExpiry = rs.getDate( m_lockExpiry ); 736 profile.setLockExpiry( rs.wasNull() ? null : lockExpiry ); 737 profile.setLoginName( rs.getString( m_loginName ) ); 738 profile.setPassword( rs.getString( m_password ) ); 739 740 // Fetch the user attributes 741 String rawAttributes = rs.getString( m_attributes ); 742 if ( rawAttributes != null ) { 743 try { 744 Map<String,? extends Serializable> attributes = Serializer.deserializeFromBase64( rawAttributes ); 745 profile.getAttributes().putAll( attributes ); 746 } catch ( IOException e ) { 747 log.error( "Could not parse user profile attributes!", e ); 748 } 749 } 750 found = true; 751 } 752 } 753 } catch( SQLException e ) { 754 throw new NoSuchPrincipalException( e.getMessage() ); 755 } 756 757 if( !found ) { 758 throw new NoSuchPrincipalException( "Could not find profile in database!" ); 759 } 760 if( !unique ) { 761 throw new NoSuchPrincipalException( "More than one profile in database!" ); 762 } 763 return profile; 764 } 765 766}