View Javadoc

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  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      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      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     protected String authenticationQuery = DEFAULT_AUTHENTICATION_QUERY;
102 
103     protected String userRolesQuery = DEFAULT_USER_ROLES_QUERY;
104 
105     protected String permissionsQuery = DEFAULT_PERMISSIONS_QUERY;
106 
107     protected boolean permissionsLookupEnabled = false;
108     
109     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         this.dataSource = dataSource;
126     }
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         this.authenticationQuery = authenticationQuery;
140     }
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         this.userRolesQuery = userRolesQuery;
154     }
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         this.permissionsQuery = permissionsQuery;
173     }
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         this.permissionsLookupEnabled = permissionsLookupEnabled;
184     }
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         this.saltStyle = saltStyle;
193         if (saltStyle == SaltStyle.COLUMN && authenticationQuery.equals(DEFAULT_AUTHENTICATION_QUERY)) {
194             authenticationQuery = DEFAULT_SALTED_AUTHENTICATION_QUERY;
195         }
196     }
197 
198     /*--------------------------------------------
199     |               M E T H O D S               |
200     ============================================*/
201 
202     protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
203 
204         UsernamePasswordToken upToken = (UsernamePasswordToken) token;
205         String username = upToken.getUsername();
206 
207         // Null username is invalid
208         if (username == null) {
209             throw new AccountException("Null usernames are not allowed by this realm.");
210         }
211 
212         Connection conn = null;
213         SimpleAuthenticationInfo info = null;
214         try {
215             conn = dataSource.getConnection();
216 
217             String password = null;
218             String salt = null;
219             switch (saltStyle) {
220             case NO_SALT:
221                 password = getPasswordForUser(conn, username)[0];
222                 break;
223             case CRYPT:
224                 // TODO: separate password and hash from getPasswordForUser[0]
225                 throw new ConfigurationException("Not implemented yet");
226                 //break;
227             case COLUMN:
228                 String[] queryResults = getPasswordForUser(conn, username);
229                 password = queryResults[0];
230                 salt = queryResults[1];
231                 break;
232             case EXTERNAL:
233                 password = getPasswordForUser(conn, username)[0];
234                 salt = getSaltForUser(username);
235             }
236 
237             if (password == null) {
238                 throw new UnknownAccountException("No account found for user [" + username + "]");
239             }
240 
241             info = new SimpleAuthenticationInfo(username, password.toCharArray(), getName());
242             
243             if (salt != null) {
244                 info.setCredentialsSalt(ByteSource.Util.bytes(salt));
245             }
246 
247         } catch (SQLException e) {
248             final String message = "There was a SQL error while authenticating user [" + username + "]";
249             if (log.isErrorEnabled()) {
250                 log.error(message, e);
251             }
252 
253             // Rethrow any SQL errors as an authentication exception
254             throw new AuthenticationException(message, e);
255         } finally {
256             JdbcUtils.closeConnection(conn);
257         }
258 
259         return info;
260     }
261 
262     private String[] getPasswordForUser(Connection conn, String username) throws SQLException {
263 
264         String[] result;
265         boolean returningSeparatedSalt = false;
266         switch (saltStyle) {
267         case NO_SALT:
268         case CRYPT:
269         case EXTERNAL:
270             result = new String[1];
271             break;
272         default:
273             result = new String[2];
274             returningSeparatedSalt = true;
275         }
276         
277         PreparedStatement ps = null;
278         ResultSet rs = null;
279         try {
280             ps = conn.prepareStatement(authenticationQuery);
281             ps.setString(1, username);
282 
283             // Execute query
284             rs = ps.executeQuery();
285 
286             // Loop over results - although we are only expecting one result, since usernames should be unique
287             boolean foundResult = false;
288             while (rs.next()) {
289 
290                 // Check to ensure only one row is processed
291                 if (foundResult) {
292                     throw new AuthenticationException("More than one user row found for user [" + username + "]. Usernames must be unique.");
293                 }
294 
295                 result[0] = rs.getString(1);
296                 if (returningSeparatedSalt) {
297                     result[1] = rs.getString(2);
298                 }
299 
300                 foundResult = true;
301             }
302         } finally {
303             JdbcUtils.closeResultSet(rs);
304             JdbcUtils.closeStatement(ps);
305         }
306 
307         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         if (principals == null) {
321             throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
322         }
323 
324         String username = (String) getAvailablePrincipal(principals);
325 
326         Connection conn = null;
327         Set<String> roleNames = null;
328         Set<String> permissions = null;
329         try {
330             conn = dataSource.getConnection();
331 
332             // Retrieve roles and permissions from database
333             roleNames = getRoleNamesForUser(conn, username);
334             if (permissionsLookupEnabled) {
335                 permissions = getPermissions(conn, username, roleNames);
336             }
337 
338         } catch (SQLException e) {
339             final String message = "There was a SQL error while authorizing user [" + username + "]";
340             if (log.isErrorEnabled()) {
341                 log.error(message, e);
342             }
343 
344             // Rethrow any SQL errors as an authorization exception
345             throw new AuthorizationException(message, e);
346         } finally {
347             JdbcUtils.closeConnection(conn);
348         }
349 
350         SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roleNames);
351         info.setStringPermissions(permissions);
352         return info;
353 
354     }
355 
356     protected Set<String> getRoleNamesForUser(Connection conn, String username) throws SQLException {
357         PreparedStatement ps = null;
358         ResultSet rs = null;
359         Set<String> roleNames = new LinkedHashSet<String>();
360         try {
361             ps = conn.prepareStatement(userRolesQuery);
362             ps.setString(1, username);
363 
364             // Execute query
365             rs = ps.executeQuery();
366 
367             // Loop over results and add each returned role to a set
368             while (rs.next()) {
369 
370                 String roleName = rs.getString(1);
371 
372                 // Add the role to the list of names if it isn't null
373                 if (roleName != null) {
374                     roleNames.add(roleName);
375                 } else {
376                     if (log.isWarnEnabled()) {
377                         log.warn("Null role name found while retrieving role names for user [" + username + "]");
378                     }
379                 }
380             }
381         } finally {
382             JdbcUtils.closeResultSet(rs);
383             JdbcUtils.closeStatement(ps);
384         }
385         return roleNames;
386     }
387 
388     protected Set<String> getPermissions(Connection conn, String username, Collection<String> roleNames) throws SQLException {
389         PreparedStatement ps = null;
390         Set<String> permissions = new LinkedHashSet<String>();
391         try {
392             ps = conn.prepareStatement(permissionsQuery);
393             for (String roleName : roleNames) {
394 
395                 ps.setString(1, roleName);
396 
397                 ResultSet rs = null;
398 
399                 try {
400                     // Execute query
401                     rs = ps.executeQuery();
402 
403                     // Loop over results and add each returned role to a set
404                     while (rs.next()) {
405 
406                         String permissionString = rs.getString(1);
407 
408                         // Add the permission to the set of permissions
409                         permissions.add(permissionString);
410                     }
411                 } finally {
412                     JdbcUtils.closeResultSet(rs);
413                 }
414 
415             }
416         } finally {
417             JdbcUtils.closeStatement(ps);
418         }
419 
420         return permissions;
421     }
422     
423     protected String getSaltForUser(String username) {
424         return username;
425     }
426 
427 }