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.authorize; 020 021import org.apache.log4j.Logger; 022import org.apache.wiki.api.core.Engine; 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; 027 028import javax.naming.Context; 029import javax.naming.InitialContext; 030import javax.naming.NamingException; 031import javax.sql.DataSource; 032import java.security.Principal; 033import java.sql.Connection; 034import java.sql.DatabaseMetaData; 035import java.sql.PreparedStatement; 036import java.sql.ResultSet; 037import java.sql.SQLException; 038import java.sql.Timestamp; 039import java.util.Date; 040import java.util.HashSet; 041import java.util.Properties; 042import java.util.Set; 043 044/** 045 * <p> 046 * Implementation of GroupDatabase that persists {@link Group} objects to a JDBC 047 * DataSource, as might typically be provided by a web container. This 048 * implementation looks up the JDBC DataSource using JNDI. The JNDI name of the 049 * datasource, backing table and mapped columns used by this class can be 050 * overridden by adding settings in <code>jspwiki.properties</code>. 051 * </p> 052 * <p> 053 * Configurable properties are these: 054 * </p> 055 * <table> 056 * <tr> <thead> 057 * <th>Property</th> 058 * <th>Default</th> 059 * <th>Definition</th> 060 * <thead> </tr> 061 * <tr> 062 * <td><code>jspwiki.groupdatabase.datasource</code></td> 063 * <td><code>jdbc/GroupDatabase</code></td> 064 * <td>The JNDI name of the DataSource</td> 065 * </tr> 066 * <tr> 067 * <td><code>jspwiki.groupdatabase.table</code></td> 068 * <td><code>groups</code></td> 069 * <td>The table that stores the groups</td> 070 * </tr> 071 * <tr> 072 * <td><code>jspwiki.groupdatabase.membertable</code></td> 073 * <td><code>group_members</code></td> 074 * <td>The table that stores the names of group members</td> 075 * </tr> 076 * <tr> 077 * <td><code>jspwiki.groupdatabase.created</code></td> 078 * <td><code>created</code></td> 079 * <td>The column containing the group's creation timestamp</td> 080 * </tr> 081 * <tr> 082 * <td><code>jspwiki.groupdatabase.creator</code></td> 083 * <td><code>creator</code></td> 084 * <td>The column containing the group creator's name</td> 085 * </tr> 086 * <tr> 087 * <td><code>jspwiki.groupdatabase.name</code></td> 088 * <td><code>name</code></td> 089 * <td>The column containing the group's name</td> 090 * </tr> 091 * <tr> 092 * <td><code>jspwiki.groupdatabase.member</code></td> 093 * <td><code>member</code></td> 094 * <td>The column containing the group member's name</td> 095 * </tr> 096 * <tr> 097 * <td><code>jspwiki.groupdatabase.modified</code></td> 098 * <td><code>modified</code></td> 099 * <td>The column containing the group's last-modified timestamp</td> 100 * </tr> 101 * <tr> 102 * <td><code>jspwiki.groupdatabase.modifier</code></td> 103 * <td><code>modifier</code></td> 104 * <td>The column containing the name of the user who last modified the group</td> 105 * </tr> 106 * </table> 107 * <p> 108 * This class is typically used in conjunction with a web container's JNDI 109 * resource factory. For example, Tomcat versions 4 and higher provide a basic 110 * JNDI factory for registering DataSources. To give JSPWiki access to the JNDI 111 * resource named by <code>jdbc/GroupDatabase</code>, you would declare the 112 * datasource resource similar to this: 113 * </p> 114 * <blockquote><code><Context ...><br/> 115 * ...<br/> 116 * <Resource name="jdbc/GroupDatabase" auth="Container"<br/> 117 * type="javax.sql.DataSource" username="dbusername" password="dbpassword"<br/> 118 * driverClassName="org.hsql.jdbcDriver" url="jdbc:HypersonicSQL:database"<br/> 119 * maxActive="8" maxIdle="4"/><br/> 120 * ...<br/> 121 * </Context></code></blockquote> 122 * <p> 123 * JDBC driver JARs should be added to Tomcat's <code>common/lib</code> 124 * directory. For more Tomcat 5.5 JNDI configuration examples, see <a 125 * href="http://tomcat.apache.org/tomcat-5.5-doc/jndi-resources-howto.html"> 126 * http://tomcat.apache.org/tomcat-5.5-doc/jndi-resources-howto.html</a>. 127 * </p> 128 * <p> 129 * JDBCGroupDatabase commits changes as transactions if the back-end database 130 * supports them. Changes are made 131 * immediately (during the {@link #save(Group, Principal)} method). 132 * </p> 133 * 134 * @since 2.3 135 */ 136public class JDBCGroupDatabase implements GroupDatabase { 137 138 /** Default column name that stores the JNDI name of the DataSource. */ 139 public static final String DEFAULT_GROUPDB_DATASOURCE = "jdbc/GroupDatabase"; 140 141 /** Default table name for the table that stores groups. */ 142 public static final String DEFAULT_GROUPDB_TABLE = "groups"; 143 144 /** Default column name that stores the names of group members. */ 145 public static final String DEFAULT_GROUPDB_MEMBER_TABLE = "group_members"; 146 147 /** Default column name that stores the the group creation timestamps. */ 148 public static final String DEFAULT_GROUPDB_CREATED = "created"; 149 150 /** Default column name that stores group creator names. */ 151 public static final String DEFAULT_GROUPDB_CREATOR = "creator"; 152 153 /** Default column name that stores the group names. */ 154 public static final String DEFAULT_GROUPDB_NAME = "name"; 155 156 /** Default column name that stores group member names. */ 157 public static final String DEFAULT_GROUPDB_MEMBER = "member"; 158 159 /** Default column name that stores group last-modified timestamps. */ 160 public static final String DEFAULT_GROUPDB_MODIFIED = "modified"; 161 162 /** Default column name that stores names of users who last modified groups. */ 163 public static final String DEFAULT_GROUPDB_MODIFIER = "modifier"; 164 165 /** The JNDI name of the DataSource. */ 166 public static final String PROP_GROUPDB_DATASOURCE = "jspwiki.groupdatabase.datasource"; 167 168 /** The table that stores the groups. */ 169 public static final String PROP_GROUPDB_TABLE = "jspwiki.groupdatabase.table"; 170 171 /** The table that stores the names of group members. */ 172 public static final String PROP_GROUPDB_MEMBER_TABLE = "jspwiki.groupdatabase.membertable"; 173 174 /** The column containing the group's creation timestamp. */ 175 public static final String PROP_GROUPDB_CREATED = "jspwiki.groupdatabase.created"; 176 177 /** The column containing the group creator's name. */ 178 public static final String PROP_GROUPDB_CREATOR = "jspwiki.groupdatabase.creator"; 179 180 /** The column containing the group's name. */ 181 public static final String PROP_GROUPDB_NAME = "jspwiki.groupdatabase.name"; 182 183 /** The column containing the group member's name. */ 184 public static final String PROP_GROUPDB_MEMBER = "jspwiki.groupdatabase.member"; 185 186 /** The column containing the group's last-modified timestamp. */ 187 public static final String PROP_GROUPDB_MODIFIED = "jspwiki.groupdatabase.modified"; 188 189 /** The column containing the name of the user who last modified the group. */ 190 public static final String PROP_GROUPDB_MODIFIER = "jspwiki.groupdatabase.modifier"; 191 192 protected static final Logger log = Logger.getLogger( JDBCGroupDatabase.class ); 193 194 private DataSource m_ds = null; 195 196 private String m_created = null; 197 198 private String m_creator = null; 199 200 private String m_name = null; 201 202 private String m_member = null; 203 204 private String m_modified = null; 205 206 private String m_modifier = null; 207 208 private String m_findAll = null; 209 210 private String m_findGroup = null; 211 212 private String m_findMembers = null; 213 214 private String m_insertGroup = null; 215 216 private String m_insertGroupMembers = null; 217 218 private String m_updateGroup = null; 219 220 private String m_deleteGroup = null; 221 222 private String m_deleteGroupMembers = null; 223 224 private boolean m_supportsCommits = false; 225 226 private Engine m_engine = null; 227 228 /** 229 * Looks up and deletes a {@link Group} from the group database. If the 230 * group database does not contain the supplied Group. this method throws a 231 * {@link NoSuchPrincipalException}. The method commits the results of the 232 * delete to persistent storage. 233 * 234 * @param group the group to remove 235 * @throws WikiSecurityException if the database does not contain the 236 * supplied group (thrown as {@link NoSuchPrincipalException}) 237 * or if the commit did not succeed 238 */ 239 @Override public void delete( final Group group ) throws WikiSecurityException 240 { 241 if( !exists( group ) ) 242 { 243 throw new NoSuchPrincipalException( "Not in database: " + group.getName() ); 244 } 245 246 final String groupName = group.getName(); 247 Connection conn = null; 248 PreparedStatement ps = null; 249 try 250 { 251 // Open the database connection 252 conn = m_ds.getConnection(); 253 if( m_supportsCommits ) 254 { 255 conn.setAutoCommit( false ); 256 } 257 258 ps = conn.prepareStatement( m_deleteGroup ); 259 ps.setString( 1, groupName ); 260 ps.execute(); 261 ps.close(); 262 263 ps = conn.prepareStatement( m_deleteGroupMembers ); 264 ps.setString( 1, groupName ); 265 ps.execute(); 266 267 // Commit and close connection 268 if( m_supportsCommits ) 269 { 270 conn.commit(); 271 } 272 } 273 catch( final SQLException e ) 274 { 275 closeQuietly( conn, ps, null ); 276 throw new WikiSecurityException( "Could not delete group " + groupName + ": " + e.getMessage(), e ); 277 } 278 finally 279 { 280 closeQuietly( conn, ps, null ); 281 } 282 } 283 284 /** 285 * Returns all wiki groups that are stored in the GroupDatabase as an array 286 * of Group objects. If the database does not contain any groups, this 287 * method will return a zero-length array. This method causes back-end 288 * storage to load the entire set of group; thus, it should be called 289 * infrequently (e.g., at initialization time). 290 * 291 * @return the wiki groups 292 * @throws WikiSecurityException if the groups cannot be returned by the 293 * back-end 294 */ 295 @Override public Group[] groups() throws WikiSecurityException 296 { 297 final Set<Group> groups = new HashSet<>(); 298 Connection conn = null; 299 PreparedStatement ps = null; 300 ResultSet rs = null; 301 try 302 { 303 // Open the database connection 304 conn = m_ds.getConnection(); 305 306 ps = conn.prepareStatement( m_findAll ); 307 rs = ps.executeQuery(); 308 while ( rs.next() ) 309 { 310 final String groupName = rs.getString( m_name ); 311 if( groupName == null ) 312 { 313 log.warn( "Detected null group name in JDBCGroupDataBase. Check your group database." ); 314 } 315 else 316 { 317 final Group group = new Group( groupName, m_engine.getApplicationName() ); 318 group.setCreated( rs.getTimestamp( m_created ) ); 319 group.setCreator( rs.getString( m_creator ) ); 320 group.setLastModified( rs.getTimestamp( m_modified ) ); 321 group.setModifier( rs.getString( m_modifier ) ); 322 populateGroup( group ); 323 groups.add( group ); 324 } 325 } 326 } 327 catch( final SQLException e ) 328 { 329 closeQuietly( conn, ps, rs ); 330 throw new WikiSecurityException( e.getMessage(), e ); 331 } 332 finally 333 { 334 closeQuietly( conn, ps, rs ); 335 } 336 337 return groups.toArray( new Group[groups.size()] ); 338 } 339 340 /** 341 * Saves a Group to the group database. Note that this method <em>must</em> 342 * fail, and throw an <code>IllegalArgumentException</code>, if the 343 * proposed group is the same name as one of the built-in Roles: e.g., 344 * Admin, Authenticated, etc. The database is responsible for setting 345 * create/modify timestamps, upon a successful save, to the Group. The 346 * method commits the results of the delete to persistent storage. 347 * 348 * @param group the Group to save 349 * @param modifier the user who saved the Group 350 * @throws WikiSecurityException if the Group could not be saved successfully 351 */ 352 @Override public void save( final Group group, final Principal modifier ) throws WikiSecurityException 353 { 354 if( group == null || modifier == null ) 355 { 356 throw new IllegalArgumentException( "Group or modifier cannot be null." ); 357 } 358 359 final boolean exists = exists( group ); 360 Connection conn = null; 361 PreparedStatement ps = null; 362 try 363 { 364 // Open the database connection 365 conn = m_ds.getConnection(); 366 if( m_supportsCommits ) 367 { 368 conn.setAutoCommit( false ); 369 } 370 371 final Timestamp ts = new Timestamp( System.currentTimeMillis() ); 372 final Date modDate = new Date( ts.getTime() ); 373 if( !exists ) 374 { 375 // Group is new: insert new group record 376 ps = conn.prepareStatement( m_insertGroup ); 377 ps.setString( 1, group.getName() ); 378 ps.setTimestamp( 2, ts ); 379 ps.setString( 3, modifier.getName() ); 380 ps.setTimestamp( 4, ts ); 381 ps.setString( 5, modifier.getName() ); 382 ps.execute(); 383 384 // Set the group creation time 385 group.setCreated( modDate ); 386 group.setCreator( modifier.getName() ); 387 ps.close(); 388 } 389 else 390 { 391 // Modify existing group record 392 ps = conn.prepareStatement( m_updateGroup ); 393 ps.setTimestamp( 1, ts ); 394 ps.setString( 2, modifier.getName() ); 395 ps.setString( 3, group.getName() ); 396 ps.execute(); 397 ps.close(); 398 } 399 // Set the group modified time 400 group.setLastModified( modDate ); 401 group.setModifier( modifier.getName() ); 402 403 // Now, update the group member list 404 405 // First, delete all existing member records 406 ps = conn.prepareStatement( m_deleteGroupMembers ); 407 ps.setString( 1, group.getName() ); 408 ps.execute(); 409 ps.close(); 410 411 // Insert group member records 412 ps = conn.prepareStatement( m_insertGroupMembers ); 413 final Principal[] members = group.members(); 414 for( int i = 0; i < members.length; i++ ) 415 { 416 final Principal member = members[i]; 417 ps.setString( 1, group.getName() ); 418 ps.setString( 2, member.getName() ); 419 ps.execute(); 420 } 421 422 // Commit and close connection 423 if( m_supportsCommits ) 424 { 425 conn.commit(); 426 } 427 } 428 catch( final SQLException e ) 429 { 430 closeQuietly(conn, ps, null ); 431 throw new WikiSecurityException( e.getMessage(), e ); 432 } 433 finally 434 { 435 closeQuietly(conn, ps, null ); 436 } 437 } 438 439 /** 440 * Initializes the group database based on values from a Properties object. 441 * 442 * @param engine the wiki engine 443 * @param props the properties used to initialize the group database 444 * @throws WikiSecurityException if the database could not be initialized 445 * successfully 446 * @throws NoRequiredPropertyException if a required property is not present 447 */ 448 @Override public void initialize( final Engine engine, final Properties props ) throws NoRequiredPropertyException, WikiSecurityException 449 { 450 final String table; 451 final String memberTable; 452 453 m_engine = engine; 454 455 final String jndiName = props.getProperty( PROP_GROUPDB_DATASOURCE, DEFAULT_GROUPDB_DATASOURCE ); 456 try 457 { 458 final Context initCtx = new InitialContext(); 459 final Context ctx = (Context) initCtx.lookup( "java:comp/env" ); 460 m_ds = (DataSource) ctx.lookup( jndiName ); 461 462 // Prepare the SQL selectors 463 table = props.getProperty( PROP_GROUPDB_TABLE, DEFAULT_GROUPDB_TABLE ); 464 memberTable = props.getProperty( PROP_GROUPDB_MEMBER_TABLE, DEFAULT_GROUPDB_MEMBER_TABLE ); 465 m_name = props.getProperty( PROP_GROUPDB_NAME, DEFAULT_GROUPDB_NAME ); 466 m_created = props.getProperty( PROP_GROUPDB_CREATED, DEFAULT_GROUPDB_CREATED ); 467 m_creator = props.getProperty( PROP_GROUPDB_CREATOR, DEFAULT_GROUPDB_CREATOR ); 468 m_modifier = props.getProperty( PROP_GROUPDB_MODIFIER, DEFAULT_GROUPDB_MODIFIER ); 469 m_modified = props.getProperty( PROP_GROUPDB_MODIFIED, DEFAULT_GROUPDB_MODIFIED ); 470 m_member = props.getProperty( PROP_GROUPDB_MEMBER, DEFAULT_GROUPDB_MEMBER ); 471 472 m_findAll = "SELECT DISTINCT * FROM " + table; 473 m_findGroup = "SELECT DISTINCT * FROM " + table + " WHERE " + m_name + "=?"; 474 m_findMembers = "SELECT * FROM " + memberTable + " WHERE " + m_name + "=?"; 475 476 // Prepare the group insert/update SQL 477 m_insertGroup = "INSERT INTO " + table + " (" + m_name + "," + m_modified + "," + m_modifier + "," + m_created + "," 478 + m_creator + ") VALUES (?,?,?,?,?)"; 479 m_updateGroup = "UPDATE " + table + " SET " + m_modified + "=?," + m_modifier + "=? WHERE " + m_name + "=?"; 480 481 // Prepare the group member insert SQL 482 m_insertGroupMembers = "INSERT INTO " + memberTable + " (" + m_name + "," + m_member + ") VALUES (?,?)"; 483 484 // Prepare the group delete SQL 485 m_deleteGroup = "DELETE FROM " + table + " WHERE " + m_name + "=?"; 486 m_deleteGroupMembers = "DELETE FROM " + memberTable + " WHERE " + m_name + "=?"; 487 } 488 catch( final NamingException e ) 489 { 490 log.error( "JDBCGroupDatabase initialization error: " + e ); 491 throw new NoRequiredPropertyException( PROP_GROUPDB_DATASOURCE, "JDBCGroupDatabase initialization error: " + e); 492 } 493 494 // Test connection by doing a quickie select 495 Connection conn = null; 496 PreparedStatement ps = null; 497 try 498 { 499 conn = m_ds.getConnection(); 500 ps = conn.prepareStatement( m_findAll ); 501 ps.executeQuery(); 502 ps.close(); 503 } 504 catch( final SQLException e ) 505 { 506 closeQuietly( conn, ps, null ); 507 log.error( "DB connectivity error: " + e.getMessage() ); 508 throw new WikiSecurityException("DB connectivity error: " + e.getMessage(), e ); 509 } 510 finally 511 { 512 closeQuietly( conn, ps, null ); 513 } 514 log.info( "JDBCGroupDatabase initialized from JNDI DataSource: " + jndiName ); 515 516 // Determine if the datasource supports commits 517 try 518 { 519 conn = m_ds.getConnection(); 520 final DatabaseMetaData dmd = conn.getMetaData(); 521 if( dmd.supportsTransactions() ) 522 { 523 m_supportsCommits = true; 524 conn.setAutoCommit( false ); 525 log.info( "JDBCGroupDatabase supports transactions. Good; we will use them." ); 526 } 527 } 528 catch( final SQLException e ) 529 { 530 closeQuietly( conn, null, null ); 531 log.warn( "JDBCGroupDatabase warning: user database doesn't seem to support transactions. Reason: " + e); 532 } 533 finally 534 { 535 closeQuietly( conn, null, null ); 536 } 537 } 538 539 /** 540 * Returns <code>true</code> if the Group exists in back-end storage. 541 * 542 * @param group the Group to look for 543 * @return the result of the search 544 */ 545 private boolean exists( final Group group ) 546 { 547 final String index = group.getName(); 548 try 549 { 550 findGroup( index ); 551 return true; 552 } 553 catch( final NoSuchPrincipalException e ) 554 { 555 return false; 556 } 557 } 558 559 /** 560 * Loads and returns a Group from the back-end database matching a supplied 561 * name. 562 * 563 * @param index the name of the Group to find 564 * @return the populated Group 565 * @throws NoSuchPrincipalException if the Group cannot be found 566 * @throws SQLException if the database query returns an error 567 */ 568 private Group findGroup( final String index ) throws NoSuchPrincipalException 569 { 570 Group group = null; 571 boolean found = false; 572 boolean unique = true; 573 ResultSet rs = null; 574 PreparedStatement ps = null; 575 Connection conn = null; 576 try 577 { 578 // Open the database connection 579 conn = m_ds.getConnection(); 580 581 ps = conn.prepareStatement( m_findGroup ); 582 ps.setString( 1, index ); 583 rs = ps.executeQuery(); 584 while ( rs.next() ) 585 { 586 if( group != null ) 587 { 588 unique = false; 589 break; 590 } 591 group = new Group( index, m_engine.getApplicationName() ); 592 group.setCreated( rs.getTimestamp( m_created ) ); 593 group.setCreator( rs.getString( m_creator ) ); 594 group.setLastModified( rs.getTimestamp( m_modified ) ); 595 group.setModifier( rs.getString( m_modifier ) ); 596 populateGroup( group ); 597 found = true; 598 } 599 } 600 catch( final SQLException e ) 601 { 602 closeQuietly( conn, ps, rs ); 603 throw new NoSuchPrincipalException( e.getMessage() ); 604 } 605 finally 606 { 607 closeQuietly( conn, ps, rs ); 608 } 609 610 if( !found ) 611 { 612 throw new NoSuchPrincipalException( "Could not find group in database!" ); 613 } 614 if( !unique ) 615 { 616 throw new NoSuchPrincipalException( "More than one group in database!" ); 617 } 618 return group; 619 } 620 621 /** 622 * Fills a Group with members. 623 * 624 * @param group the group to populate 625 * @return the populated Group 626 */ 627 private Group populateGroup( final Group group ) 628 { 629 ResultSet rs = null; 630 PreparedStatement ps = null; 631 Connection conn = null; 632 try 633 { 634 // Open the database connection 635 conn = m_ds.getConnection(); 636 637 ps = conn.prepareStatement( m_findMembers ); 638 ps.setString( 1, group.getName() ); 639 rs = ps.executeQuery(); 640 while ( rs.next() ) 641 { 642 final String memberName = rs.getString( m_member ); 643 if( memberName != null ) 644 { 645 final WikiPrincipal principal = new WikiPrincipal( memberName, WikiPrincipal.UNSPECIFIED ); 646 group.add( principal ); 647 } 648 } 649 } 650 catch( final SQLException e ) 651 { 652 // I guess that means there aren't any principals... 653 } 654 finally 655 { 656 closeQuietly( conn, ps, rs ); 657 } 658 return group; 659 } 660 661 void closeQuietly( final Connection conn, final PreparedStatement ps, final ResultSet rs ) { 662 if( conn != null ) { 663 try { 664 conn.close(); 665 } catch( final Exception e ) { 666 } 667 } 668 if( ps != null ) { 669 try { 670 ps.close(); 671 } catch( final Exception e ) { 672 } 673 } 674 if( rs != null ) { 675 try { 676 rs.close(); 677 } catch( final Exception e ) { 678 } 679 } 680 } 681 682}