001: /*
002: * $Header: /home/cvs/jakarta-tomcat-4.0/catalina/src/share/org/apache/catalina/realm/JDBCRealm.java,v 1.21 2002/06/09 02:19:43 remm Exp $
003: * $Revision: 1.21 $
004: * $Date: 2002/06/09 02:19:43 $
005: *
006: * ====================================================================
007: * The Apache Software License, Version 1.1
008: *
009: * Copyright (c) 1999 The Apache Software Foundation. All rights
010: * reserved.
011: *
012: * Redistribution and use in source and binary forms, with or without
013: * modification, are permitted provided that the following conditions
014: * are met:
015: *
016: * 1. Redistributions of source code must retain the above copyright
017: * notice, this list of conditions and the following disclaimer.
018: *
019: * 2. Redistributions in binary form must reproduce the above copyright
020: * notice, this list of conditions and the following disclaimer in
021: * the documentation and/or other materials provided with the
022: * distribution.
023: *
024: * 3. The end-user documentation included with the redistribution, if
025: * any, must include the following acknowlegement:
026: * "This product includes software developed by the
027: * Apache Software Foundation (http://www.apache.org/)."
028: * Alternately, this acknowlegement may appear in the software itself,
029: * if and wherever such third-party acknowlegements normally appear.
030: *
031: * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software
032: * Foundation" must not be used to endorse or promote products derived
033: * from this software without prior written permission. For written
034: * permission, please contact apache@apache.org.
035: *
036: * 5. Products derived from this software may not be called "Apache"
037: * nor may "Apache" appear in their names without prior written
038: * permission of the Apache Group.
039: *
040: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
041: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
042: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
043: * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
044: * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
045: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
046: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
047: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
048: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
049: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
050: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
051: * SUCH DAMAGE.
052: * ====================================================================
053: *
054: * This software consists of voluntary contributions made by many
055: * individuals on behalf of the Apache Software Foundation. For more
056: * information on the Apache Software Foundation, please see
057: * <http://www.apache.org/>.
058: *
059: * [Additional notices, if required by prior licensing conditions]
060: *
061: */
062:
063: package org.apache.catalina.realm;
064:
065: import java.io.File;
066: import java.security.MessageDigest;
067: import java.security.Principal;
068: import java.sql.Connection;
069: import java.sql.Driver;
070: import java.sql.PreparedStatement;
071: import java.sql.ResultSet;
072: import java.sql.SQLException;
073: import java.util.ArrayList;
074: import java.util.Properties;
075: import org.apache.catalina.Container;
076: import org.apache.catalina.Lifecycle;
077: import org.apache.catalina.LifecycleEvent;
078: import org.apache.catalina.LifecycleException;
079: import org.apache.catalina.LifecycleListener;
080: import org.apache.catalina.Logger;
081: import org.apache.catalina.Realm;
082: import org.apache.catalina.util.HexUtils;
083: import org.apache.catalina.util.LifecycleSupport;
084: import org.apache.catalina.util.StringManager;
085: import org.apache.catalina.util.Base64;
086:
087: /**
088: *
089: * Implmentation of <b>Realm</b> that works with any JDBC supported database.
090: * See the JDBCRealm.howto for more details on how to set up the database and
091: * for configuration options.
092: *
093: * <p><strong>TODO</strong> - Support connection pooling (including message
094: * format objects) so that <code>authenticate()</code> does not have to be
095: * synchronized.</p>
096: *
097: * @author Craig R. McClanahan
098: * @author Carson McDonald
099: * @author Ignacio Ortega
100: * @version $Revision: 1.21 $ $Date: 2002/06/09 02:19:43 $
101: */
102:
103: public class JDBCRealm extends RealmBase {
104:
105: // ----------------------------------------------------- Instance Variables
106:
107: /**
108: * The connection username to use when trying to connect to the database.
109: */
110: protected String connectionName = null;
111:
112: /**
113: * The connection URL to use when trying to connect to the database.
114: */
115: protected String connectionPassword = null;
116:
117: /**
118: * The connection URL to use when trying to connect to the database.
119: */
120: protected String connectionURL = null;
121:
122: /**
123: * The connection to the database.
124: */
125: protected Connection dbConnection = null;
126:
127: /**
128: * Instance of the JDBC Driver class we use as a connection factory.
129: */
130: protected Driver driver = null;
131:
132: /**
133: * The JDBC driver to use.
134: */
135: protected String driverName = null;
136:
137: /**
138: * Descriptive information about this Realm implementation.
139: */
140: protected static final String info = "org.apache.catalina.realm.JDBCRealm/1.0";
141:
142: /**
143: * Descriptive information about this Realm implementation.
144: */
145: protected static final String name = "JDBCRealm";
146:
147: /**
148: * The PreparedStatement to use for authenticating users.
149: */
150: protected PreparedStatement preparedCredentials = null;
151:
152: /**
153: * The PreparedStatement to use for identifying the roles for
154: * a specified user.
155: */
156: protected PreparedStatement preparedRoles = null;
157:
158: /**
159: * The column in the user role table that names a role
160: */
161: protected String roleNameCol = null;
162:
163: /**
164: * The string manager for this package.
165: */
166: protected static final StringManager sm = StringManager
167: .getManager(Constants.Package);
168:
169: /**
170: * The column in the user table that holds the user's credintials
171: */
172: protected String userCredCol = null;
173:
174: /**
175: * The column in the user table that holds the user's name
176: */
177: protected String userNameCol = null;
178:
179: /**
180: * The table that holds the relation between user's and roles
181: */
182: protected String userRoleTable = null;
183:
184: /**
185: * The table that holds user data.
186: */
187: protected String userTable = null;
188:
189: // ------------------------------------------------------------- Properties
190:
191: /**
192: * Return the username to use to connect to the database.
193: *
194: */
195: public String getConnectionName() {
196: return connectionName;
197: }
198:
199: /**
200: * Set the username to use to connect to the database.
201: *
202: * @param connectionName Username
203: */
204: public void setConnectionName(String connectionName) {
205: this .connectionName = connectionName;
206: }
207:
208: /**
209: * Return the password to use to connect to the database.
210: *
211: */
212: public String getConnectionPassword() {
213: return connectionPassword;
214: }
215:
216: /**
217: * Set the password to use to connect to the database.
218: *
219: * @param connectionPassword User password
220: */
221: public void setConnectionPassword(String connectionPassword) {
222: this .connectionPassword = connectionPassword;
223: }
224:
225: /**
226: * Return the URL to use to connect to the database.
227: *
228: */
229: public String getConnectionURL() {
230: return connectionURL;
231: }
232:
233: /**
234: * Set the URL to use to connect to the database.
235: *
236: * @param connectionURL The new connection URL
237: */
238: public void setConnectionURL(String connectionURL) {
239: this .connectionURL = connectionURL;
240: }
241:
242: /**
243: * Return the JDBC driver that will be used.
244: *
245: */
246: public String getDriverName() {
247: return driverName;
248: }
249:
250: /**
251: * Set the JDBC driver that will be used.
252: *
253: * @param driverName The driver name
254: */
255: public void setDriverName(String driverName) {
256: this .driverName = driverName;
257: }
258:
259: /**
260: * Return the column in the user role table that names a role.
261: *
262: */
263: public String getRoleNameCol() {
264: return roleNameCol;
265: }
266:
267: /**
268: * Set the column in the user role table that names a role.
269: *
270: * @param roleNameCol The column name
271: */
272: public void setRoleNameCol(String roleNameCol) {
273: this .roleNameCol = roleNameCol;
274: }
275:
276: /**
277: * Return the column in the user table that holds the user's credentials.
278: *
279: */
280: public String getUserCredCol() {
281: return userCredCol;
282: }
283:
284: /**
285: * Set the column in the user table that holds the user's credentials.
286: *
287: * @param userCredCol The column name
288: */
289: public void setUserCredCol(String userCredCol) {
290: this .userCredCol = userCredCol;
291: }
292:
293: /**
294: * Return the column in the user table that holds the user's name.
295: *
296: */
297: public String getUserNameCol() {
298: return userNameCol;
299: }
300:
301: /**
302: * Set the column in the user table that holds the user's name.
303: *
304: * @param userNameCol The column name
305: */
306: public void setUserNameCol(String userNameCol) {
307: this .userNameCol = userNameCol;
308: }
309:
310: /**
311: * Return the table that holds the relation between user's and roles.
312: *
313: */
314: public String getUserRoleTable() {
315: return userRoleTable;
316: }
317:
318: /**
319: * Set the table that holds the relation between user's and roles.
320: *
321: * @param userRoleTable The table name
322: */
323: public void setUserRoleTable(String userRoleTable) {
324: this .userRoleTable = userRoleTable;
325: }
326:
327: /**
328: * Return the table that holds user data..
329: *
330: */
331: public String getUserTable() {
332: return userTable;
333: }
334:
335: /**
336: * Set the table that holds user data.
337: *
338: * @param userTable The table name
339: */
340: public void setUserTable(String userTable) {
341: this .userTable = userTable;
342: }
343:
344: // --------------------------------------------------------- Public Methods
345:
346: /**
347: * Return the Principal associated with the specified username and
348: * credentials, if there is one; otherwise return <code>null</code>.
349: *
350: * If there are any errors with the JDBC connection, executing
351: * the query or anything we return null (don't authenticate). This
352: * event is also logged, and the connection will be closed so that
353: * a subsequent request will automatically re-open it.
354: *
355: * @param username Username of the Principal to look up
356: * @param credentials Password or other credentials to use in
357: * authenticating this username
358: */
359: public Principal authenticate(String username, String credentials) {
360:
361: Connection dbConnection = null;
362:
363: try {
364:
365: // Ensure that we have an open database connection
366: dbConnection = open();
367:
368: // Acquire a Principal object for this user
369: Principal principal = authenticate(dbConnection, username,
370: credentials);
371:
372: // Release the database connection we just used
373: release(dbConnection);
374:
375: // Return the Principal (if any)
376: return (principal);
377:
378: } catch (SQLException e) {
379:
380: // Log the problem for posterity
381: log(sm.getString("jdbcRealm.exception"), e);
382:
383: // Close the connection so that it gets reopened next time
384: if (dbConnection != null)
385: close(dbConnection);
386:
387: // Return "not authenticated" for this request
388: return (null);
389:
390: }
391:
392: }
393:
394: // -------------------------------------------------------- Package Methods
395:
396: // ------------------------------------------------------ Protected Methods
397:
398: /**
399: * Return the Principal associated with the specified username and
400: * credentials, if there is one; otherwise return <code>null</code>.
401: *
402: * @param dbConnection The database connection to be used
403: * @param username Username of the Principal to look up
404: * @param credentials Password or other credentials to use in
405: * authenticating this username
406: *
407: * @exception SQLException if a database error occurs
408: */
409: public synchronized Principal authenticate(Connection dbConnection,
410: String username, String credentials) throws SQLException {
411:
412: // Look up the user's credentials
413: String dbCredentials = null;
414: PreparedStatement stmt = credentials(dbConnection, username);
415: ResultSet rs = stmt.executeQuery();
416: while (rs.next()) {
417: dbCredentials = rs.getString(1).trim();
418: }
419: rs.close();
420: if (dbCredentials == null) {
421: return (null);
422: }
423:
424: // Validate the user's credentials
425: boolean validated = false;
426: if (hasMessageDigest()) {
427: // Hex hashes should be compared case-insensitive
428: validated = (digest(credentials)
429: .equalsIgnoreCase(dbCredentials));
430: } else
431: validated = (digest(credentials).equals(dbCredentials));
432:
433: if (validated) {
434: if (debug >= 2)
435: log(sm.getString("jdbcRealm.authenticateSuccess",
436: username));
437: } else {
438: if (debug >= 2)
439: log(sm.getString("jdbcRealm.authenticateFailure",
440: username));
441: return (null);
442: }
443:
444: // Accumulate the user's roles
445: ArrayList list = new ArrayList();
446: stmt = roles(dbConnection, username);
447: rs = stmt.executeQuery();
448: while (rs.next()) {
449: list.add(rs.getString(1).trim());
450: }
451: rs.close();
452: dbConnection.commit();
453:
454: // Create and return a suitable Principal for this user
455: return (new GenericPrincipal(this , username, credentials, list));
456:
457: }
458:
459: /**
460: * Close the specified database connection.
461: *
462: * @param dbConnection The connection to be closed
463: */
464: protected void close(Connection dbConnection) {
465:
466: // Do nothing if the database connection is already closed
467: if (dbConnection == null)
468: return;
469:
470: // Close our prepared statements (if any)
471: try {
472: preparedCredentials.close();
473: } catch (Throwable f) {
474: ;
475: }
476: try {
477: preparedRoles.close();
478: } catch (Throwable f) {
479: ;
480: }
481:
482: // Close this database connection, and log any errors
483: try {
484: dbConnection.close();
485: } catch (SQLException e) {
486: log(sm.getString("jdbcRealm.close"), e); // Just log it here
487: }
488:
489: // Release resources associated with the closed connection
490: this .dbConnection = null;
491: this .preparedCredentials = null;
492: this .preparedRoles = null;
493:
494: }
495:
496: /**
497: * Return a PreparedStatement configured to perform the SELECT required
498: * to retrieve user credentials for the specified username.
499: *
500: * @param dbConnection The database connection to be used
501: * @param username Username for which credentials should be retrieved
502: *
503: * @exception SQLException if a database error occurs
504: */
505: protected PreparedStatement credentials(Connection dbConnection,
506: String username) throws SQLException {
507:
508: if (preparedCredentials == null) {
509: StringBuffer sb = new StringBuffer("SELECT ");
510: sb.append(userCredCol);
511: sb.append(" FROM ");
512: sb.append(userTable);
513: sb.append(" WHERE ");
514: sb.append(userNameCol);
515: sb.append(" = ?");
516: preparedCredentials = dbConnection.prepareStatement(sb
517: .toString());
518: }
519:
520: preparedCredentials.setString(1, username);
521: return (preparedCredentials);
522:
523: }
524:
525: /**
526: * Return a short name for this Realm implementation.
527: */
528: protected String getName() {
529:
530: return (this .name);
531:
532: }
533:
534: /**
535: * Return the password associated with the given principal's user name.
536: */
537: protected String getPassword(String username) {
538:
539: return (null);
540:
541: }
542:
543: /**
544: * Return the Principal associated with the given user name.
545: */
546: protected Principal getPrincipal(String username) {
547:
548: return (null);
549:
550: }
551:
552: /**
553: * Open (if necessary) and return a database connection for use by
554: * this Realm.
555: *
556: * @exception SQLException if a database error occurs
557: */
558: protected Connection open() throws SQLException {
559:
560: // Do nothing if there is a database connection already open
561: if (dbConnection != null)
562: return (dbConnection);
563:
564: // Instantiate our database driver if necessary
565: if (driver == null) {
566: try {
567: Class clazz = Class.forName(driverName);
568: driver = (Driver) clazz.newInstance();
569: } catch (Throwable e) {
570: throw new SQLException(e.getMessage());
571: }
572: }
573:
574: // Open a new connection
575: Properties props = new Properties();
576: if (connectionName != null)
577: props.put("user", connectionName);
578: if (connectionPassword != null)
579: props.put("password", connectionPassword);
580: dbConnection = driver.connect(connectionURL, props);
581: dbConnection.setAutoCommit(false);
582: return (dbConnection);
583:
584: }
585:
586: /**
587: * Release our use of this connection so that it can be recycled.
588: *
589: * @param dbConnnection The connection to be released
590: */
591: protected void release(Connection dbConnection) {
592:
593: ; // NO-OP since we are not pooling anything
594:
595: }
596:
597: /**
598: * Return a PreparedStatement configured to perform the SELECT required
599: * to retrieve user roles for the specified username.
600: *
601: * @param dbConnection The database connection to be used
602: * @param username Username for which roles should be retrieved
603: *
604: * @exception SQLException if a database error occurs
605: */
606: protected PreparedStatement roles(Connection dbConnection,
607: String username) throws SQLException {
608:
609: if (preparedRoles == null) {
610: StringBuffer sb = new StringBuffer("SELECT ");
611: sb.append(roleNameCol);
612: sb.append(" FROM ");
613: sb.append(userRoleTable);
614: sb.append(" WHERE ");
615: sb.append(userNameCol);
616: sb.append(" = ?");
617: preparedRoles = dbConnection
618: .prepareStatement(sb.toString());
619: }
620:
621: preparedRoles.setString(1, username);
622: return (preparedRoles);
623:
624: }
625:
626: // ------------------------------------------------------ Lifecycle Methods
627:
628: /**
629: *
630: * Prepare for active use of the public methods of this Component.
631: *
632: * @exception LifecycleException if this component detects a fatal error
633: * that prevents it from being started
634: */
635: public void start() throws LifecycleException {
636:
637: // Validate that we can open our connection
638: try {
639: open();
640: } catch (SQLException e) {
641: throw new LifecycleException(
642: sm.getString("jdbcRealm.open"), e);
643: }
644:
645: // Perform normal superclass initialization
646: super .start();
647:
648: }
649:
650: /**
651: * Gracefully shut down active use of the public methods of this Component.
652: *
653: * @exception LifecycleException if this component detects a fatal error
654: * that needs to be reported
655: */
656: public void stop() throws LifecycleException {
657:
658: // Perform normal superclass finalization
659: super .stop();
660:
661: // Close any open DB connection
662: close(this.dbConnection);
663:
664: }
665:
666: }
|