001: /**
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. 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: */package org.apache.geronimo.security.realm.providers;
017:
018: import java.io.IOException;
019: import java.security.MessageDigest;
020: import java.security.NoSuchAlgorithmException;
021: import java.security.Principal;
022: import java.sql.Connection;
023: import java.sql.Driver;
024: import java.sql.PreparedStatement;
025: import java.sql.ResultSet;
026: import java.sql.SQLException;
027: import java.util.Arrays;
028: import java.util.Collections;
029: import java.util.HashSet;
030: import java.util.List;
031: import java.util.Map;
032: import java.util.Properties;
033: import java.util.Set;
034:
035: import javax.security.auth.Subject;
036: import javax.security.auth.callback.Callback;
037: import javax.security.auth.callback.CallbackHandler;
038: import javax.security.auth.callback.NameCallback;
039: import javax.security.auth.callback.PasswordCallback;
040: import javax.security.auth.callback.UnsupportedCallbackException;
041: import javax.security.auth.login.FailedLoginException;
042: import javax.security.auth.login.LoginException;
043: import javax.security.auth.spi.LoginModule;
044: import javax.sql.DataSource;
045:
046: import org.apache.commons.logging.Log;
047: import org.apache.commons.logging.LogFactory;
048: import org.apache.geronimo.gbean.AbstractName;
049: import org.apache.geronimo.gbean.AbstractNameQuery;
050: import org.apache.geronimo.j2ee.j2eeobjectnames.NameFactory;
051: import org.apache.geronimo.kernel.GBeanNotFoundException;
052: import org.apache.geronimo.kernel.Kernel;
053: import org.apache.geronimo.kernel.KernelRegistry;
054: import org.apache.geronimo.management.geronimo.JCAManagedConnectionFactory;
055: import org.apache.geronimo.security.jaas.JaasLoginModuleUse;
056: import org.apache.geronimo.security.jaas.WrappingLoginModule;
057: import org.apache.geronimo.crypto.encoders.Base64;
058: import org.apache.geronimo.crypto.encoders.HexTranslator;
059:
060: /**
061: * A login module that loads security information from a SQL database. Expects
062: * to be run by a GenericSecurityRealm (doesn't work on its own).
063: * <p/>
064: * This requires database connectivity information (either 1: a dataSourceName and
065: * optional dataSourceApplication or 2: a JDBC driver, URL, username, and password)
066: * and 2 SQL queries.
067: * <p/>
068: * The userSelect query should return 2 values, the username and the password in
069: * that order. It should include one PreparedStatement parameter (a ?) which
070: * will be filled in with the username. In other words, the query should look
071: * like: <tt>SELECT user, password FROM credentials WHERE username=?</tt>
072: * <p/>
073: * The groupSelect query should return 2 values, the username and the group name in
074: * that order (but it may return multiple rows, one per group). It should include
075: * one PreparedStatement parameter (a ?) which will be filled in with the username.
076: * In other words, the query should look like:
077: * <tt>SELECT user, role FROM user_roles WHERE username=?</tt>
078: * <p/>
079: * This login module checks security credentials so the lifecycle methods must return true to indicate success
080: * or throw LoginException to indicate failure.
081: *
082: * @version $Rev: 617588 $ $Date: 2008-02-01 10:20:07 -0800 (Fri, 01 Feb 2008) $
083: */
084: public class SQLLoginModule implements LoginModule {
085: private static Log log = LogFactory.getLog(SQLLoginModule.class);
086: public final static String USER_SELECT = "userSelect";
087: public final static String GROUP_SELECT = "groupSelect";
088: public final static String CONNECTION_URL = "jdbcURL";
089: public final static String USER = "jdbcUser";
090: public final static String PASSWORD = "jdbcPassword";
091: public final static String DRIVER = "jdbcDriver";
092: public final static String DATABASE_POOL_NAME = "dataSourceName";
093: public final static String DATABASE_POOL_APP_NAME = "dataSourceApplication";
094: public final static String DIGEST = "digest";
095: public final static String ENCODING = "encoding";
096: public final static List<String> supportedOptions = Collections
097: .unmodifiableList(Arrays.asList(USER_SELECT, GROUP_SELECT,
098: CONNECTION_URL, USER, PASSWORD, DRIVER,
099: DATABASE_POOL_NAME, DATABASE_POOL_APP_NAME, DIGEST,
100: ENCODING));
101:
102: private String connectionURL;
103: private Properties properties;
104: private Driver driver;
105: private JCAManagedConnectionFactory factory;
106: private String userSelect;
107: private String groupSelect;
108: private String digest;
109: private String encoding;
110:
111: private boolean loginSucceeded;
112: private Subject subject;
113: private CallbackHandler handler;
114: private String cbUsername;
115: private String cbPassword;
116: private final Set<String> groups = new HashSet<String>();
117: private final Set<Principal> allPrincipals = new HashSet<Principal>();
118:
119: public void initialize(Subject subject,
120: CallbackHandler callbackHandler, Map sharedState,
121: Map options) {
122: this .subject = subject;
123: this .handler = callbackHandler;
124: for (Object option : options.keySet()) {
125: if (!supportedOptions.contains(option)
126: && !JaasLoginModuleUse.supportedOptions
127: .contains(option)
128: && !WrappingLoginModule.supportedOptions
129: .contains(option)) {
130: log.warn("Ignoring option: " + option
131: + ". Not supported.");
132: }
133: }
134: userSelect = (String) options.get(USER_SELECT);
135: groupSelect = (String) options.get(GROUP_SELECT);
136:
137: digest = (String) options.get(DIGEST);
138: encoding = (String) options.get(ENCODING);
139: if (digest != null && !digest.equals("")) {
140: // Check if the digest algorithm is available
141: try {
142: MessageDigest.getInstance(digest);
143: } catch (NoSuchAlgorithmException e) {
144: log.error("Initialization failed. Digest algorithm "
145: + digest + " is not available.", e);
146: throw new IllegalArgumentException(
147: "Unable to configure SQL login module: "
148: + e.getMessage(), e);
149: }
150: if (encoding != null && !"hex".equalsIgnoreCase(encoding)
151: && !"base64".equalsIgnoreCase(encoding)) {
152: log.error("Initialization failed. Digest Encoding "
153: + encoding + " is not supported.");
154: throw new IllegalArgumentException(
155: "Unable to configure SQL login module. Digest Encoding "
156: + encoding + " not supported.");
157: }
158: }
159:
160: String dataSourceName = (String) options
161: .get(DATABASE_POOL_NAME);
162: if (dataSourceName != null) {
163: dataSourceName = dataSourceName.trim();
164: String dataSourceAppName = (String) options
165: .get(DATABASE_POOL_APP_NAME);
166: if (dataSourceAppName == null
167: || dataSourceAppName.trim().equals("")) {
168: dataSourceAppName = "null";
169: } else {
170: dataSourceAppName = dataSourceAppName.trim();
171: }
172: String kernelName = (String) options
173: .get(JaasLoginModuleUse.KERNEL_NAME_LM_OPTION);
174: Kernel kernel = KernelRegistry.getKernel(kernelName);
175: Set<AbstractName> set = kernel
176: .listGBeans(new AbstractNameQuery(
177: JCAManagedConnectionFactory.class.getName()));
178: JCAManagedConnectionFactory factory;
179: for (AbstractName name : set) {
180: if (name.getName().get(NameFactory.J2EE_APPLICATION)
181: .equals(dataSourceAppName)
182: && name.getName().get(NameFactory.J2EE_NAME)
183: .equals(dataSourceName)) {
184: try {
185: factory = (JCAManagedConnectionFactory) kernel
186: .getGBean(name);
187: String type = factory
188: .getConnectionFactoryInterface();
189: if (type.equals(DataSource.class.getName())) {
190: this .factory = factory;
191: break;
192: }
193: } catch (GBeanNotFoundException e) {
194: // ignore... GBean was unregistered
195: }
196: }
197: }
198: } else {
199: connectionURL = (String) options.get(CONNECTION_URL);
200: properties = new Properties();
201: if (options.get(USER) != null) {
202: properties.put("user", options.get(USER));
203: }
204: if (options.get(PASSWORD) != null) {
205: properties.put("password", options.get(PASSWORD));
206: }
207: ClassLoader cl = (ClassLoader) options
208: .get(JaasLoginModuleUse.CLASSLOADER_LM_OPTION);
209: try {
210: driver = (Driver) cl.loadClass(
211: (String) options.get(DRIVER)).newInstance();
212: } catch (ClassNotFoundException e) {
213: throw new IllegalArgumentException(
214: "Driver class "
215: + options.get(DRIVER)
216: + " is not available. Perhaps you need to add it as a dependency in your deployment plan?",
217: e);
218: } catch (Exception e) {
219: throw new IllegalArgumentException(
220: "Unable to load, instantiate, register driver "
221: + options.get(DRIVER) + ": "
222: + e.getMessage(), e);
223: }
224: }
225: }
226:
227: /**
228: * This LoginModule is not to be ignored. So, this method should never return false.
229: * @return true if authentication succeeds, or throw a LoginException such as FailedLoginException
230: * if authentication fails
231: */
232: public boolean login() throws LoginException {
233: loginSucceeded = false;
234: Callback[] callbacks = new Callback[2];
235:
236: callbacks[0] = new NameCallback("User name");
237: callbacks[1] = new PasswordCallback("Password", false);
238: try {
239: handler.handle(callbacks);
240: } catch (IOException ioe) {
241: throw (LoginException) new LoginException().initCause(ioe);
242: } catch (UnsupportedCallbackException uce) {
243: throw (LoginException) new LoginException().initCause(uce);
244: }
245: assert callbacks.length == 2;
246: cbUsername = ((NameCallback) callbacks[0]).getName();
247: if (cbUsername == null || cbUsername.equals("")) {
248: throw new FailedLoginException();
249: }
250: char[] provided = ((PasswordCallback) callbacks[1])
251: .getPassword();
252: cbPassword = provided == null ? null : new String(provided);
253:
254: try {
255: Connection conn;
256: if (factory != null) {
257: DataSource ds = (DataSource) factory
258: .getConnectionFactory();
259: conn = ds.getConnection();
260: } else {
261: conn = driver.connect(connectionURL, properties);
262: }
263:
264: try {
265: PreparedStatement statement = conn
266: .prepareStatement(userSelect);
267: try {
268: int count = countParameters(userSelect);
269: for (int i = 0; i < count; i++) {
270: statement.setObject(i + 1, cbUsername);
271: }
272: ResultSet result = statement.executeQuery();
273:
274: try {
275: boolean found = false;
276: while (result.next()) {
277: String userName = result.getString(1);
278: String userPassword = result.getString(2);
279:
280: if (cbUsername.equals(userName)) {
281: found = true;
282: if (!checkPassword(userPassword,
283: cbPassword)) {
284: throw new FailedLoginException();
285: }
286: break;
287: }
288: }
289: if (!found) {
290: // User does not exist
291: throw new FailedLoginException();
292: }
293: } finally {
294: result.close();
295: }
296: } finally {
297: statement.close();
298: }
299:
300: statement = conn.prepareStatement(groupSelect);
301: try {
302: int count = countParameters(groupSelect);
303: for (int i = 0; i < count; i++) {
304: statement.setObject(i + 1, cbUsername);
305: }
306: ResultSet result = statement.executeQuery();
307:
308: try {
309: while (result.next()) {
310: String userName = result.getString(1);
311: String groupName = result.getString(2);
312:
313: if (cbUsername.equals(userName)) {
314: groups.add(groupName);
315: }
316: }
317: } finally {
318: result.close();
319: }
320: } finally {
321: statement.close();
322: }
323: } finally {
324: conn.close();
325: }
326: } catch (LoginException e) {
327: // Clear out the private state
328: cbUsername = null;
329: cbPassword = null;
330: groups.clear();
331: throw e;
332: } catch (SQLException sqle) {
333: // Clear out the private state
334: cbUsername = null;
335: cbPassword = null;
336: groups.clear();
337: throw (LoginException) new LoginException("SQL error")
338: .initCause(sqle);
339: } catch (Exception e) {
340: // Clear out the private state
341: cbUsername = null;
342: cbPassword = null;
343: groups.clear();
344: throw (LoginException) new LoginException(
345: "Could not access datasource").initCause(e);
346: }
347:
348: loginSucceeded = true;
349: return true;
350: }
351:
352: /*
353: * @exception LoginException if login succeeded but commit failed.
354: *
355: * @return true if login succeeded and commit succeeded, or false if login failed but commit succeeded.
356: */
357: public boolean commit() throws LoginException {
358: if (loginSucceeded) {
359: if (cbUsername != null) {
360: allPrincipals
361: .add(new GeronimoUserPrincipal(cbUsername));
362: }
363: for (String group : groups) {
364: allPrincipals.add(new GeronimoGroupPrincipal(group));
365: }
366: subject.getPrincipals().addAll(allPrincipals);
367: }
368:
369: // Clear out the private state
370: cbUsername = null;
371: cbPassword = null;
372: groups.clear();
373:
374: return loginSucceeded;
375: }
376:
377: public boolean abort() throws LoginException {
378: if (loginSucceeded) {
379: // Clear out the private state
380: cbUsername = null;
381: cbPassword = null;
382: groups.clear();
383: allPrincipals.clear();
384: }
385: return loginSucceeded;
386: }
387:
388: public boolean logout() throws LoginException {
389: // Clear out the private state
390: loginSucceeded = false;
391: cbUsername = null;
392: cbPassword = null;
393: groups.clear();
394: if (!subject.isReadOnly()) {
395: // Remove principals added by this LoginModule
396: subject.getPrincipals().removeAll(allPrincipals);
397: }
398: allPrincipals.clear();
399: return true;
400: }
401:
402: private static int countParameters(String sql) {
403: int count = 0;
404: int pos = -1;
405: while ((pos = sql.indexOf('?', pos + 1)) != -1) {
406: ++count;
407: }
408: return count;
409: }
410:
411: /**
412: * This method checks if the provided password is correct. The original password may have been digested.
413: *
414: * @param real Original password in digested form if applicable
415: * @param provided User provided password in clear text
416: * @return true If the password is correct
417: */
418: private boolean checkPassword(String real, String provided) {
419: if (real == null && provided == null) {
420: return true;
421: }
422: if (real == null || provided == null) {
423: return false;
424: }
425:
426: //both are non-null
427: if (digest == null || digest.equals("")) {
428: // No digest algorithm is used
429: return real.equals(provided);
430: }
431: try {
432: // Digest the user provided password
433: MessageDigest md = MessageDigest.getInstance(digest);
434: byte[] data = md.digest(provided.getBytes());
435: if (encoding == null || "hex".equalsIgnoreCase(encoding)) {
436: // Convert bytes to hex digits
437: byte[] hexData = new byte[data.length * 2];
438: HexTranslator ht = new HexTranslator();
439: ht.encode(data, 0, data.length, hexData, 0);
440: // Compare the digested provided password with the actual one
441: return real.equalsIgnoreCase(new String(hexData));
442: } else if ("base64".equalsIgnoreCase(encoding)) {
443: return real.equals(new String(Base64.encode(data)));
444: }
445: } catch (NoSuchAlgorithmException e) {
446: // Should not occur. Availability of algorithm has been checked at initialization
447: log
448: .error(
449: "Should not occur. Availability of algorithm has been checked at initialization.",
450: e);
451: }
452: return false;
453: }
454: }
|