001 /** 002 * 003 * Copyright 2003-2004 The Apache Software Foundation 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018 package org.apache.geronimo.security.realm.providers; 019 020 import java.io.IOException; 021 import java.sql.Connection; 022 import java.sql.Driver; 023 import java.sql.PreparedStatement; 024 import java.sql.ResultSet; 025 import java.sql.SQLException; 026 import java.util.HashSet; 027 import java.util.Iterator; 028 import java.util.Map; 029 import java.util.Properties; 030 import java.util.Set; 031 import javax.security.auth.Subject; 032 import javax.security.auth.callback.Callback; 033 import javax.security.auth.callback.CallbackHandler; 034 import javax.security.auth.callback.NameCallback; 035 import javax.security.auth.callback.PasswordCallback; 036 import javax.security.auth.callback.UnsupportedCallbackException; 037 import javax.security.auth.login.FailedLoginException; 038 import javax.security.auth.login.LoginException; 039 import javax.security.auth.spi.LoginModule; 040 import javax.sql.DataSource; 041 042 import org.apache.geronimo.gbean.AbstractName; 043 import org.apache.geronimo.gbean.AbstractNameQuery; 044 import org.apache.geronimo.j2ee.j2eeobjectnames.NameFactory; 045 import org.apache.geronimo.kernel.GBeanNotFoundException; 046 import org.apache.geronimo.kernel.Kernel; 047 import org.apache.geronimo.kernel.KernelRegistry; 048 import org.apache.geronimo.management.geronimo.JCAManagedConnectionFactory; 049 import org.apache.geronimo.security.jaas.JaasLoginModuleUse; 050 051 052 /** 053 * A login module that loads security information from a SQL database. Expects 054 * to be run by a GenericSecurityRealm (doesn't work on its own). 055 * <p> 056 * This requires database connectivity information (either 1: a dataSourceName and 057 * optional dataSourceApplication or 2: a JDBC driver, URL, username, and password) 058 * and 2 SQL queries. 059 * <p> 060 * The userSelect query should return 2 values, the username and the password in 061 * that order. It should include one PreparedStatement parameter (a ?) which 062 * will be filled in with the username. In other words, the query should look 063 * like: <tt>SELECT user, password FROM users WHERE username=?</tt> 064 * <p> 065 * The groupSelect query should return 2 values, the username and the group name in 066 * that order (but it may return multiple rows, one per group). It should include 067 * one PreparedStatement parameter (a ?) which will be filled in with the username. 068 * In other words, the query should look like: 069 * <tt>SELECT user, role FROM user_roles WHERE username=?</tt> 070 * 071 * @version $Rev: 407961 $ $Date: 2006-05-20 00:26:08 -0700 (Sat, 20 May 2006) $ 072 */ 073 public class SQLLoginModule implements LoginModule { 074 public final static String USER_SELECT = "userSelect"; 075 public final static String GROUP_SELECT = "groupSelect"; 076 public final static String CONNECTION_URL = "jdbcURL"; 077 public final static String USER = "jdbcUser"; 078 public final static String PASSWORD = "jdbcPassword"; 079 public final static String DRIVER = "jdbcDriver"; 080 public final static String DATABASE_POOL_NAME = "dataSourceName"; 081 public final static String DATABASE_POOL_APP_NAME = "dataSourceApplication"; 082 private String connectionURL; 083 private Properties properties; 084 private Driver driver; 085 private JCAManagedConnectionFactory factory; 086 private String userSelect; 087 private String groupSelect; 088 089 private Subject subject; 090 private CallbackHandler handler; 091 private String cbUsername; 092 private String cbPassword; 093 private final Set groups = new HashSet(); 094 095 public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { 096 this.subject = subject; 097 this.handler = callbackHandler; 098 userSelect = (String) options.get(USER_SELECT); 099 groupSelect = (String) options.get(GROUP_SELECT); 100 101 String dataSourceName = (String) options.get(DATABASE_POOL_NAME); 102 if(dataSourceName != null) { 103 dataSourceName = dataSourceName.trim(); 104 String dataSourceAppName = (String) options.get(DATABASE_POOL_APP_NAME); 105 if(dataSourceAppName == null || dataSourceAppName.trim().equals("")) { 106 dataSourceAppName = "null"; 107 } else { 108 dataSourceAppName = dataSourceAppName.trim(); 109 } 110 String kernelName = (String) options.get(JaasLoginModuleUse.KERNEL_NAME_LM_OPTION); 111 Kernel kernel = KernelRegistry.getKernel(kernelName); 112 Set set = kernel.listGBeans(new AbstractNameQuery(JCAManagedConnectionFactory.class.getName())); 113 JCAManagedConnectionFactory factory; 114 for (Iterator it = set.iterator(); it.hasNext();) { 115 AbstractName name = (AbstractName) it.next(); 116 if(name.getName().get(NameFactory.J2EE_APPLICATION).equals(dataSourceAppName) && 117 name.getName().get(NameFactory.J2EE_NAME).equals(dataSourceName)) { 118 try { 119 factory = (JCAManagedConnectionFactory) kernel.getGBean(name); 120 String type = factory.getConnectionFactoryInterface(); 121 if(type.equals(DataSource.class.getName())) { 122 this.factory = factory; 123 break; 124 } 125 } catch (GBeanNotFoundException e) { 126 // ignore... GBean was unregistered 127 } 128 } 129 } 130 } else { 131 connectionURL = (String) options.get(CONNECTION_URL); 132 properties = new Properties(); 133 if(options.get(USER) != null) { 134 properties.put("user", options.get(USER)); 135 } 136 if(options.get(PASSWORD) != null) { 137 properties.put("password", options.get(PASSWORD)); 138 } 139 ClassLoader cl = (ClassLoader) options.get(JaasLoginModuleUse.CLASSLOADER_LM_OPTION); 140 try { 141 driver = (Driver) cl.loadClass((String) options.get(DRIVER)).newInstance(); 142 } catch (ClassNotFoundException e) { 143 throw new IllegalArgumentException("Driver class " + options.get(DRIVER) + " is not available. Perhaps you need to add it as a dependency in your deployment plan?"); 144 } catch (Exception e) { 145 throw new IllegalArgumentException("Unable to load, instantiate, register driver " + options.get(DRIVER) + ": " + e.getMessage()); 146 } 147 } 148 } 149 150 public boolean login() throws LoginException { 151 Callback[] callbacks = new Callback[2]; 152 153 callbacks[0] = new NameCallback("User name"); 154 callbacks[1] = new PasswordCallback("Password", false); 155 try { 156 handler.handle(callbacks); 157 } catch (IOException ioe) { 158 throw (LoginException) new LoginException().initCause(ioe); 159 } catch (UnsupportedCallbackException uce) { 160 throw (LoginException) new LoginException().initCause(uce); 161 } 162 assert callbacks.length == 2; 163 cbUsername = ((NameCallback) callbacks[0]).getName(); 164 if (cbUsername == null || cbUsername.equals("")) { 165 return false; 166 } 167 char[] provided = ((PasswordCallback) callbacks[1]).getPassword(); 168 cbPassword = provided == null ? null : new String(provided); 169 170 boolean found = false; 171 try { 172 Connection conn; 173 if(factory != null) { 174 DataSource ds = (DataSource) factory.getConnectionFactory(); 175 conn = ds.getConnection(); 176 } else { 177 conn = driver.connect(connectionURL, properties); 178 } 179 180 try { 181 PreparedStatement statement = conn.prepareStatement(userSelect); 182 try { 183 int count = countParameters(userSelect); 184 for(int i=0; i<count; i++) { 185 statement.setObject(i+1, cbUsername); 186 } 187 ResultSet result = statement.executeQuery(); 188 189 try { 190 while (result.next()) { 191 String userName = result.getString(1); 192 String userPassword = result.getString(2); 193 194 if (cbUsername.equals(userName)) { 195 found = (cbPassword == null && userPassword == null) || 196 (cbPassword != null && userPassword != null && cbPassword.equals(userPassword)); 197 break; 198 } 199 } 200 } finally { 201 result.close(); 202 } 203 } finally { 204 statement.close(); 205 } 206 207 if (!found) { 208 throw new FailedLoginException(); 209 } 210 211 statement = conn.prepareStatement(groupSelect); 212 try { 213 int count = countParameters(groupSelect); 214 for(int i=0; i<count; i++) { 215 statement.setObject(i+1, cbUsername); 216 } 217 ResultSet result = statement.executeQuery(); 218 219 try { 220 while (result.next()) { 221 String userName = result.getString(1); 222 String groupName = result.getString(2); 223 224 if (cbUsername.equals(userName)) { 225 groups.add(new GeronimoGroupPrincipal(groupName)); 226 } 227 } 228 } finally { 229 result.close(); 230 } 231 } finally { 232 statement.close(); 233 } 234 } finally { 235 conn.close(); 236 } 237 } catch (SQLException sqle) { 238 throw (LoginException) new LoginException("SQL error").initCause(sqle); 239 } 240 241 return true; 242 } 243 244 public boolean commit() throws LoginException { 245 Set principals = subject.getPrincipals(); 246 principals.add(new GeronimoUserPrincipal(cbUsername)); 247 Iterator iter = groups.iterator(); 248 while (iter.hasNext()) { 249 principals.add(iter.next()); 250 } 251 252 return true; 253 } 254 255 public boolean abort() throws LoginException { 256 cbUsername = null; 257 cbPassword = null; 258 259 return true; 260 } 261 262 public boolean logout() throws LoginException { 263 cbUsername = null; 264 cbPassword = null; 265 //todo: should remove principals put in by commit 266 return true; 267 } 268 269 private static int countParameters(String sql) { 270 int count = 0; 271 int pos = -1; 272 while((pos = sql.indexOf('?', pos+1)) != -1) { 273 ++count; 274 } 275 return count; 276 } 277 }