001: /*
002: * Copyright 1999,2004 The Apache Software Foundation.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package org.apache.catalina.realm;
018:
019: import java.security.Principal;
020: import java.sql.Connection;
021: import java.sql.Driver;
022: import java.sql.PreparedStatement;
023: import java.sql.ResultSet;
024: import java.sql.SQLException;
025: import java.util.ArrayList;
026: import java.util.Properties;
027:
028: import org.apache.catalina.LifecycleException;
029: import org.apache.catalina.util.StringManager;
030:
031: /**
032: *
033: * Implmentation of <b>Realm</b> that works with any JDBC supported database.
034: * See the JDBCRealm.howto for more details on how to set up the database and
035: * for configuration options.
036: *
037: * <p><strong>TODO</strong> - Support connection pooling (including message
038: * format objects) so that <code>authenticate()</code> does not have to be
039: * synchronized and would fix the ugly connection logic. </p>
040: *
041: * @author Craig R. McClanahan
042: * @author Carson McDonald
043: * @author Ignacio Ortega
044: * @version $Revision: 1.7 $ $Date: 2004/05/26 15:51:08 $
045: */
046:
047: public class JDBCRealm extends RealmBase {
048:
049: // ----------------------------------------------------- Instance Variables
050:
051: /**
052: * The connection username to use when trying to connect to the database.
053: */
054: protected String connectionName = null;
055:
056: /**
057: * The connection URL to use when trying to connect to the database.
058: */
059: protected String connectionPassword = null;
060:
061: /**
062: * The connection URL to use when trying to connect to the database.
063: */
064: protected String connectionURL = null;
065:
066: /**
067: * The connection to the database.
068: */
069: protected Connection dbConnection = null;
070:
071: /**
072: * Instance of the JDBC Driver class we use as a connection factory.
073: */
074: protected Driver driver = null;
075:
076: /**
077: * The JDBC driver to use.
078: */
079: protected String driverName = null;
080:
081: /**
082: * Descriptive information about this Realm implementation.
083: */
084: protected static final String info = "org.apache.catalina.realm.JDBCRealm/1.0";
085:
086: /**
087: * Descriptive information about this Realm implementation.
088: */
089: protected static final String name = "JDBCRealm";
090:
091: /**
092: * The PreparedStatement to use for authenticating users.
093: */
094: protected PreparedStatement preparedCredentials = null;
095:
096: /**
097: * The PreparedStatement to use for identifying the roles for
098: * a specified user.
099: */
100: protected PreparedStatement preparedRoles = null;
101:
102: /**
103: * The column in the user role table that names a role
104: */
105: protected String roleNameCol = null;
106:
107: /**
108: * The string manager for this package.
109: */
110: protected static final StringManager sm = StringManager
111: .getManager(Constants.Package);
112:
113: /**
114: * The column in the user table that holds the user's credintials
115: */
116: protected String userCredCol = null;
117:
118: /**
119: * The column in the user table that holds the user's name
120: */
121: protected String userNameCol = null;
122:
123: /**
124: * The table that holds the relation between user's and roles
125: */
126: protected String userRoleTable = null;
127:
128: /**
129: * The table that holds user data.
130: */
131: protected String userTable = null;
132:
133: // ------------------------------------------------------------- Properties
134:
135: /**
136: * Return the username to use to connect to the database.
137: *
138: */
139: public String getConnectionName() {
140: return connectionName;
141: }
142:
143: /**
144: * Set the username to use to connect to the database.
145: *
146: * @param connectionName Username
147: */
148: public void setConnectionName(String connectionName) {
149: this .connectionName = connectionName;
150: }
151:
152: /**
153: * Return the password to use to connect to the database.
154: *
155: */
156: public String getConnectionPassword() {
157: return connectionPassword;
158: }
159:
160: /**
161: * Set the password to use to connect to the database.
162: *
163: * @param connectionPassword User password
164: */
165: public void setConnectionPassword(String connectionPassword) {
166: this .connectionPassword = connectionPassword;
167: }
168:
169: /**
170: * Return the URL to use to connect to the database.
171: *
172: */
173: public String getConnectionURL() {
174: return connectionURL;
175: }
176:
177: /**
178: * Set the URL to use to connect to the database.
179: *
180: * @param connectionURL The new connection URL
181: */
182: public void setConnectionURL(String connectionURL) {
183: this .connectionURL = connectionURL;
184: }
185:
186: /**
187: * Return the JDBC driver that will be used.
188: *
189: */
190: public String getDriverName() {
191: return driverName;
192: }
193:
194: /**
195: * Set the JDBC driver that will be used.
196: *
197: * @param driverName The driver name
198: */
199: public void setDriverName(String driverName) {
200: this .driverName = driverName;
201: }
202:
203: /**
204: * Return the column in the user role table that names a role.
205: *
206: */
207: public String getRoleNameCol() {
208: return roleNameCol;
209: }
210:
211: /**
212: * Set the column in the user role table that names a role.
213: *
214: * @param roleNameCol The column name
215: */
216: public void setRoleNameCol(String roleNameCol) {
217: this .roleNameCol = roleNameCol;
218: }
219:
220: /**
221: * Return the column in the user table that holds the user's credentials.
222: *
223: */
224: public String getUserCredCol() {
225: return userCredCol;
226: }
227:
228: /**
229: * Set the column in the user table that holds the user's credentials.
230: *
231: * @param userCredCol The column name
232: */
233: public void setUserCredCol(String userCredCol) {
234: this .userCredCol = userCredCol;
235: }
236:
237: /**
238: * Return the column in the user table that holds the user's name.
239: *
240: */
241: public String getUserNameCol() {
242: return userNameCol;
243: }
244:
245: /**
246: * Set the column in the user table that holds the user's name.
247: *
248: * @param userNameCol The column name
249: */
250: public void setUserNameCol(String userNameCol) {
251: this .userNameCol = userNameCol;
252: }
253:
254: /**
255: * Return the table that holds the relation between user's and roles.
256: *
257: */
258: public String getUserRoleTable() {
259: return userRoleTable;
260: }
261:
262: /**
263: * Set the table that holds the relation between user's and roles.
264: *
265: * @param userRoleTable The table name
266: */
267: public void setUserRoleTable(String userRoleTable) {
268: this .userRoleTable = userRoleTable;
269: }
270:
271: /**
272: * Return the table that holds user data..
273: *
274: */
275: public String getUserTable() {
276: return userTable;
277: }
278:
279: /**
280: * Set the table that holds user data.
281: *
282: * @param userTable The table name
283: */
284: public void setUserTable(String userTable) {
285: this .userTable = userTable;
286: }
287:
288: // --------------------------------------------------------- Public Methods
289:
290: /**
291: * Return the Principal associated with the specified username and
292: * credentials, if there is one; otherwise return <code>null</code>.
293: *
294: * If there are any errors with the JDBC connection, executing
295: * the query or anything we return null (don't authenticate). This
296: * event is also logged, and the connection will be closed so that
297: * a subsequent request will automatically re-open it.
298: *
299: *
300: * @param username Username of the Principal to look up
301: * @param credentials Password or other credentials to use in
302: * authenticating this username
303: */
304: public synchronized Principal authenticate(String username,
305: String credentials) {
306:
307: // Number of tries is the numebr of attempts to connect to the database
308: // during this login attempt (if we need to open the database)
309: // This needs rewritten wuth better pooling support, the existing code
310: // needs signature changes since the Prepared statements needs cached
311: // with the connections.
312: // The code below will try twice if there is a SQLException so the
313: // connection may try to be opened again. On normal conditions (including
314: // invalid login - the above is only used once.
315: int numberOfTries = 2;
316: while (numberOfTries > 0) {
317: try {
318:
319: // Ensure that we have an open database connection
320: open();
321:
322: // Acquire a Principal object for this user
323: Principal principal = authenticate(dbConnection,
324: username, credentials);
325:
326: // Return the Principal (if any)
327: return (principal);
328:
329: } catch (SQLException e) {
330:
331: // Log the problem for posterity
332: log(sm.getString("jdbcRealm.exception"), e);
333:
334: // Close the connection so that it gets reopened next time
335: if (dbConnection != null)
336: close(dbConnection);
337:
338: }
339:
340: numberOfTries--;
341: }
342:
343: // Worst case scenario
344: return null;
345:
346: }
347:
348: // -------------------------------------------------------- Package Methods
349:
350: // ------------------------------------------------------ Protected Methods
351:
352: /**
353: * Return the Principal associated with the specified username and
354: * credentials, if there is one; otherwise return <code>null</code>.
355: *
356: * @param dbConnection The database connection to be used
357: * @param username Username of the Principal to look up
358: * @param credentials Password or other credentials to use in
359: * authenticating this username
360: *
361: * @exception SQLException if a database error occurs
362: */
363: public synchronized Principal authenticate(Connection dbConnection,
364: String username, String credentials) throws SQLException {
365:
366: // Look up the user's credentials
367: String dbCredentials = null;
368: PreparedStatement stmt = null;
369: ResultSet rs = null;
370:
371: try {
372: stmt = credentials(dbConnection, username);
373: rs = stmt.executeQuery();
374:
375: if (rs.next()) {
376: dbCredentials = rs.getString(1);
377: }
378: rs.close();
379: rs = null;
380: if (dbCredentials == null) {
381: return (null);
382: }
383:
384: dbCredentials = dbCredentials.trim();
385:
386: // Validate the user's credentials
387: boolean validated = false;
388: if (hasMessageDigest()) {
389: // Hex hashes should be compared case-insensitive
390: validated = (digest(credentials)
391: .equalsIgnoreCase(dbCredentials));
392: } else {
393: validated = (digest(credentials).equals(dbCredentials));
394: }
395:
396: if (validated) {
397: if (debug >= 2)
398: log(sm.getString("jdbcRealm.authenticateSuccess",
399: username));
400: } else {
401: if (debug >= 2)
402: log(sm.getString("jdbcRealm.authenticateFailure",
403: username));
404: return (null);
405: }
406:
407: // Accumulate the user's roles
408: ArrayList roleList = new ArrayList();
409: stmt = roles(dbConnection, username);
410: rs = stmt.executeQuery();
411: while (rs.next()) {
412: String role = rs.getString(1);
413: if (null != role) {
414: roleList.add(role.trim());
415: }
416: }
417: rs.close();
418: rs = null;
419:
420: // Create and return a suitable Principal for this user
421: return (new GenericPrincipal(this , username, credentials,
422: roleList));
423: } finally {
424: if (rs != null) {
425: try {
426: rs.close();
427: } catch (SQLException e) {
428: log(sm
429: .getString("jdbcRealm.abnormalCloseResultSet"));
430: }
431: }
432: dbConnection.commit();
433: }
434:
435: }
436:
437: /**
438: * Close the specified database connection.
439: *
440: * @param dbConnection The connection to be closed
441: */
442: protected void close(Connection dbConnection) {
443:
444: // Do nothing if the database connection is already closed
445: if (dbConnection == null)
446: return;
447:
448: // Close our prepared statements (if any)
449: try {
450: preparedCredentials.close();
451: } catch (Throwable f) {
452: ;
453: }
454: this .preparedCredentials = null;
455:
456: try {
457: preparedRoles.close();
458: } catch (Throwable f) {
459: ;
460: }
461: this .preparedRoles = null;
462:
463: // Close this database connection, and log any errors
464: try {
465: dbConnection.close();
466: } catch (SQLException e) {
467: log(sm.getString("jdbcRealm.close"), e); // Just log it here
468: } finally {
469: this .dbConnection = null;
470: }
471:
472: }
473:
474: /**
475: * Return a PreparedStatement configured to perform the SELECT required
476: * to retrieve user credentials for the specified username.
477: *
478: * @param dbConnection The database connection to be used
479: * @param username Username for which credentials should be retrieved
480: *
481: * @exception SQLException if a database error occurs
482: */
483: protected PreparedStatement credentials(Connection dbConnection,
484: String username) throws SQLException {
485:
486: if (preparedCredentials == null) {
487: StringBuffer sb = new StringBuffer("SELECT ");
488: sb.append(userCredCol);
489: sb.append(" FROM ");
490: sb.append(userTable);
491: sb.append(" WHERE ");
492: sb.append(userNameCol);
493: sb.append(" = ?");
494: preparedCredentials = dbConnection.prepareStatement(sb
495: .toString());
496: }
497:
498: if (username == null) {
499: preparedCredentials.setNull(1, java.sql.Types.VARCHAR);
500: } else {
501: preparedCredentials.setString(1, username);
502: }
503:
504: return (preparedCredentials);
505: }
506:
507: /**
508: * Return a short name for this Realm implementation.
509: */
510: protected String getName() {
511:
512: return (name);
513:
514: }
515:
516: /**
517: * Return the password associated with the given principal's user name.
518: */
519: protected String getPassword(String username) {
520:
521: return (null);
522:
523: }
524:
525: /**
526: * Return the Principal associated with the given user name.
527: */
528: protected Principal getPrincipal(String username) {
529:
530: return (null);
531:
532: }
533:
534: /**
535: * Open (if necessary) and return a database connection for use by
536: * this Realm.
537: *
538: * @exception SQLException if a database error occurs
539: */
540: protected Connection open() throws SQLException {
541:
542: // Do nothing if there is a database connection already open
543: if (dbConnection != null)
544: return (dbConnection);
545:
546: // Instantiate our database driver if necessary
547: if (driver == null) {
548: try {
549: Class clazz = Class.forName(driverName);
550: driver = (Driver) clazz.newInstance();
551: } catch (Throwable e) {
552: throw new SQLException(e.getMessage());
553: }
554: }
555:
556: // Open a new connection
557: Properties props = new Properties();
558: if (connectionName != null)
559: props.put("user", connectionName);
560: if (connectionPassword != null)
561: props.put("password", connectionPassword);
562: dbConnection = driver.connect(connectionURL, props);
563: dbConnection.setAutoCommit(false);
564: return (dbConnection);
565:
566: }
567:
568: /**
569: * Release our use of this connection so that it can be recycled.
570: *
571: * @param dbConnection The connection to be released
572: */
573: protected void release(Connection dbConnection) {
574:
575: ; // NO-OP since we are not pooling anything
576:
577: }
578:
579: /**
580: * Return a PreparedStatement configured to perform the SELECT required
581: * to retrieve user roles for the specified username.
582: *
583: * @param dbConnection The database connection to be used
584: * @param username Username for which roles should be retrieved
585: *
586: * @exception SQLException if a database error occurs
587: */
588: protected PreparedStatement roles(Connection dbConnection,
589: String username) throws SQLException {
590:
591: if (preparedRoles == null) {
592: StringBuffer sb = new StringBuffer("SELECT ");
593: sb.append(roleNameCol);
594: sb.append(" FROM ");
595: sb.append(userRoleTable);
596: sb.append(" WHERE ");
597: sb.append(userNameCol);
598: sb.append(" = ?");
599: preparedRoles = dbConnection
600: .prepareStatement(sb.toString());
601: }
602:
603: preparedRoles.setString(1, username);
604: return (preparedRoles);
605:
606: }
607:
608: // ------------------------------------------------------ Lifecycle Methods
609:
610: /**
611: *
612: * Prepare for active use of the public methods of this Component.
613: *
614: * @exception LifecycleException if this component detects a fatal error
615: * that prevents it from being started
616: */
617: public void start() throws LifecycleException {
618:
619: // Validate that we can open our connection - but let tomcat
620: // startup in case the database is temporarily unavailable
621: try {
622: open();
623: } catch (SQLException e) {
624: log(sm.getString("jdbcRealm.open"), e);
625: }
626:
627: // Perform normal superclass initialization
628: super .start();
629:
630: }
631:
632: /**
633: * Gracefully shut down active use of the public methods of this Component.
634: *
635: * @exception LifecycleException if this component detects a fatal error
636: * that needs to be reported
637: */
638: public void stop() throws LifecycleException {
639:
640: // Perform normal superclass finalization
641: super .stop();
642:
643: // Close any open DB connection
644: close(this.dbConnection);
645:
646: }
647:
648: }
|