001 /*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements. See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership. The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License. You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied. See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019 package org.apache.shiro.realm.jdbc;
020
021 import org.apache.shiro.authc.*;
022 import org.apache.shiro.authz.AuthorizationException;
023 import org.apache.shiro.authz.AuthorizationInfo;
024 import org.apache.shiro.authz.SimpleAuthorizationInfo;
025 import org.apache.shiro.realm.AuthorizingRealm;
026 import org.apache.shiro.subject.PrincipalCollection;
027 import org.apache.shiro.util.JdbcUtils;
028 import org.slf4j.Logger;
029 import org.slf4j.LoggerFactory;
030
031 import javax.sql.DataSource;
032 import java.sql.Connection;
033 import java.sql.PreparedStatement;
034 import java.sql.ResultSet;
035 import java.sql.SQLException;
036 import java.util.Collection;
037 import java.util.LinkedHashSet;
038 import java.util.Set;
039
040
041 /**
042 * Realm that allows authentication and authorization via JDBC calls. The default queries suggest a potential schema
043 * for retrieving the user's password for authentication, and querying for a user's roles and permissions. The
044 * default queries can be overridden by setting the query properties of the realm.
045 * <p/>
046 * If the default implementation
047 * of authentication and authorization cannot handle your schema, this class can be subclassed and the
048 * appropriate methods overridden. (usually {@link #doGetAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)},
049 * {@link #getRoleNamesForUser(java.sql.Connection,String)}, and/or {@link #getPermissions(java.sql.Connection,String,java.util.Collection)}
050 * <p/>
051 * This realm supports caching by extending from {@link org.apache.shiro.realm.AuthorizingRealm}.
052 *
053 * @author Jeremy Haile
054 * @since 0.2
055 */
056 public class JdbcRealm extends AuthorizingRealm {
057
058 //TODO - complete JavaDoc
059
060 /*--------------------------------------------
061 | C O N S T A N T S |
062 ============================================*/
063 /**
064 * The default query used to retrieve account data for the user.
065 */
066 protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?";
067
068 /**
069 * The default query used to retrieve the roles that apply to a user.
070 */
071 protected static final String DEFAULT_USER_ROLES_QUERY = "select role_name from user_roles where username = ?";
072
073 /**
074 * The default query used to retrieve permissions that apply to a particular role.
075 */
076 protected static final String DEFAULT_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ?";
077
078 private static final Logger log = LoggerFactory.getLogger(JdbcRealm.class);
079
080 /*--------------------------------------------
081 | I N S T A N C E V A R I A B L E S |
082 ============================================*/
083 protected DataSource dataSource;
084
085 protected String authenticationQuery = DEFAULT_AUTHENTICATION_QUERY;
086
087 protected String userRolesQuery = DEFAULT_USER_ROLES_QUERY;
088
089 protected String permissionsQuery = DEFAULT_PERMISSIONS_QUERY;
090
091 protected boolean permissionsLookupEnabled = false;
092
093 /*--------------------------------------------
094 | C O N S T R U C T O R S |
095 ============================================*/
096
097 /*--------------------------------------------
098 | A C C E S S O R S / M O D I F I E R S |
099 ============================================*/
100
101 /**
102 * Sets the datasource that should be used to retrieve connections used by this realm.
103 *
104 * @param dataSource the SQL data source.
105 */
106 public void setDataSource(DataSource dataSource) {
107 this.dataSource = dataSource;
108 }
109
110 /**
111 * Overrides the default query used to retrieve a user's password during authentication. When using the default
112 * implementation, this query must take the user's username as a single parameter and return a single result
113 * with the user's password as the first column. If you require a solution that does not match this query
114 * structure, you can override {@link #doGetAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)} or
115 * just {@link #getPasswordForUser(java.sql.Connection,String)}
116 *
117 * @param authenticationQuery the query to use for authentication.
118 * @see #DEFAULT_AUTHENTICATION_QUERY
119 */
120 public void setAuthenticationQuery(String authenticationQuery) {
121 this.authenticationQuery = authenticationQuery;
122 }
123
124 /**
125 * Overrides the default query used to retrieve a user's roles during authorization. When using the default
126 * implementation, this query must take the user's username as a single parameter and return a row
127 * per role with a single column containing the role name. If you require a solution that does not match this query
128 * structure, you can override {@link #doGetAuthorizationInfo(PrincipalCollection)} or just
129 * {@link #getRoleNamesForUser(java.sql.Connection,String)}
130 *
131 * @param userRolesQuery the query to use for retrieving a user's roles.
132 * @see #DEFAULT_USER_ROLES_QUERY
133 */
134 public void setUserRolesQuery(String userRolesQuery) {
135 this.userRolesQuery = userRolesQuery;
136 }
137
138 /**
139 * Overrides the default query used to retrieve a user's permissions during authorization. When using the default
140 * implementation, this query must take a role name as the single parameter and return a row
141 * per permission with three columns containing the fully qualified name of the permission class, the permission
142 * name, and the permission actions (in that order). If you require a solution that does not match this query
143 * structure, you can override {@link #doGetAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)} or just
144 * {@link #getPermissions(java.sql.Connection,String,java.util.Collection)}</p>
145 * <p/>
146 * <b>Permissions are only retrieved if you set {@link #permissionsLookupEnabled} to true. Otherwise,
147 * this query is ignored.</b>
148 *
149 * @param permissionsQuery the query to use for retrieving permissions for a role.
150 * @see #DEFAULT_PERMISSIONS_QUERY
151 * @see #setPermissionsLookupEnabled(boolean)
152 */
153 public void setPermissionsQuery(String permissionsQuery) {
154 this.permissionsQuery = permissionsQuery;
155 }
156
157 /**
158 * Enables lookup of permissions during authorization. The default is "false" - meaning that only roles
159 * are associated with a user. Set this to true in order to lookup roles <b>and</b> permissions.
160 *
161 * @param permissionsLookupEnabled true if permissions should be looked up during authorization, or false if only
162 * roles should be looked up.
163 */
164 public void setPermissionsLookupEnabled(boolean permissionsLookupEnabled) {
165 this.permissionsLookupEnabled = permissionsLookupEnabled;
166 }
167
168 /*--------------------------------------------
169 | M E T H O D S |
170 ============================================*/
171
172 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
173
174 UsernamePasswordToken upToken = (UsernamePasswordToken) token;
175 String username = upToken.getUsername();
176
177 // Null username is invalid
178 if (username == null) {
179 throw new AccountException("Null usernames are not allowed by this realm.");
180 }
181
182 Connection conn = null;
183 AuthenticationInfo info = null;
184 try {
185 conn = dataSource.getConnection();
186
187 String password = getPasswordForUser(conn, username);
188
189 if (password == null) {
190 throw new UnknownAccountException("No account found for user [" + username + "]");
191 }
192
193 info = buildAuthenticationInfo(username, password.toCharArray());
194
195 } catch (SQLException e) {
196 final String message = "There was a SQL error while authenticating user [" + username + "]";
197 if (log.isErrorEnabled()) {
198 log.error(message, e);
199 }
200
201 // Rethrow any SQL errors as an authentication exception
202 throw new AuthenticationException(message, e);
203 } finally {
204 JdbcUtils.closeConnection(conn);
205 }
206
207 return info;
208 }
209
210 protected AuthenticationInfo buildAuthenticationInfo(String username, char[] password) {
211 return new SimpleAuthenticationInfo(username, password, getName());
212 }
213
214 private String getPasswordForUser(Connection conn, String username) throws SQLException {
215
216 PreparedStatement ps = null;
217 ResultSet rs = null;
218 String password = null;
219 try {
220 ps = conn.prepareStatement(authenticationQuery);
221 ps.setString(1, username);
222
223 // Execute query
224 rs = ps.executeQuery();
225
226 // Loop over results - although we are only expecting one result, since usernames should be unique
227 boolean foundResult = false;
228 while (rs.next()) {
229
230 // Check to ensure only one row is processed
231 if (foundResult) {
232 throw new AuthenticationException("More than one user row found for user [" + username + "]. Usernames must be unique.");
233 }
234
235 password = rs.getString(1);
236
237 foundResult = true;
238 }
239 } finally {
240 JdbcUtils.closeResultSet(rs);
241 JdbcUtils.closeStatement(ps);
242 }
243
244 return password;
245 }
246
247 /**
248 * This implementation of the interface expects the principals collection to return a String username keyed off of
249 * this realm's {@link #getName() name}
250 *
251 * @see #getAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)
252 */
253 @Override
254 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
255
256 //null usernames are invalid
257 if (principals == null) {
258 throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
259 }
260
261 String username = (String) getAvailablePrincipal(principals);
262
263 Connection conn = null;
264 Set<String> roleNames = null;
265 Set<String> permissions = null;
266 try {
267 conn = dataSource.getConnection();
268
269 // Retrieve roles and permissions from database
270 roleNames = getRoleNamesForUser(conn, username);
271 if (permissionsLookupEnabled) {
272 permissions = getPermissions(conn, username, roleNames);
273 }
274
275 } catch (SQLException e) {
276 final String message = "There was a SQL error while authorizing user [" + username + "]";
277 if (log.isErrorEnabled()) {
278 log.error(message, e);
279 }
280
281 // Rethrow any SQL errors as an authorization exception
282 throw new AuthorizationException(message, e);
283 } finally {
284 JdbcUtils.closeConnection(conn);
285 }
286
287 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roleNames);
288 info.setStringPermissions(permissions);
289 return info;
290
291 }
292
293 protected Set<String> getRoleNamesForUser(Connection conn, String username) throws SQLException {
294 PreparedStatement ps = null;
295 ResultSet rs = null;
296 Set<String> roleNames = new LinkedHashSet<String>();
297 try {
298 ps = conn.prepareStatement(userRolesQuery);
299 ps.setString(1, username);
300
301 // Execute query
302 rs = ps.executeQuery();
303
304 // Loop over results and add each returned role to a set
305 while (rs.next()) {
306
307 String roleName = rs.getString(1);
308
309 // Add the role to the list of names if it isn't null
310 if (roleName != null) {
311 roleNames.add(roleName);
312 } else {
313 if (log.isWarnEnabled()) {
314 log.warn("Null role name found while retrieving role names for user [" + username + "]");
315 }
316 }
317 }
318 } finally {
319 JdbcUtils.closeResultSet(rs);
320 JdbcUtils.closeStatement(ps);
321 }
322 return roleNames;
323 }
324
325 protected Set<String> getPermissions(Connection conn, String username, Collection<String> roleNames) throws SQLException {
326 PreparedStatement ps = null;
327 ResultSet rs = null;
328 Set<String> permissions = new LinkedHashSet<String>();
329 try {
330 for (String roleName : roleNames) {
331
332 ps = conn.prepareStatement(permissionsQuery);
333 ps.setString(1, roleName);
334
335 // Execute query
336 rs = ps.executeQuery();
337
338 // Loop over results and add each returned role to a set
339 while (rs.next()) {
340
341 String permissionString = rs.getString(1);
342
343 // Add the permission to the set of permissions
344 permissions.add(permissionString);
345 }
346
347 }
348 } finally {
349 JdbcUtils.closeResultSet(rs);
350 JdbcUtils.closeStatement(ps);
351 }
352
353 return permissions;
354 }
355
356 }