MedRecDBMSPlugin.java
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 = (CustomDBMSAuthenticatorMBeanmBean;
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(Stringgroups.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[inull;
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((StringgrpItr.next()))
514           return true;
515       }
516     else {
517       for (Iterator grpItr = groups.iterator(); grpItr.hasNext();) {
518         if (member.equalsIgnoreCase((StringgrpItr.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(Stringgroups.elementAt(i);
568         if (isDebugEnabled()) {
569           logDebug("   " + retVal[i]);
570         }
571       }
572     }
573     return retVal;
574   }
575 
576 }