Home » geronimo-2.2-source-release » org.apache.geronimo.security.realm.providers » [javadoc | source]

    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   package org.apache.geronimo.security.realm.providers;
   19   
   20   import java.io.IOException;
   21   import java.security.MessageDigest;
   22   import java.security.NoSuchAlgorithmException;
   23   import java.security.Principal;
   24   import java.sql.Connection;
   25   import java.sql.Driver;
   26   import java.sql.PreparedStatement;
   27   import java.sql.ResultSet;
   28   import java.sql.SQLException;
   29   import java.util.Arrays;
   30   import java.util.Collections;
   31   import java.util.HashSet;
   32   import java.util.List;
   33   import java.util.Map;
   34   import java.util.Properties;
   35   import java.util.Set;
   36   
   37   import javax.security.auth.Subject;
   38   import javax.security.auth.callback.Callback;
   39   import javax.security.auth.callback.CallbackHandler;
   40   import javax.security.auth.callback.NameCallback;
   41   import javax.security.auth.callback.PasswordCallback;
   42   import javax.security.auth.callback.UnsupportedCallbackException;
   43   import javax.security.auth.login.FailedLoginException;
   44   import javax.security.auth.login.LoginException;
   45   import javax.security.auth.spi.LoginModule;
   46   import javax.sql.DataSource;
   47   
   48   import org.slf4j.Logger;
   49   import org.slf4j.LoggerFactory;
   50   import org.apache.geronimo.gbean.AbstractName;
   51   import org.apache.geronimo.gbean.AbstractNameQuery;
   52   import org.apache.geronimo.kernel.GBeanNotFoundException;
   53   import org.apache.geronimo.kernel.Kernel;
   54   import org.apache.geronimo.kernel.KernelRegistry;
   55   import org.apache.geronimo.management.geronimo.JCAManagedConnectionFactory;
   56   import org.apache.geronimo.security.jaas.JaasLoginModuleUse;
   57   import org.apache.geronimo.security.jaas.WrappingLoginModule;
   58   import org.apache.geronimo.crypto.encoders.Base64;
   59   import org.apache.geronimo.crypto.encoders.HexTranslator;
   60   
   61   /**
   62    * A login module that loads security information from a SQL database.  Expects
   63    * to be run by a GenericSecurityRealm (doesn't work on its own).
   64    * <p/>
   65    * This requires database connectivity information (either 1: a dataSourceName and
   66    * optional dataSourceApplication or 2: a JDBC driver, URL, username, and password)
   67    * and 2 SQL queries.
   68    * <p/>
   69    * The userSelect query should return 2 values, the username and the password in
   70    * that order.  It should include one PreparedStatement parameter (a ?) which
   71    * will be filled in with the username.  In other words, the query should look
   72    * like: <tt>SELECT user, password FROM credentials WHERE username=?</tt>
   73    * <p/>
   74    * The groupSelect query should return 2 values, the username and the group name in
   75    * that order (but it may return multiple rows, one per group).  It should include
   76    * one PreparedStatement parameter (a ?) which will be filled in with the username.
   77    * In other words, the query should look like:
   78    * <tt>SELECT user, role FROM user_roles WHERE username=?</tt>
   79    * <p/>
   80    * This login module checks security credentials so the lifecycle methods must return true to indicate success
   81    * or throw LoginException to indicate failure.
   82    *
   83    * @version $Rev: 698367 $ $Date: 2008-09-23 14:51:48 -0700 (Tue, 23 Sep 2008) $
   84    */
   85   public class SQLLoginModule implements LoginModule {
   86       private static final Logger log = LoggerFactory.getLogger(SQLLoginModule.class);
   87       
   88       public final static String USER_SELECT = "userSelect";
   89       public final static String GROUP_SELECT = "groupSelect";
   90       public final static String CONNECTION_URL = "jdbcURL";
   91       public final static String USER = "jdbcUser";
   92       public final static String PASSWORD = "jdbcPassword";
   93       public final static String DRIVER = "jdbcDriver";
   94       public final static String DATABASE_POOL_NAME = "dataSourceName";
   95       public final static String DATABASE_POOL_APP_NAME = "dataSourceApplication";
   96       public final static String DIGEST = "digest";
   97       public final static String ENCODING = "encoding";
   98       public final static List<String> supportedOptions = Collections.unmodifiableList(Arrays.asList(USER_SELECT, GROUP_SELECT, CONNECTION_URL,
   99               USER, PASSWORD, DRIVER, DATABASE_POOL_NAME, DATABASE_POOL_APP_NAME, DIGEST, ENCODING));
  100   
  101       private String connectionURL;
  102       private Properties properties;
  103       private Driver driver;
  104       private JCAManagedConnectionFactory factory;
  105       private String userSelect;
  106       private String groupSelect;
  107       private String digest;
  108       private String encoding;
  109   
  110       private boolean loginSucceeded;
  111       private Subject subject;
  112       private CallbackHandler handler;
  113       private String cbUsername;
  114       private String cbPassword;
  115       private final Set<String> groups = new HashSet<String>();
  116       private final Set<Principal> allPrincipals = new HashSet<Principal>();
  117   
  118       public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
  119           this.subject = subject;
  120           this.handler = callbackHandler;
  121           for(Object option: options.keySet()) {
  122               if(!supportedOptions.contains(option) && !JaasLoginModuleUse.supportedOptions.contains(option)
  123                       && !WrappingLoginModule.supportedOptions.contains(option)) {
  124                   log.warn("Ignoring option: "+option+". Not supported.");
  125               }
  126           }
  127           userSelect = (String) options.get(USER_SELECT);
  128           groupSelect = (String) options.get(GROUP_SELECT);
  129   
  130           digest = (String) options.get(DIGEST);
  131           encoding = (String) options.get(ENCODING);
  132           if (digest != null && !digest.equals("")) {
  133               // Check if the digest algorithm is available
  134               try {
  135                   MessageDigest.getInstance(digest);
  136               } catch (NoSuchAlgorithmException e) {
  137                   log.error("Initialization failed. Digest algorithm " + digest + " is not available.", e);
  138                   throw new IllegalArgumentException("Unable to configure SQL login module: " + e.getMessage(), e);
  139               }
  140               if (encoding != null && !"hex".equalsIgnoreCase(encoding) && !"base64".equalsIgnoreCase(encoding)) {
  141                   log.error("Initialization failed. Digest Encoding " + encoding + " is not supported.");
  142                   throw new IllegalArgumentException(
  143                           "Unable to configure SQL login module. Digest Encoding " + encoding + " not supported.");
  144               }
  145           }
  146   
  147           String dataSourceName = (String) options.get(DATABASE_POOL_NAME);
  148           if (dataSourceName != null) {
  149               dataSourceName = dataSourceName.trim();
  150               String dataSourceAppName = (String) options.get(DATABASE_POOL_APP_NAME);
  151               if (dataSourceAppName == null || dataSourceAppName.trim().equals("")) {
  152                   dataSourceAppName = "null";
  153               } else {
  154                   dataSourceAppName = dataSourceAppName.trim();
  155               }
  156               String kernelName = (String) options.get(JaasLoginModuleUse.KERNEL_NAME_LM_OPTION);
  157               Kernel kernel = KernelRegistry.getKernel(kernelName);
  158               Set<AbstractName> set = kernel.listGBeans(new AbstractNameQuery(JCAManagedConnectionFactory.class.getName()));
  159               JCAManagedConnectionFactory factory;
  160               for (AbstractName name : set) {
  161                   if (name.getName().get("J2EEApplication").equals(dataSourceAppName) &&
  162                           name.getName().get("name").equals(dataSourceName)) {
  163                       try {
  164                           factory = (JCAManagedConnectionFactory) kernel.getGBean(name);
  165                           String type = factory.getConnectionFactoryInterface();
  166                           if (type.equals(DataSource.class.getName())) {
  167                               this.factory = factory;
  168                               break;
  169                           }
  170                       } catch (GBeanNotFoundException e) {
  171                           // ignore... GBean was unregistered
  172                       }
  173                   }
  174               }
  175           } else {
  176               connectionURL = (String) options.get(CONNECTION_URL);
  177               properties = new Properties();
  178               if (options.get(USER) != null) {
  179                   properties.put("user", options.get(USER));
  180               }
  181               if (options.get(PASSWORD) != null) {
  182                   properties.put("password", options.get(PASSWORD));
  183               }
  184               ClassLoader cl = (ClassLoader) options.get(JaasLoginModuleUse.CLASSLOADER_LM_OPTION);
  185               try {
  186                   driver = (Driver) cl.loadClass((String) options.get(DRIVER)).newInstance();
  187               } catch (ClassNotFoundException e) {
  188                   throw new IllegalArgumentException("Driver class " + options.get(
  189                           DRIVER) + " is not available.  Perhaps you need to add it as a dependency in your deployment plan?",
  190                           e);
  191               } catch (Exception e) {
  192                   throw new IllegalArgumentException(
  193                           "Unable to load, instantiate, register driver " + options.get(DRIVER) + ": " + e.getMessage(),
  194                           e);
  195               }
  196           }
  197       }
  198   
  199       /**
  200        * This LoginModule is not to be ignored.  So, this method should never return false.
  201        * @return true if authentication succeeds, or throw a LoginException such as FailedLoginException
  202        *         if authentication fails
  203        */
  204       public boolean login() throws LoginException {
  205           loginSucceeded = false;
  206           Callback[] callbacks = new Callback[2];
  207   
  208           callbacks[0] = new NameCallback("User name");
  209           callbacks[1] = new PasswordCallback("Password", false);
  210           try {
  211               handler.handle(callbacks);
  212           } catch (IOException ioe) {
  213               throw (LoginException) new LoginException().initCause(ioe);
  214           } catch (UnsupportedCallbackException uce) {
  215               throw (LoginException) new LoginException().initCause(uce);
  216           }
  217           assert callbacks.length == 2;
  218           cbUsername = ((NameCallback) callbacks[0]).getName();
  219           if (cbUsername == null || cbUsername.equals("")) {
  220               throw new FailedLoginException();
  221           }
  222           char[] provided = ((PasswordCallback) callbacks[1]).getPassword();
  223           cbPassword = provided == null ? null : new String(provided);
  224   
  225           try {
  226               Connection conn;
  227               if (factory != null) {
  228                   DataSource ds = (DataSource) factory.getConnectionFactory();
  229                   conn = ds.getConnection();
  230               } else {
  231                   conn = driver.connect(connectionURL, properties);
  232               }
  233   
  234               try {
  235                   PreparedStatement statement = conn.prepareStatement(userSelect);
  236                   try {
  237                       int count = countParameters(userSelect);
  238                       for (int i = 0; i < count; i++) {
  239                           statement.setObject(i + 1, cbUsername);
  240                       }
  241                       ResultSet result = statement.executeQuery();
  242   
  243                       try {
  244                           boolean found = false;
  245                           while (result.next()) {
  246                               String userName = result.getString(1);
  247                               String userPassword = result.getString(2);
  248   
  249                               if (cbUsername.equals(userName)) {
  250                                   found = true;
  251                                   if (!checkPassword(userPassword, cbPassword)) {
  252                                       throw new FailedLoginException();
  253                                   }
  254                                   break;
  255                               }
  256                           }
  257                           if(!found) {
  258                               // User does not exist
  259                               throw new FailedLoginException();
  260                           }
  261                       } finally {
  262                           result.close();
  263                       }
  264                   } finally {
  265                       statement.close();
  266                   }
  267   
  268                   statement = conn.prepareStatement(groupSelect);
  269                   try {
  270                       int count = countParameters(groupSelect);
  271                       for (int i = 0; i < count; i++) {
  272                           statement.setObject(i + 1, cbUsername);
  273                       }
  274                       ResultSet result = statement.executeQuery();
  275   
  276                       try {
  277                           while (result.next()) {
  278                               String userName = result.getString(1);
  279                               String groupName = result.getString(2);
  280   
  281                               if (cbUsername.equals(userName)) {
  282                                   groups.add(groupName);
  283                               }
  284                           }
  285                       } finally {
  286                           result.close();
  287                       }
  288                   } finally {
  289                       statement.close();
  290                   }
  291               } finally {
  292                   conn.close();
  293               }
  294           } catch (LoginException e) {
  295               // Clear out the private state
  296               cbUsername = null;
  297               cbPassword = null;
  298               groups.clear();
  299               throw e;
  300           } catch (SQLException sqle) {
  301               // Clear out the private state
  302               cbUsername = null;
  303               cbPassword = null;
  304               groups.clear();
  305               throw (LoginException) new LoginException("SQL error").initCause(sqle);
  306           } catch (Exception e) {
  307               // Clear out the private state
  308               cbUsername = null;
  309               cbPassword = null;
  310               groups.clear();
  311               throw (LoginException) new LoginException("Could not access datasource").initCause(e);
  312           }
  313   
  314           loginSucceeded = true;
  315           return true;
  316       }
  317   
  318       /*
  319        * @exception LoginException if login succeeded but commit failed.
  320        *
  321        * @return true if login succeeded and commit succeeded, or false if login failed but commit succeeded.
  322        */
  323       public boolean commit() throws LoginException {
  324           if(loginSucceeded) {
  325               if(cbUsername != null) {
  326                   allPrincipals.add(new GeronimoUserPrincipal(cbUsername));
  327               }
  328               for(String group: groups) {
  329                   allPrincipals.add(new GeronimoGroupPrincipal(group));
  330               }
  331               subject.getPrincipals().addAll(allPrincipals);
  332           }
  333   
  334           // Clear out the private state
  335           cbUsername = null;
  336           cbPassword = null;
  337           groups.clear();
  338   
  339           return loginSucceeded;
  340       }
  341   
  342       public boolean abort() throws LoginException {
  343           if(loginSucceeded) {
  344               // Clear out the private state
  345               cbUsername = null;
  346               cbPassword = null;
  347               groups.clear();
  348               allPrincipals.clear();
  349           }
  350           return loginSucceeded;
  351       }
  352   
  353       public boolean logout() throws LoginException {
  354           // Clear out the private state
  355           loginSucceeded = false;
  356           cbUsername = null;
  357           cbPassword = null;
  358           groups.clear();
  359           if(!subject.isReadOnly()) {
  360               // Remove principals added by this LoginModule
  361               subject.getPrincipals().removeAll(allPrincipals);
  362           }
  363           allPrincipals.clear();
  364           return true;
  365       }
  366   
  367       private static int countParameters(String sql) {
  368           int count = 0;
  369           int pos = -1;
  370           while ((pos = sql.indexOf('?', pos + 1)) != -1) {
  371               ++count;
  372           }
  373           return count;
  374       }
  375   
  376       /**
  377        * This method checks if the provided password is correct.  The original password may have been digested.
  378        *
  379        * @param real     Original password in digested form if applicable
  380        * @param provided User provided password in clear text
  381        * @return true     If the password is correct
  382        */
  383       private boolean checkPassword(String real, String provided) {
  384           if (real == null && provided == null) {
  385               return true;
  386           }
  387           if (real == null || provided == null) {
  388               return false;
  389           }
  390   
  391           //both are non-null
  392           if (digest == null || digest.equals("")) {
  393               // No digest algorithm is used
  394               return real.equals(provided);
  395           }
  396           try {
  397               // Digest the user provided password
  398               MessageDigest md = MessageDigest.getInstance(digest);
  399               byte[] data = md.digest(provided.getBytes());
  400               if (encoding == null || "hex".equalsIgnoreCase(encoding)) {
  401                   // Convert bytes to hex digits
  402                   byte[] hexData = new byte[data.length * 2];
  403                   HexTranslator ht = new HexTranslator();
  404                   ht.encode(data, 0, data.length, hexData, 0);
  405                   // Compare the digested provided password with the actual one
  406                   return real.equalsIgnoreCase(new String(hexData));
  407               } else if ("base64".equalsIgnoreCase(encoding)) {
  408                   return real.equals(new String(Base64.encode(data)));
  409               }
  410           } catch (NoSuchAlgorithmException e) {
  411               // Should not occur.  Availability of algorithm has been checked at initialization
  412               log.error("Should not occur.  Availability of algorithm has been checked at initialization.", e);
  413           }
  414           return false;
  415       }
  416   }

Home » geronimo-2.2-source-release » org.apache.geronimo.security.realm.providers » [javadoc | source]