001 package com.bea.medrec.security;
002
003 import java.sql.PreparedStatement;
004 import java.sql.ResultSet;
005 import java.sql.SQLException;
006 import java.util.Iterator;
007 import java.util.Properties;
008 import java.util.Vector;
009 import weblogic.logging.LoggingHelper;
010 import weblogic.logging.WLLevel;
011 import weblogic.management.security.ProviderMBean;
012 import weblogic.security.providers.authentication.CustomDBMSAuthenticatorMBean;
013 import weblogic.security.providers.authentication.CustomDBMSAuthenticatorPlugin;
014
015 /**
016 * Sample plugin for the CustomDBMSAuthenticatorPlugin ATN provider that matches
017 * the MedRec ATN provider SQL modified for the test system to use the same SQL
018 * that the SQL providers use.
019 *
020 * @author Copyright (c) 2006 by BEA Systems. All Rights Reserved.
021 */
022 public class MedRecDBMSPlugin implements CustomDBMSAuthenticatorPlugin {
023
024 /**
025 * Default SQL that corresponds to the MedRec sample DBMS
026 *
027 * private String GET_PASSWORD_STMT = "SELECT password FROM medrec_user
028 * WHERE username = ? AND status = 'ACTIVE'";
029 * private String PASSWORD_COLUMN = "password";
030 * private String GET_GROUPS_STMT = "SELECT group_name FROM groups groups
031 * WHERE groups.username = ?";
032 * private String GROUP_NAME_COLUMN = "group_name";
033 *
034 *
035 * Default SQL that corresponds to the WLS SQL based DBMS providers
036 *
037 * private String GET_PASSWORD_STMT = "SELECT U_PASSWORD FROM users
038 * WHERE U_NAME = ?";
039 * private String PASSWORD_COLUMN = "U_PASSWORD";
040 * private String GET_GROUPS_STMT = "SELECT G_NAME FROM groupmembers
041 * WHERE G_MEMBER = ?";
042 * private String GROUP_NAME_COLUMN = "G_NAME";
043 */
044
045 private String GET_PASSWORD_STMT = "SELECT password FROM medrec_user WHERE "+
046 "username = ? AND status = 'ACTIVE'";
047 private String PASSWORD_COLUMN = "password";
048 private String GET_GROUPS_STMT = "SELECT group_name FROM groups groups WHERE "+
049 "groups.username = ?";
050 private String GROUP_NAME_COLUMN = "group_name";
051
052 private int maxRecursionDepth = -1;
053 private boolean caseSensitive = false;
054
055 private CustomDBMSAuthenticatorMBean pluggableMBean = null;
056
057 /**
058 * Executed on initialization of the CustomDBMSAuthenticatorPlugin.
059 * @param mBean Properties configured for the plugin
060 */
061 public void initialize(ProviderMBean mBean) {
062 if (isDebugEnabled()) {
063 logDebug("MedRecDBMSPlugin.initialize() called");
064 }
065 if (mBean == null) {
066 if (isDebugEnabled()) {
067 logDebug("No mBean supplied, using default setting values");
068 }
069 return;
070 }
071 if (!(mBean instanceof CustomDBMSAuthenticatorMBean)) {
072 if (isDebugEnabled()) {
073 logDebug("Not a pluggable runtime provider MBean, using default setting values");
074 }
075 return;
076 }
077
078 pluggableMBean = (CustomDBMSAuthenticatorMBean) mBean;
079 getMBeanInfo();
080 }
081
082 /**
083 * Executed on shutdown of the authentication provider, or if the
084 * plugin is replaced dynamically at runtime with another implementation class.
085 */
086 public void shutdown() {
087 if (isDebugEnabled()) {
088 logDebug("MedRecDBMSPlugin.shutdown() called");
089 }
090 }
091
092 /**
093 * Called during authentication process to retrieve password for user.
094 *
095 * @param connection JDBC connection
096 * @param user String representing the username
097 * @return String representing the password in one of the supported formats
098 * @exception SQLException if a database access error occurs
099 */
100 public String lookupPassword(java.sql.Connection connection,
101 String user)
102 throws java.sql.SQLException {
103 if (isDebugEnabled()) {
104 logDebug("MedRecDBMSPlugin.lookupPassword() called");
105 logDebug("MedRecDBMSPlugin: Looking up user: " + user);
106 }
107 if ((connection == null) || (user == null)) {
108 if (isErrorEnabled()) {
109 logError("MedRecDBMSPlugin.lookupPassword() failure, connection or " +
110 "user was null");
111 }
112 return null;
113 }
114
115 PreparedStatement stmt = null;
116 ResultSet rs = null;
117 String password = null;
118 try {
119 if (isDebugEnabled()) {
120 logDebug("MedRecDBMSPlugin.lookupPassword() SQL is: " +
121 GET_PASSWORD_STMT);
122 }
123 stmt = connection.prepareStatement(GET_PASSWORD_STMT);
124 stmt.setString(1, user);
125 rs = stmt.executeQuery();
126 if (rs.next())
127 password = rs.getString(PASSWORD_COLUMN);
128 } catch (SQLException e) {
129 if (isWarnEnabled()) {
130 logWarn("MedRecDBMSPlugin.lookupPassword() exception", e);
131 }
132 throw e;
133 } finally {
134 cleanup(rs, stmt);
135 }
136 return password;
137 }
138
139 /**
140 * Called during Identity Assertion to verify existence of user.
141 *
142 * @param connection JDBC connection
143 * @param user String representing the username
144 * @return boolean indicating user exists or not. true if user exists,
145 * false if it doesn't
146 * @exception SQLException if a database access error occurs
147 */
148 public boolean userExists(java.sql.Connection connection,
149 String user)
150 throws java.sql.SQLException {
151 if (isDebugEnabled()) {
152 logDebug("MedRecDBMSPlugin.userExists() called");
153 }
154 if ((connection == null) || (user == null)) {
155 if (isErrorEnabled()) {
156 logError("MedRecDBMSPlugin.userExists() failure, connection or user " +
157 "was null");
158 }
159 return false;
160 }
161 String password = lookupPassword(connection, user);
162 if (password != null) {
163 if (isInfoEnabled()) {
164 logInfo("MedRecDBMSPlugin.userExists() found user " + user);
165 }
166 return true;
167 }
168
169 if (isInfoEnabled()) {
170 logInfo("MedRecDBMSPlugin.userExists() didn't find user " + user);
171 }
172 return false;
173 }
174
175 /**
176 * Called during authentication and identity assertion to determine the users
177 * group membership.
178 *
179 * @param connection Connection to the database
180 * @param user String representing the username
181 * @return an array of group strings, or null for no groups.
182 * @exception SQLException if a database access error occurs
183 */
184 public String[] lookupUserGroups(java.sql.Connection connection,
185 String user)
186 throws java.sql.SQLException {
187 if (isDebugEnabled()) {
188 logDebug("MedRecDBMSPlugin.lookupUserGroups() called");
189 }
190 if ((connection == null) || (user == null)) {
191 if (isErrorEnabled()) {
192 logError("MedRecDBMSPlugin.lookupUserGroups() failure, connection or " +
193 "user was null");
194 }
195 return null;
196 }
197
198 PreparedStatement stmt = null;
199 Vector<String> groups = new Vector<String>();
200 try {
201 if (isDebugEnabled()) {
202 logDebug("MedRecDBMSPlugin.lookupUserGroups() SQL : " + GET_GROUPS_STMT);
203 }
204 stmt = connection.prepareStatement(GET_GROUPS_STMT);
205 findMemberGroups(stmt, user, groups, 0);
206 } catch (SQLException e) {
207 if (isErrorEnabled()) {
208 logError("MedRecDBMSPlugin.lookupUserGroups() Exception!", e);
209 }
210 throw e;
211 } finally {
212 cleanup(stmt);
213 }
214
215 if (isInfoEnabled()) {
216 logInfo("MedRecDBMSPlugin.lookupUserGroups() found " + groups.size() +
217 " groups:");
218 }
219 String[] retVal = null;
220 if (groups.size() > 0) {
221 retVal = new String[groups.size()];
222 for (int i = 0; i < groups.size(); i++) {
223 retVal[i] = (String) groups.elementAt(i);
224 if (isDebugEnabled()) {
225 logDebug(" " + retVal[i]);
226 }
227 }
228 }
229 return retVal;
230 }
231
232 // Implementation specific utility class for cleaning up statements and results
233 private void cleanup(ResultSet pResultSet, PreparedStatement pStmt)
234 throws SQLException {
235 try {
236 cleanup(pResultSet);
237 } catch (SQLException e) {
238 if (isWarnEnabled()) {
239 logWarn("Cleanup failed on ResultSet", e);
240 }
241 throw e;
242 } finally {
243 cleanup(pStmt);
244 }
245 }
246
247 // Implementation specific utility class for cleaning up results
248 private void cleanup(ResultSet pResultSet)
249 throws SQLException {
250 try {
251 if (pResultSet != null) {
252 pResultSet.close();
253 pResultSet = null;
254 }
255 } catch (SQLException e) {
256 if (isWarnEnabled()) {
257 logWarn("Cleanup failed on ResultSet", e);
258 }
259 throw e;
260 }
261 }
262
263 // Implementation specific utility class for cleaning up statements
264 private void cleanup(PreparedStatement pStmt)
265 throws SQLException {
266 try {
267 if (pStmt != null) {
268 pStmt.close();
269 pStmt = null;
270 }
271 } catch (SQLException e) {
272 if (isWarnEnabled()) {
273 logWarn("Cleanup failed on Statement", e);
274 }
275 throw e;
276 }
277 }
278
279 // Implementation specific utility class for debug logging
280 private boolean isDebugEnabled() {
281 return LoggingHelper.getServerLogger().isLoggable(WLLevel.DEBUG);
282 }
283
284 // Implementation specific utility class for debug logging
285 private void logDebug(String msg) {
286 LoggingHelper.getServerLogger().log(WLLevel.DEBUG, msg);
287 }
288
289 // Implementation specific utility class for error logging
290 private boolean isErrorEnabled() {
291 return LoggingHelper.getServerLogger().isLoggable(WLLevel.ERROR);
292 }
293
294 // Implementation specific utility class for error logging
295 private void logError(String msg) {
296 LoggingHelper.getServerLogger().log(WLLevel.ERROR, msg);
297 }
298
299 // Implementation specific utility class for error logging
300 private void logError(String msg, Throwable th) {
301 LoggingHelper.getServerLogger().log(WLLevel.ERROR, msg, th);
302 }
303
304 // Implementation specific utility class for warn logging
305 private boolean isWarnEnabled() {
306 return LoggingHelper.getServerLogger().isLoggable(WLLevel.WARNING);
307 }
308
309 // Implementation specific utility class for warn logging
310 private void logWarn(String msg, Throwable th) {
311 LoggingHelper.getServerLogger().log(WLLevel.WARNING, msg, th);
312 }
313
314 // Implementation specific utility class for warn logging
315 private void logWarn(String msg) {
316 LoggingHelper.getServerLogger().log(WLLevel.WARNING, msg);
317 }
318
319 // Implementation specific utility class for general logging
320 private boolean isInfoEnabled() {
321 return LoggingHelper.getServerLogger().isLoggable(WLLevel.INFO);
322 }
323
324 // Implementation specific utility class for general logging
325 private void logInfo(String msg) {
326 LoggingHelper.getServerLogger().log(WLLevel.INFO, msg);
327 }
328
329 // Implementation specific utility to get settings from MBean
330 private void getMBeanInfo() {
331
332 // Sub plugins are not required to have any properties, but if the
333 // sub plugin has special settings that it would like to represent in
334 // the configuration, the properties are how to do that.
335 //
336 // The sample subplugin allows SQL statements and column names to be
337 // specified as Properties, so look for those
338 Properties testProperties = pluggableMBean.getPluginProperties();
339 if (testProperties != null) {
340 String testPropertyValue = null;
341
342 testPropertyValue = testProperties.getProperty("GET_PASSWORD_STMT");
343 if (testPropertyValue != null) {
344 if (isDebugEnabled()) {
345 logDebug("GET_PASSWORD_STMT = " + testPropertyValue);
346 }
347 GET_PASSWORD_STMT = testPropertyValue;
348 }
349 testPropertyValue = testProperties.getProperty("PASSWORD_COLUMN");
350 if (testPropertyValue != null) {
351 if (isDebugEnabled()) {
352 logDebug("PASSWORD_COLUMN = " + testPropertyValue);
353 }
354 PASSWORD_COLUMN = testPropertyValue;
355 }
356 testPropertyValue = testProperties.getProperty("GET_GROUPS_STMT");
357 if (testPropertyValue != null) {
358 if (isDebugEnabled()) {
359 logDebug("GET_GROUPS_STMT = " + testPropertyValue);
360 }
361 GET_GROUPS_STMT = testPropertyValue;
362 }
363 testPropertyValue = testProperties.getProperty("GROUP_NAME_COLUMN");
364 if (testPropertyValue != null) {
365 if (isDebugEnabled()) {
366 logDebug("GROUP_NAME_COLUMN = " + testPropertyValue);
367 }
368 GROUP_NAME_COLUMN = testPropertyValue;
369 }
370 } else {
371 if (isDebugEnabled()) {
372 logDebug("No properties specified for sub-plugin");
373 }
374 }
375
376 /** Sub plugins are expected to handle any group membership recursion performed
377 internally. This means that they may not handle it at all or they may
378 handle it in their own way. If they do handle group membership recursion, they
379 should make an attempt to handle the group membership recursion limits that
380 are configured (but they can ignore those settings if they choose).
381
382 The sample handles group membership recursion and honors the recursion limit
383 settings, so get those now. */
384 String testGroupMembershipSearching =
385 pluggableMBean.getGroupMembershipSearching();
386 if (testGroupMembershipSearching != null) {
387 if ("limited".equalsIgnoreCase(testGroupMembershipSearching)) {
388 int testMaxRecursionDepth =
389 pluggableMBean.getMaxGroupMembershipSearchLevel().intValue();
390 if (testMaxRecursionDepth < 0) {
391 if (isDebugEnabled()) {
392 logDebug("MaxGroupMembershipSearchLevel was invalid " +
393 testMaxRecursionDepth);
394 }
395 } else {
396 maxRecursionDepth = testMaxRecursionDepth;
397 if (isDebugEnabled()) {
398 logDebug("MaxGroupMembershipSearchLevel is " + maxRecursionDepth);
399 }
400 }
401 } else if ("unlimited".equalsIgnoreCase(testGroupMembershipSearching)) {
402 if (isDebugEnabled()) {
403 logDebug("Unlimited GroupMembershipSearching");
404 }
405 maxRecursionDepth = -1;
406 } else {
407 if (isDebugEnabled()) {
408 logDebug("Unrecognized GroupMembershipSearching setting");
409 }
410 }
411 }
412 return;
413 }
414
415 // Implementation specific utility for handling recursive group membership lookups
416 // FIXME: There needs to be more thought around how to pass along the groups
417 // efficiently. There is too much conversion going on and searching for
418 // names in an unordered vector is to check for cycles is very expensive
419 private final void findMemberGroups(PreparedStatement statement,
420 String name,
421 Vector<String> resultGroups,
422 int depth)
423 throws SQLException {
424 // Check for bad inputs
425 if (statement == null) {
426 String msg = "No SQL Statement found for retrieving groups. " +
427 "Check configuration";
428 if (isDebugEnabled()) {
429 logDebug(msg);
430 }
431 throw new SQLException(msg);
432 }
433 if ((name == null) || (resultGroups == null)) {
434 if (isDebugEnabled()) {
435 logDebug("Invalid parameters to findMemberGroups");
436 }
437 return;
438 }
439
440 if (isDebugEnabled()) {
441 logDebug("findMemberGroups for " + name);
442 }
443
444 // Lookup groups in the database
445 String[] results = null;
446 try {
447 results = queryMemberGroups(statement, name);
448 } catch (SQLException se) {
449 if (isWarnEnabled()) {
450 logWarn("SQL Exception when retrieving groups", se);
451 }
452 throw se;
453 }
454
455 if ((results == null) || (results.length == 0)) {
456 if (isInfoEnabled()) {
457 logInfo("No groups found in database for " + name);
458 }
459 return;
460 }
461
462 // Add the results to the running list
463 for (int i = 0; i < results.length; i++) {
464 if (memberAlreadyInGroups(caseSensitive, results[i], resultGroups)) {
465 if (isDebugEnabled()) {
466 logDebug("Cycle detected, not recursing further for: " + name);
467 }
468 results[i] = null;
469 continue;
470 }
471 resultGroups.add(results[i]);
472 }
473
474 // If the depth will be exceeded by more recursion stop now
475 if ((maxRecursionDepth != -1) && (depth >= maxRecursionDepth)) {
476 if(isDebugEnabled()){
477 logDebug("recursive membership search depth limit reached");
478 }
479 return;
480 }
481
482 // If we got here, then we got some new group names and we need to
483 // check their group memberships recursively
484 for (int i = 0; i < results.length; i++) {
485 if (results[i] != null)
486 findMemberGroups(statement, results[i], resultGroups, depth+1);
487 }
488 return;
489 }
490
491 private final boolean namesMatch(boolean caseSensitive,
492 String name1,
493 String name2) {
494 if (caseSensitive)
495 return name1.equals(name2);
496 return name1.equalsIgnoreCase(name2);
497 }
498
499 private final boolean memberAlreadyInGroups(boolean caseSensitive,
500 String member,
501 Vector groups) {
502 // If we don't have a member we're done, and the caller likely is too
503 if (member == null)
504 return true;
505
506 // If we have a user and no groups yet, it isn't in there
507 if ((groups == null) || (groups.size() == 0))
508 return false;
509
510 // FIXME: Really inefficient, maybe can use a hashmap or something here...
511 if (caseSensitive) {
512 for (Iterator grpItr = groups.iterator(); grpItr.hasNext();) {
513 if (member.equals((String) grpItr.next()))
514 return true;
515 }
516 } else {
517 for (Iterator grpItr = groups.iterator(); grpItr.hasNext();) {
518 if (member.equalsIgnoreCase((String) grpItr.next()))
519 return true;
520 }
521 }
522 return false;
523 }
524
525 private final String[] queryMemberGroups(PreparedStatement statement,
526 String userOrGroup)
527 throws java.sql.SQLException {
528 if (isDebugEnabled()) {
529 logDebug("MedRecDBMSPlugin.queryMemberGroups() called");
530 }
531 if ((statement == null) || (userOrGroup == null)) {
532 if (isDebugEnabled()) {
533 logDebug("MedRecDBMSPlugin.queryMemberGroups() failure, prepared " +
534 "statement or user/group was null");
535 }
536 return null;
537 }
538
539 ResultSet rs = null;
540 Vector<String> groups = new Vector<String>();
541 try {
542 if (isDebugEnabled()) {
543 logDebug("MedRecDBMSPlugin.queryMemberGroups() execute lookup statement");
544 }
545 statement.setString(1, userOrGroup);
546 rs = statement.executeQuery();
547 while (rs.next()) {
548 groups.addElement(rs.getString(GROUP_NAME_COLUMN));
549 }
550 } catch (SQLException e) {
551 if (isWarnEnabled()) {
552 logWarn("MedRecDBMSPlugin.queryMemberGroups() Exception!", e);
553 }
554 throw e;
555 } finally {
556 cleanup(rs);
557 }
558
559 if (isDebugEnabled()) {
560 logDebug("MedRecDBMSPlugin.lookupUserGroups() found " + groups.size() +
561 " groups:");
562 }
563 String[] retVal = null;
564 if (groups.size() > 0) {
565 retVal = new String[groups.size()];
566 for (int i = 0; i < groups.size(); i++) {
567 retVal[i] = (String) groups.elementAt(i);
568 if (isDebugEnabled()) {
569 logDebug(" " + retVal[i]);
570 }
571 }
572 }
573 return retVal;
574 }
575
576 }
|