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