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 }