Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
JdbcRealm |
|
| 3.75;3.75 | ||||
JdbcRealm$1 |
|
| 3.75;3.75 | ||||
JdbcRealm$SaltStyle |
|
| 3.75;3.75 |
1 | /* | |
2 | * Licensed to the Apache Software Foundation (ASF) under one | |
3 | * or more contributor license agreements. See the NOTICE file | |
4 | * distributed with this work for additional information | |
5 | * regarding copyright ownership. The ASF licenses this file | |
6 | * to you under the Apache License, Version 2.0 (the | |
7 | * "License"); you may not use this file except in compliance | |
8 | * with the License. You may obtain a copy of the License at | |
9 | * | |
10 | * http://www.apache.org/licenses/LICENSE-2.0 | |
11 | * | |
12 | * Unless required by applicable law or agreed to in writing, | |
13 | * software distributed under the License is distributed on an | |
14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
15 | * KIND, either express or implied. See the License for the | |
16 | * specific language governing permissions and limitations | |
17 | * under the License. | |
18 | */ | |
19 | package org.apache.shiro.realm.jdbc; | |
20 | ||
21 | import org.apache.shiro.authc.*; | |
22 | import org.apache.shiro.authz.AuthorizationException; | |
23 | import org.apache.shiro.authz.AuthorizationInfo; | |
24 | import org.apache.shiro.authz.SimpleAuthorizationInfo; | |
25 | import org.apache.shiro.config.ConfigurationException; | |
26 | import org.apache.shiro.realm.AuthorizingRealm; | |
27 | import org.apache.shiro.subject.PrincipalCollection; | |
28 | import org.apache.shiro.util.ByteSource; | |
29 | import org.apache.shiro.util.JdbcUtils; | |
30 | import org.slf4j.Logger; | |
31 | import org.slf4j.LoggerFactory; | |
32 | ||
33 | import javax.sql.DataSource; | |
34 | import java.sql.Connection; | |
35 | import java.sql.PreparedStatement; | |
36 | import java.sql.ResultSet; | |
37 | import java.sql.SQLException; | |
38 | import java.util.Collection; | |
39 | import java.util.LinkedHashSet; | |
40 | import java.util.Set; | |
41 | ||
42 | ||
43 | /** | |
44 | * Realm that allows authentication and authorization via JDBC calls. The default queries suggest a potential schema | |
45 | * for retrieving the user's password for authentication, and querying for a user's roles and permissions. The | |
46 | * default queries can be overridden by setting the query properties of the realm. | |
47 | * <p/> | |
48 | * If the default implementation | |
49 | * of authentication and authorization cannot handle your schema, this class can be subclassed and the | |
50 | * appropriate methods overridden. (usually {@link #doGetAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)}, | |
51 | * {@link #getRoleNamesForUser(java.sql.Connection,String)}, and/or {@link #getPermissions(java.sql.Connection,String,java.util.Collection)} | |
52 | * <p/> | |
53 | * This realm supports caching by extending from {@link org.apache.shiro.realm.AuthorizingRealm}. | |
54 | * | |
55 | * @since 0.2 | |
56 | */ | |
57 | 22 | public class JdbcRealm extends AuthorizingRealm { |
58 | ||
59 | //TODO - complete JavaDoc | |
60 | ||
61 | /*-------------------------------------------- | |
62 | | C O N S T A N T S | | |
63 | ============================================*/ | |
64 | /** | |
65 | * The default query used to retrieve account data for the user. | |
66 | */ | |
67 | protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?"; | |
68 | ||
69 | /** | |
70 | * The default query used to retrieve account data for the user when {@link #saltStyle} is COLUMN. | |
71 | */ | |
72 | protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?"; | |
73 | ||
74 | /** | |
75 | * The default query used to retrieve the roles that apply to a user. | |
76 | */ | |
77 | protected static final String DEFAULT_USER_ROLES_QUERY = "select role_name from user_roles where username = ?"; | |
78 | ||
79 | /** | |
80 | * The default query used to retrieve permissions that apply to a particular role. | |
81 | */ | |
82 | protected static final String DEFAULT_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ?"; | |
83 | ||
84 | 2 | private static final Logger log = LoggerFactory.getLogger(JdbcRealm.class); |
85 | ||
86 | /** | |
87 | * Password hash salt configuration. <ul> | |
88 | * <li>NO_SALT - password hashes are not salted.</li> | |
89 | * <li>CRYPT - password hashes are stored in unix crypt format.</li> | |
90 | * <li>COLUMN - salt is in a separate column in the database.</li> | |
91 | * <li>EXTERNAL - salt is not stored in the database. {@link #getSaltForUser(String)} will be called | |
92 | * to get the salt</li></ul> | |
93 | */ | |
94 | 12 | public enum SaltStyle {NO_SALT, CRYPT, COLUMN, EXTERNAL}; |
95 | ||
96 | /*-------------------------------------------- | |
97 | | I N S T A N C E V A R I A B L E S | | |
98 | ============================================*/ | |
99 | protected DataSource dataSource; | |
100 | ||
101 | 22 | protected String authenticationQuery = DEFAULT_AUTHENTICATION_QUERY; |
102 | ||
103 | 22 | protected String userRolesQuery = DEFAULT_USER_ROLES_QUERY; |
104 | ||
105 | 22 | protected String permissionsQuery = DEFAULT_PERMISSIONS_QUERY; |
106 | ||
107 | 22 | protected boolean permissionsLookupEnabled = false; |
108 | ||
109 | 22 | protected SaltStyle saltStyle = SaltStyle.NO_SALT; |
110 | ||
111 | /*-------------------------------------------- | |
112 | | C O N S T R U C T O R S | | |
113 | ============================================*/ | |
114 | ||
115 | /*-------------------------------------------- | |
116 | | A C C E S S O R S / M O D I F I E R S | | |
117 | ============================================*/ | |
118 | ||
119 | /** | |
120 | * Sets the datasource that should be used to retrieve connections used by this realm. | |
121 | * | |
122 | * @param dataSource the SQL data source. | |
123 | */ | |
124 | public void setDataSource(DataSource dataSource) { | |
125 | 22 | this.dataSource = dataSource; |
126 | 22 | } |
127 | ||
128 | /** | |
129 | * Overrides the default query used to retrieve a user's password during authentication. When using the default | |
130 | * implementation, this query must take the user's username as a single parameter and return a single result | |
131 | * with the user's password as the first column. If you require a solution that does not match this query | |
132 | * structure, you can override {@link #doGetAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)} or | |
133 | * just {@link #getPasswordForUser(java.sql.Connection,String)} | |
134 | * | |
135 | * @param authenticationQuery the query to use for authentication. | |
136 | * @see #DEFAULT_AUTHENTICATION_QUERY | |
137 | */ | |
138 | public void setAuthenticationQuery(String authenticationQuery) { | |
139 | 0 | this.authenticationQuery = authenticationQuery; |
140 | 0 | } |
141 | ||
142 | /** | |
143 | * Overrides the default query used to retrieve a user's roles during authorization. When using the default | |
144 | * implementation, this query must take the user's username as a single parameter and return a row | |
145 | * per role with a single column containing the role name. If you require a solution that does not match this query | |
146 | * structure, you can override {@link #doGetAuthorizationInfo(PrincipalCollection)} or just | |
147 | * {@link #getRoleNamesForUser(java.sql.Connection,String)} | |
148 | * | |
149 | * @param userRolesQuery the query to use for retrieving a user's roles. | |
150 | * @see #DEFAULT_USER_ROLES_QUERY | |
151 | */ | |
152 | public void setUserRolesQuery(String userRolesQuery) { | |
153 | 0 | this.userRolesQuery = userRolesQuery; |
154 | 0 | } |
155 | ||
156 | /** | |
157 | * Overrides the default query used to retrieve a user's permissions during authorization. When using the default | |
158 | * implementation, this query must take a role name as the single parameter and return a row | |
159 | * per permission with three columns containing the fully qualified name of the permission class, the permission | |
160 | * name, and the permission actions (in that order). If you require a solution that does not match this query | |
161 | * structure, you can override {@link #doGetAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)} or just | |
162 | * {@link #getPermissions(java.sql.Connection,String,java.util.Collection)}</p> | |
163 | * <p/> | |
164 | * <b>Permissions are only retrieved if you set {@link #permissionsLookupEnabled} to true. Otherwise, | |
165 | * this query is ignored.</b> | |
166 | * | |
167 | * @param permissionsQuery the query to use for retrieving permissions for a role. | |
168 | * @see #DEFAULT_PERMISSIONS_QUERY | |
169 | * @see #setPermissionsLookupEnabled(boolean) | |
170 | */ | |
171 | public void setPermissionsQuery(String permissionsQuery) { | |
172 | 0 | this.permissionsQuery = permissionsQuery; |
173 | 0 | } |
174 | ||
175 | /** | |
176 | * Enables lookup of permissions during authorization. The default is "false" - meaning that only roles | |
177 | * are associated with a user. Set this to true in order to lookup roles <b>and</b> permissions. | |
178 | * | |
179 | * @param permissionsLookupEnabled true if permissions should be looked up during authorization, or false if only | |
180 | * roles should be looked up. | |
181 | */ | |
182 | public void setPermissionsLookupEnabled(boolean permissionsLookupEnabled) { | |
183 | 4 | this.permissionsLookupEnabled = permissionsLookupEnabled; |
184 | 4 | } |
185 | ||
186 | /** | |
187 | * Sets the salt style. See {@link #saltStyle}. | |
188 | * | |
189 | * @param saltStyle new SaltStyle to set. | |
190 | */ | |
191 | public void setSaltStyle(SaltStyle saltStyle) { | |
192 | 22 | this.saltStyle = saltStyle; |
193 | 22 | if (saltStyle == SaltStyle.COLUMN && authenticationQuery.equals(DEFAULT_AUTHENTICATION_QUERY)) { |
194 | 4 | authenticationQuery = DEFAULT_SALTED_AUTHENTICATION_QUERY; |
195 | } | |
196 | 22 | } |
197 | ||
198 | /*-------------------------------------------- | |
199 | | M E T H O D S | | |
200 | ============================================*/ | |
201 | ||
202 | protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { | |
203 | ||
204 | 22 | UsernamePasswordToken upToken = (UsernamePasswordToken) token; |
205 | 22 | String username = upToken.getUsername(); |
206 | ||
207 | // Null username is invalid | |
208 | 22 | if (username == null) { |
209 | 0 | throw new AccountException("Null usernames are not allowed by this realm."); |
210 | } | |
211 | ||
212 | 22 | Connection conn = null; |
213 | 22 | SimpleAuthenticationInfo info = null; |
214 | try { | |
215 | 22 | conn = dataSource.getConnection(); |
216 | ||
217 | 22 | String password = null; |
218 | 22 | String salt = null; |
219 | 2 | switch (saltStyle) { |
220 | case NO_SALT: | |
221 | 14 | password = getPasswordForUser(conn, username)[0]; |
222 | 12 | break; |
223 | case CRYPT: | |
224 | // TODO: separate password and hash from getPasswordForUser[0] | |
225 | 0 | throw new ConfigurationException("Not implemented yet"); |
226 | //break; | |
227 | case COLUMN: | |
228 | 4 | String[] queryResults = getPasswordForUser(conn, username); |
229 | 4 | password = queryResults[0]; |
230 | 4 | salt = queryResults[1]; |
231 | 4 | break; |
232 | case EXTERNAL: | |
233 | 4 | password = getPasswordForUser(conn, username)[0]; |
234 | 4 | salt = getSaltForUser(username); |
235 | } | |
236 | ||
237 | 20 | if (password == null) { |
238 | 0 | throw new UnknownAccountException("No account found for user [" + username + "]"); |
239 | } | |
240 | ||
241 | 20 | info = new SimpleAuthenticationInfo(username, password.toCharArray(), getName()); |
242 | ||
243 | 20 | if (salt != null) { |
244 | 8 | info.setCredentialsSalt(ByteSource.Util.bytes(salt)); |
245 | } | |
246 | ||
247 | 0 | } catch (SQLException e) { |
248 | 0 | final String message = "There was a SQL error while authenticating user [" + username + "]"; |
249 | 0 | if (log.isErrorEnabled()) { |
250 | 0 | log.error(message, e); |
251 | } | |
252 | ||
253 | // Rethrow any SQL errors as an authentication exception | |
254 | 0 | throw new AuthenticationException(message, e); |
255 | } finally { | |
256 | 22 | JdbcUtils.closeConnection(conn); |
257 | 20 | } |
258 | ||
259 | 20 | return info; |
260 | } | |
261 | ||
262 | private String[] getPasswordForUser(Connection conn, String username) throws SQLException { | |
263 | ||
264 | String[] result; | |
265 | 22 | boolean returningSeparatedSalt = false; |
266 | 22 | switch (saltStyle) { |
267 | case NO_SALT: | |
268 | case CRYPT: | |
269 | case EXTERNAL: | |
270 | 18 | result = new String[1]; |
271 | 18 | break; |
272 | default: | |
273 | 4 | result = new String[2]; |
274 | 4 | returningSeparatedSalt = true; |
275 | } | |
276 | ||
277 | 22 | PreparedStatement ps = null; |
278 | 22 | ResultSet rs = null; |
279 | try { | |
280 | 22 | ps = conn.prepareStatement(authenticationQuery); |
281 | 22 | ps.setString(1, username); |
282 | ||
283 | // Execute query | |
284 | 22 | rs = ps.executeQuery(); |
285 | ||
286 | // Loop over results - although we are only expecting one result, since usernames should be unique | |
287 | 22 | boolean foundResult = false; |
288 | 44 | while (rs.next()) { |
289 | ||
290 | // Check to ensure only one row is processed | |
291 | 24 | if (foundResult) { |
292 | 2 | throw new AuthenticationException("More than one user row found for user [" + username + "]. Usernames must be unique."); |
293 | } | |
294 | ||
295 | 22 | result[0] = rs.getString(1); |
296 | 22 | if (returningSeparatedSalt) { |
297 | 4 | result[1] = rs.getString(2); |
298 | } | |
299 | ||
300 | 22 | foundResult = true; |
301 | } | |
302 | } finally { | |
303 | 22 | JdbcUtils.closeResultSet(rs); |
304 | 22 | JdbcUtils.closeStatement(ps); |
305 | 20 | } |
306 | ||
307 | 20 | return result; |
308 | } | |
309 | ||
310 | /** | |
311 | * This implementation of the interface expects the principals collection to return a String username keyed off of | |
312 | * this realm's {@link #getName() name} | |
313 | * | |
314 | * @see #getAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection) | |
315 | */ | |
316 | @Override | |
317 | protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { | |
318 | ||
319 | //null usernames are invalid | |
320 | 8 | if (principals == null) { |
321 | 0 | throw new AuthorizationException("PrincipalCollection method argument cannot be null."); |
322 | } | |
323 | ||
324 | 8 | String username = (String) getAvailablePrincipal(principals); |
325 | ||
326 | 8 | Connection conn = null; |
327 | 8 | Set<String> roleNames = null; |
328 | 8 | Set<String> permissions = null; |
329 | try { | |
330 | 8 | conn = dataSource.getConnection(); |
331 | ||
332 | // Retrieve roles and permissions from database | |
333 | 8 | roleNames = getRoleNamesForUser(conn, username); |
334 | 8 | if (permissionsLookupEnabled) { |
335 | 4 | permissions = getPermissions(conn, username, roleNames); |
336 | } | |
337 | ||
338 | 0 | } catch (SQLException e) { |
339 | 0 | final String message = "There was a SQL error while authorizing user [" + username + "]"; |
340 | 0 | if (log.isErrorEnabled()) { |
341 | 0 | log.error(message, e); |
342 | } | |
343 | ||
344 | // Rethrow any SQL errors as an authorization exception | |
345 | 0 | throw new AuthorizationException(message, e); |
346 | } finally { | |
347 | 8 | JdbcUtils.closeConnection(conn); |
348 | 8 | } |
349 | ||
350 | 8 | SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roleNames); |
351 | 8 | info.setStringPermissions(permissions); |
352 | 8 | return info; |
353 | ||
354 | } | |
355 | ||
356 | protected Set<String> getRoleNamesForUser(Connection conn, String username) throws SQLException { | |
357 | 8 | PreparedStatement ps = null; |
358 | 8 | ResultSet rs = null; |
359 | 8 | Set<String> roleNames = new LinkedHashSet<String>(); |
360 | try { | |
361 | 8 | ps = conn.prepareStatement(userRolesQuery); |
362 | 8 | ps.setString(1, username); |
363 | ||
364 | // Execute query | |
365 | 8 | rs = ps.executeQuery(); |
366 | ||
367 | // Loop over results and add each returned role to a set | |
368 | 16 | while (rs.next()) { |
369 | ||
370 | 8 | String roleName = rs.getString(1); |
371 | ||
372 | // Add the role to the list of names if it isn't null | |
373 | 8 | if (roleName != null) { |
374 | 8 | roleNames.add(roleName); |
375 | } else { | |
376 | 0 | if (log.isWarnEnabled()) { |
377 | 0 | log.warn("Null role name found while retrieving role names for user [" + username + "]"); |
378 | } | |
379 | } | |
380 | 8 | } |
381 | } finally { | |
382 | 8 | JdbcUtils.closeResultSet(rs); |
383 | 8 | JdbcUtils.closeStatement(ps); |
384 | 8 | } |
385 | 8 | return roleNames; |
386 | } | |
387 | ||
388 | protected Set<String> getPermissions(Connection conn, String username, Collection<String> roleNames) throws SQLException { | |
389 | 4 | PreparedStatement ps = null; |
390 | 4 | Set<String> permissions = new LinkedHashSet<String>(); |
391 | try { | |
392 | 4 | ps = conn.prepareStatement(permissionsQuery); |
393 | 4 | for (String roleName : roleNames) { |
394 | ||
395 | 4 | ps.setString(1, roleName); |
396 | ||
397 | 4 | ResultSet rs = null; |
398 | ||
399 | try { | |
400 | // Execute query | |
401 | 4 | rs = ps.executeQuery(); |
402 | ||
403 | // Loop over results and add each returned role to a set | |
404 | 8 | while (rs.next()) { |
405 | ||
406 | 4 | String permissionString = rs.getString(1); |
407 | ||
408 | // Add the permission to the set of permissions | |
409 | 4 | permissions.add(permissionString); |
410 | 4 | } |
411 | } finally { | |
412 | 4 | JdbcUtils.closeResultSet(rs); |
413 | 4 | } |
414 | ||
415 | 4 | } |
416 | } finally { | |
417 | 4 | JdbcUtils.closeStatement(ps); |
418 | 4 | } |
419 | ||
420 | 4 | return permissions; |
421 | } | |
422 | ||
423 | protected String getSaltForUser(String username) { | |
424 | 4 | return username; |
425 | } | |
426 | ||
427 | } |