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.PreparedStatement;
023: import java.sql.ResultSet;
024: import java.sql.SQLException;
025: import java.util.ArrayList;
026:
027: import javax.naming.Context;
028: import javax.sql.DataSource;
029:
030: import org.apache.naming.ContextBindings;
031: import org.apache.catalina.LifecycleException;
032: import org.apache.catalina.ServerFactory;
033: import org.apache.catalina.core.StandardServer;
034: import org.apache.catalina.util.StringManager;
035:
036: /**
037: *
038: * Implmentation of <b>Realm</b> that works with any JDBC JNDI DataSource.
039: * See the JDBCRealm.howto for more details on how to set up the database and
040: * for configuration options.
041: *
042: * @author Glenn L. Nielsen
043: * @author Craig R. McClanahan
044: * @author Carson McDonald
045: * @author Ignacio Ortega
046: * @version $Revision: 543691 $
047: */
048:
049: public class DataSourceRealm extends RealmBase {
050:
051: // ----------------------------------------------------- Instance Variables
052:
053: /**
054: * The generated string for the roles PreparedStatement
055: */
056: private String preparedRoles = null;
057:
058: /**
059: * The generated string for the credentials PreparedStatement
060: */
061: private String preparedCredentials = null;
062:
063: /**
064: * The name of the JNDI JDBC DataSource
065: */
066: protected String dataSourceName = null;
067:
068: /**
069: * Descriptive information about this Realm implementation.
070: */
071: protected static final String info = "org.apache.catalina.realm.DataSourceRealm/1.0";
072:
073: /**
074: * Context local datasource.
075: */
076: protected boolean localDataSource = false;
077:
078: /**
079: * Descriptive information about this Realm implementation.
080: */
081: protected static final String name = "DataSourceRealm";
082:
083: /**
084: * The column in the user role table that names a role
085: */
086: protected String roleNameCol = null;
087:
088: /**
089: * The string manager for this package.
090: */
091: protected static final StringManager sm = StringManager
092: .getManager(Constants.Package);
093:
094: /**
095: * The column in the user table that holds the user's credintials
096: */
097: protected String userCredCol = null;
098:
099: /**
100: * The column in the user table that holds the user's name
101: */
102: protected String userNameCol = null;
103:
104: /**
105: * The table that holds the relation between user's and roles
106: */
107: protected String userRoleTable = null;
108:
109: /**
110: * The table that holds user data.
111: */
112: protected String userTable = null;
113:
114: // ------------------------------------------------------------- Properties
115:
116: /**
117: * Return the name of the JNDI JDBC DataSource.
118: *
119: */
120: public String getDataSourceName() {
121: return dataSourceName;
122: }
123:
124: /**
125: * Set the name of the JNDI JDBC DataSource.
126: *
127: * @param dataSourceName the name of the JNDI JDBC DataSource
128: */
129: public void setDataSourceName(String dataSourceName) {
130: this .dataSourceName = dataSourceName;
131: }
132:
133: /**
134: * Return if the datasource will be looked up in the webapp JNDI Context.
135: */
136: public boolean getLocalDataSource() {
137: return localDataSource;
138: }
139:
140: /**
141: * Set to true to cause the datasource to be looked up in the webapp JNDI
142: * Context.
143: *
144: * @param localDataSource the new flag value
145: */
146: public void setLocalDataSource(boolean localDataSource) {
147: this .localDataSource = localDataSource;
148: }
149:
150: /**
151: * Return the column in the user role table that names a role.
152: *
153: */
154: public String getRoleNameCol() {
155: return roleNameCol;
156: }
157:
158: /**
159: * Set the column in the user role table that names a role.
160: *
161: * @param roleNameCol The column name
162: */
163: public void setRoleNameCol(String roleNameCol) {
164: this .roleNameCol = roleNameCol;
165: }
166:
167: /**
168: * Return the column in the user table that holds the user's credentials.
169: *
170: */
171: public String getUserCredCol() {
172: return userCredCol;
173: }
174:
175: /**
176: * Set the column in the user table that holds the user's credentials.
177: *
178: * @param userCredCol The column name
179: */
180: public void setUserCredCol(String userCredCol) {
181: this .userCredCol = userCredCol;
182: }
183:
184: /**
185: * Return the column in the user table that holds the user's name.
186: *
187: */
188: public String getUserNameCol() {
189: return userNameCol;
190: }
191:
192: /**
193: * Set the column in the user table that holds the user's name.
194: *
195: * @param userNameCol The column name
196: */
197: public void setUserNameCol(String userNameCol) {
198: this .userNameCol = userNameCol;
199: }
200:
201: /**
202: * Return the table that holds the relation between user's and roles.
203: *
204: */
205: public String getUserRoleTable() {
206: return userRoleTable;
207: }
208:
209: /**
210: * Set the table that holds the relation between user's and roles.
211: *
212: * @param userRoleTable The table name
213: */
214: public void setUserRoleTable(String userRoleTable) {
215: this .userRoleTable = userRoleTable;
216: }
217:
218: /**
219: * Return the table that holds user data..
220: *
221: */
222: public String getUserTable() {
223: return userTable;
224: }
225:
226: /**
227: * Set the table that holds user data.
228: *
229: * @param userTable The table name
230: */
231: public void setUserTable(String userTable) {
232: this .userTable = userTable;
233: }
234:
235: // --------------------------------------------------------- Public Methods
236:
237: /**
238: * Return the Principal associated with the specified username and
239: * credentials, if there is one; otherwise return <code>null</code>.
240: *
241: * If there are any errors with the JDBC connection, executing
242: * the query or anything we return null (don't authenticate). This
243: * event is also logged, and the connection will be closed so that
244: * a subsequent request will automatically re-open it.
245: *
246: * @param username Username of the Principal to look up
247: * @param credentials Password or other credentials to use in
248: * authenticating this username
249: */
250: public Principal authenticate(String username, String credentials) {
251:
252: // No user - can't possibly authenticate, don't bother the database then
253: if (username == null) {
254: return null;
255: }
256:
257: Connection dbConnection = null;
258:
259: try {
260:
261: // Ensure that we have an open database connection
262: dbConnection = open();
263: if (dbConnection == null) {
264: // If the db connection open fails, return "not authenticated"
265: return null;
266: }
267:
268: // Acquire a Principal object for this user
269: return authenticate(dbConnection, username, credentials);
270:
271: } catch (SQLException e) {
272: // Log the problem for posterity
273: containerLog.error(sm
274: .getString("dataSourceRealm.exception"), e);
275:
276: // Return "not authenticated" for this request
277: return (null);
278:
279: } finally {
280: close(dbConnection);
281: }
282:
283: }
284:
285: // -------------------------------------------------------- Package Methods
286:
287: // ------------------------------------------------------ Protected Methods
288:
289: /**
290: * Return the Principal associated with the specified username and
291: * credentials, if there is one; otherwise return <code>null</code>.
292: *
293: * @param dbConnection The database connection to be used
294: * @param username Username of the Principal to look up
295: * @param credentials Password or other credentials to use in
296: * authenticating this username
297: */
298: protected Principal authenticate(Connection dbConnection,
299: String username, String credentials) throws SQLException {
300:
301: String dbCredentials = getPassword(dbConnection, username);
302:
303: // Validate the user's credentials
304: boolean validated = false;
305: if (hasMessageDigest()) {
306: // Hex hashes should be compared case-insensitive
307: validated = (digest(credentials)
308: .equalsIgnoreCase(dbCredentials));
309: } else
310: validated = (digest(credentials).equals(dbCredentials));
311:
312: if (validated) {
313: if (containerLog.isTraceEnabled())
314: containerLog.trace(sm
315: .getString(
316: "dataSourceRealm.authenticateSuccess",
317: username));
318: } else {
319: if (containerLog.isTraceEnabled())
320: containerLog.trace(sm
321: .getString(
322: "dataSourceRealm.authenticateFailure",
323: username));
324: return (null);
325: }
326:
327: ArrayList<String> list = getRoles(dbConnection, username);
328:
329: // Create and return a suitable Principal for this user
330: return (new GenericPrincipal(this , username, credentials, list));
331:
332: }
333:
334: /**
335: * Close the specified database connection.
336: *
337: * @param dbConnection The connection to be closed
338: */
339: protected void close(Connection dbConnection) {
340:
341: // Do nothing if the database connection is already closed
342: if (dbConnection == null)
343: return;
344:
345: // Commit if not auto committed
346: try {
347: if (!dbConnection.getAutoCommit()) {
348: dbConnection.commit();
349: }
350: } catch (SQLException e) {
351: containerLog.error(
352: "Exception committing connection before closing:",
353: e);
354: }
355:
356: // Close this database connection, and log any errors
357: try {
358: dbConnection.close();
359: } catch (SQLException e) {
360: containerLog
361: .error(sm.getString("dataSourceRealm.close"), e); // Just log it here
362: }
363:
364: }
365:
366: /**
367: * Open the specified database connection.
368: *
369: * @return Connection to the database
370: */
371: protected Connection open() {
372:
373: try {
374: Context context = null;
375: if (localDataSource) {
376: context = ContextBindings.getClassLoader();
377: context = (Context) context.lookup("comp/env");
378: } else {
379: StandardServer server = (StandardServer) ServerFactory
380: .getServer();
381: context = server.getGlobalNamingContext();
382: }
383: DataSource dataSource = (DataSource) context
384: .lookup(dataSourceName);
385: return dataSource.getConnection();
386: } catch (Exception e) {
387: // Log the problem for posterity
388: containerLog.error(sm
389: .getString("dataSourceRealm.exception"), e);
390: }
391: return null;
392: }
393:
394: /**
395: * Return a short name for this Realm implementation.
396: */
397: protected String getName() {
398:
399: return (name);
400:
401: }
402:
403: /**
404: * Return the password associated with the given principal's user name.
405: */
406: protected String getPassword(String username) {
407:
408: Connection dbConnection = null;
409:
410: // Ensure that we have an open database connection
411: dbConnection = open();
412: if (dbConnection == null) {
413: return null;
414: }
415:
416: try {
417: return getPassword(dbConnection, username);
418: } finally {
419: close(dbConnection);
420: }
421: }
422:
423: /**
424: * Return the password associated with the given principal's user name.
425: * @param dbConnection The database connection to be used
426: * @param username Username for which password should be retrieved
427: */
428: protected String getPassword(Connection dbConnection,
429: String username) {
430:
431: ResultSet rs = null;
432: PreparedStatement stmt = null;
433: String dbCredentials = null;
434:
435: try {
436: stmt = credentials(dbConnection, username);
437: rs = stmt.executeQuery();
438: if (rs.next()) {
439: dbCredentials = rs.getString(1);
440: }
441:
442: return (dbCredentials != null) ? dbCredentials.trim()
443: : null;
444:
445: } catch (SQLException e) {
446: containerLog.error(sm.getString(
447: "dataSourceRealm.getPassword.exception", username));
448: } finally {
449: try {
450: if (rs != null) {
451: rs.close();
452: }
453: if (stmt != null) {
454: stmt.close();
455: }
456: } catch (SQLException e) {
457: containerLog.error(sm.getString(
458: "dataSourceRealm.getPassword.exception",
459: username));
460:
461: }
462: }
463:
464: return null;
465: }
466:
467: /**
468: * Return the Principal associated with the given user name.
469: */
470: protected Principal getPrincipal(String username) {
471: Connection dbConnection = open();
472: if (dbConnection == null) {
473: return new GenericPrincipal(this , username, null, null);
474: }
475: try {
476: return (new GenericPrincipal(this , username, getPassword(
477: dbConnection, username), getRoles(dbConnection,
478: username)));
479: } finally {
480: close(dbConnection);
481: }
482:
483: }
484:
485: /**
486: * Return the roles associated with the given user name.
487: * @param username Username for which roles should be retrieved
488: */
489: protected ArrayList getRoles(String username) {
490:
491: Connection dbConnection = null;
492:
493: // Ensure that we have an open database connection
494: dbConnection = open();
495: if (dbConnection == null) {
496: return null;
497: }
498:
499: try {
500: return getRoles(dbConnection, username);
501: } finally {
502: close(dbConnection);
503: }
504: }
505:
506: /**
507: * Return the roles associated with the given user name
508: * @param dbConnection The database connection to be used
509: * @param username Username for which roles should be retrieved
510: */
511: protected ArrayList<String> getRoles(Connection dbConnection,
512: String username) {
513:
514: ResultSet rs = null;
515: PreparedStatement stmt = null;
516: ArrayList<String> list = null;
517:
518: try {
519: stmt = roles(dbConnection, username);
520: rs = stmt.executeQuery();
521: list = new ArrayList<String>();
522:
523: while (rs.next()) {
524: String role = rs.getString(1);
525: if (role != null) {
526: list.add(role.trim());
527: }
528: }
529: return list;
530: } catch (SQLException e) {
531: containerLog.error(sm.getString(
532: "dataSourceRealm.getRoles.exception", username));
533: } finally {
534: try {
535: if (rs != null) {
536: rs.close();
537: }
538: if (stmt != null) {
539: stmt.close();
540: }
541: } catch (SQLException e) {
542: containerLog
543: .error(sm.getString(
544: "dataSourceRealm.getRoles.exception",
545: username));
546: }
547: }
548:
549: return null;
550: }
551:
552: /**
553: * Return a PreparedStatement configured to perform the SELECT required
554: * to retrieve user credentials for the specified username.
555: *
556: * @param dbConnection The database connection to be used
557: * @param username Username for which credentials should be retrieved
558: *
559: * @exception SQLException if a database error occurs
560: */
561: private PreparedStatement credentials(Connection dbConnection,
562: String username) throws SQLException {
563:
564: PreparedStatement credentials = dbConnection
565: .prepareStatement(preparedCredentials);
566:
567: credentials.setString(1, username);
568: return (credentials);
569:
570: }
571:
572: /**
573: * Return a PreparedStatement configured to perform the SELECT required
574: * to retrieve user roles for the specified username.
575: *
576: * @param dbConnection The database connection to be used
577: * @param username Username for which roles should be retrieved
578: *
579: * @exception SQLException if a database error occurs
580: */
581: private PreparedStatement roles(Connection dbConnection,
582: String username) throws SQLException {
583:
584: PreparedStatement roles = dbConnection
585: .prepareStatement(preparedRoles);
586:
587: roles.setString(1, username);
588: return (roles);
589:
590: }
591:
592: // ------------------------------------------------------ Lifecycle Methods
593:
594: /**
595: *
596: * Prepare for active use of the public methods of this Component.
597: *
598: * @exception LifecycleException if this component detects a fatal error
599: * that prevents it from being started
600: */
601: public void start() throws LifecycleException {
602:
603: // Perform normal superclass initialization
604: super .start();
605:
606: // Create the roles PreparedStatement string
607: StringBuffer temp = new StringBuffer("SELECT ");
608: temp.append(roleNameCol);
609: temp.append(" FROM ");
610: temp.append(userRoleTable);
611: temp.append(" WHERE ");
612: temp.append(userNameCol);
613: temp.append(" = ?");
614: preparedRoles = temp.toString();
615:
616: // Create the credentials PreparedStatement string
617: temp = new StringBuffer("SELECT ");
618: temp.append(userCredCol);
619: temp.append(" FROM ");
620: temp.append(userTable);
621: temp.append(" WHERE ");
622: temp.append(userNameCol);
623: temp.append(" = ?");
624: preparedCredentials = temp.toString();
625: }
626:
627: /**
628: * Gracefully shut down active use of the public methods of this Component.
629: *
630: * @exception LifecycleException if this component detects a fatal error
631: * that needs to be reported
632: */
633: public void stop() throws LifecycleException {
634:
635: // Perform normal superclass finalization
636: super.stop();
637:
638: }
639:
640: }
|