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