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    }