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