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