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