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