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