1 /*
2 * JBoss, the OpenSource WebOS
3 *
4 * Distributable under LGPL license.
5 * See terms of license at gnu.org.
6 */
7 package org.jboss.security.auth.spi;
8
9 import java.security.acl.Group;
10 import java.util.HashMap;
11 import java.util.Map;
12 import java.sql.Connection;
13 import java.sql.PreparedStatement;
14 import java.sql.ResultSet;
15 import java.sql.SQLException;
16 import javax.naming.InitialContext;
17 import javax.naming.NamingException;
18 import javax.sql.DataSource;
19 import javax.security.auth.Subject;
20 import javax.security.auth.callback.CallbackHandler;
21 import javax.security.auth.login.LoginException;
22 import javax.security.auth.login.FailedLoginException;
23
24 import org.jboss.security.SimpleGroup;
25 import org.jboss.security.SimplePrincipal;
26 import org.jboss.security.auth.spi.UsernamePasswordLoginModule;
27
28 /**
29 * A JDBC based login module that supports authentication and role mapping.
30 * It is based on two logical tables:
31 * <ul>
32 * <li>Principals(PrincipalID text, Password text)
33 * <li>Roles(PrincipalID text, Role text, RoleGroup text)
34 * </ul>
35 * <p>
36 * LoginModule options:
37 * <ul>
38 * <li><em>dsJndiName</em>: The name of the DataSource of the database containing the Principals, Roles tables
39 * <li><em>principalsQuery</em>: The prepared statement query, equivalent to:
40 * <pre>
41 * "select Password from Principals where PrincipalID=?"
42 * </pre>
43 * <li><em>rolesQuery</em>: The prepared statement query, equivalent to:
44 * <pre>
45 * "select Role, RoleGroup from Roles where PrincipalID=?"
46 * </pre>
47 * </ul>
48 *
49 * @author <a href="mailto:on@ibis.odessa.ua">Oleg Nitz</a>
50 * @author Scott.Stark@jboss.org
51 * @version $Revision: 1.6.4.1 $
52 */
53 public class DatabaseServerLoginModule extends UsernamePasswordLoginModule
54 {
55 private String dsJndiName;
56 private String principalsQuery = "select Password from Principals where PrincipalID=?";
57 private String rolesQuery = "select Role, RoleGroup from Roles where PrincipalID=?";
58
59 /**
60 * Initialize this LoginModule.
61 */
62 public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options)
63 {
64 super.initialize(subject, callbackHandler, sharedState, options);
65 dsJndiName = (String) options.get("dsJndiName");
66 if( dsJndiName == null )
67 dsJndiName = "java:/DefaultDS";
68 Object tmp = options.get("principalsQuery");
69 if( tmp != null )
70 principalsQuery = tmp.toString();
71 tmp = options.get("rolesQuery");
72 if( tmp != null )
73 rolesQuery = tmp.toString();
74 log.trace("DatabaseServerLoginModule, dsJndiName="+dsJndiName);
75 log.trace("principalsQuery="+principalsQuery);
76 log.trace("rolesQuery="+rolesQuery);
77 }
78
79 /** Get the expected password for the current username available via
80 * the getUsername() method. This is called from within the login()
81 * method after the CallbackHandler has returned the username and
82 * candidate password.
83 * @return the valid password String
84 */
85 protected String getUsersPassword() throws LoginException
86 {
87 String username = getUsername();
88 String password = null;
89 Connection conn = null;
90 PreparedStatement ps = null;
91
92 try
93 {
94 InitialContext ctx = new InitialContext();
95 DataSource ds = (DataSource) ctx.lookup(dsJndiName);
96 conn = ds.getConnection();
97 // Get the password
98 ps = conn.prepareStatement(principalsQuery);
99 ps.setString(1, username);
100 ResultSet rs = ps.executeQuery();
101 if( rs.next() == false )
102 throw new FailedLoginException("No matching username found in Principals");
103
104 password = rs.getString(1);
105 password = convertRawPassword(password);
106 rs.close();
107 }
108 catch(NamingException ex)
109 {
110 throw new LoginException(ex.toString(true));
111 }
112 catch(SQLException ex)
113 {
114 log.error("Query failed", ex);
115 throw new LoginException(ex.toString());
116 }
117 finally
118 {
119 if( ps != null )
120 {
121 try
122 {
123 ps.close();
124 }
125 catch(SQLException e)
126 {}
127 }
128 if( conn != null )
129 {
130 try
131 {
132 conn.close();
133 }
134 catch (SQLException ex)
135 {}
136 }
137 }
138 return password;
139 }
140
141 /** Overriden by subclasses to return the Groups that correspond to the
142 to the role sets assigned to the user. Subclasses should create at
143 least a Group named "Roles" that contains the roles assigned to the user.
144 A second common group is "CallerPrincipal" that provides the application
145 identity of the user rather than the security domain identity.
146 @return Group[] containing the sets of roles
147 */
148 protected Group[] getRoleSets() throws LoginException
149 {
150 String username = getUsername();
151 Connection conn = null;
152 HashMap setsMap = new HashMap();
153 PreparedStatement ps = null;
154
155 try
156 {
157 InitialContext ctx = new InitialContext();
158 DataSource ds = (DataSource) ctx.lookup(dsJndiName);
159 conn = ds.getConnection();
160 // Get the user role names
161 ps = conn.prepareStatement(rolesQuery);
162 try
163 {
164 ps.setString(1, username);
165 }
166 catch(ArrayIndexOutOfBoundsException ignore)
167 {
168 // The query may not have any parameters so just try it
169 }
170 ResultSet rs = ps.executeQuery();
171 if( rs.next() == false )
172 {
173 if( getUnauthenticatedIdentity() == null )
174 throw new FailedLoginException("No matching username found in Roles");
175 /* We are running with an unauthenticatedIdentity so create an
176 empty Roles set and return.
177 */
178 Group[] roleSets = { new SimpleGroup("Roles") };
179 return roleSets;
180 }
181
182 do
183 {
184 String name = rs.getString(1);
185 String groupName = rs.getString(2);
186 if( groupName == null || groupName.length() == 0 )
187 groupName = "Roles";
188 Group group = (Group) setsMap.get(groupName);
189 if( group == null )
190 {
191 group = new SimpleGroup(groupName);
192 setsMap.put(groupName, group);
193 }
194 group.addMember(new SimplePrincipal(name));
195 } while( rs.next() );
196 rs.close();
197 }
198 catch(NamingException ex)
199 {
200 throw new LoginException(ex.toString(true));
201 }
202 catch(SQLException ex)
203 {
204 super.log.error("SQL failure", ex);
205 throw new LoginException(ex.toString());
206 }
207 finally
208 {
209 if( ps != null )
210 {
211 try
212 {
213 ps.close();
214 }
215 catch(SQLException e)
216 {}
217 }
218 if( conn != null )
219 {
220 try
221 {
222 conn.close();
223 }
224 catch (Exception ex)
225 {}
226 }
227 }
228
229 Group[] roleSets = new Group[setsMap.size()];
230 setsMap.values().toArray(roleSets);
231 return roleSets;
232 }
233
234 /** A hook to allow subclasses to convert a password from the database
235 into a plain text string or whatever form is used for matching against
236 the user input. It is called from within the getUsersPassword() method.
237 @param rawPassword, the password as obtained from the database
238 @return the argument rawPassword
239 */
240 protected String convertRawPassword(String rawPassword)
241 {
242 return rawPassword;
243 }
244 }