Coverage Report - org.apache.shiro.realm.jdbc.JdbcRealm
 
Classes in this File Line Coverage Branch Coverage Complexity
JdbcRealm
83%
109/131
65%
29/44
3.75
JdbcRealm$1
100%
1/1
N/A
3.75
JdbcRealm$SaltStyle
100%
1/1
N/A
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  11
 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  1
     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  6
     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  11
     protected String authenticationQuery = DEFAULT_AUTHENTICATION_QUERY;
 102  
 
 103  11
     protected String userRolesQuery = DEFAULT_USER_ROLES_QUERY;
 104  
 
 105  11
     protected String permissionsQuery = DEFAULT_PERMISSIONS_QUERY;
 106  
 
 107  11
     protected boolean permissionsLookupEnabled = false;
 108  
     
 109  11
     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  11
         this.dataSource = dataSource;
 126  11
     }
 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  2
         this.permissionsLookupEnabled = permissionsLookupEnabled;
 184  2
     }
 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  11
         this.saltStyle = saltStyle;
 193  11
         if (saltStyle == SaltStyle.COLUMN && authenticationQuery.equals(DEFAULT_AUTHENTICATION_QUERY)) {
 194  2
             authenticationQuery = DEFAULT_SALTED_AUTHENTICATION_QUERY;
 195  
         }
 196  11
     }
 197  
 
 198  
     /*--------------------------------------------
 199  
     |               M E T H O D S               |
 200  
     ============================================*/
 201  
 
 202  
     protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
 203  
 
 204  11
         UsernamePasswordToken upToken = (UsernamePasswordToken) token;
 205  11
         String username = upToken.getUsername();
 206  
 
 207  
         // Null username is invalid
 208  11
         if (username == null) {
 209  0
             throw new AccountException("Null usernames are not allowed by this realm.");
 210  
         }
 211  
 
 212  11
         Connection conn = null;
 213  11
         SimpleAuthenticationInfo info = null;
 214  
         try {
 215  11
             conn = dataSource.getConnection();
 216  
 
 217  11
             String password = null;
 218  11
             String salt = null;
 219  1
             switch (saltStyle) {
 220  
             case NO_SALT:
 221  7
                 password = getPasswordForUser(conn, username)[0];
 222  6
                 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  2
                 String[] queryResults = getPasswordForUser(conn, username);
 229  2
                 password = queryResults[0];
 230  2
                 salt = queryResults[1];
 231  2
                 break;
 232  
             case EXTERNAL:
 233  2
                 password = getPasswordForUser(conn, username)[0];
 234  2
                 salt = getSaltForUser(username);
 235  
             }
 236  
 
 237  10
             if (password == null) {
 238  0
                 throw new UnknownAccountException("No account found for user [" + username + "]");
 239  
             }
 240  
 
 241  10
             info = new SimpleAuthenticationInfo(username, password.toCharArray(), getName());
 242  
             
 243  10
             if (salt != null) {
 244  4
                 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  11
             JdbcUtils.closeConnection(conn);
 257  10
         }
 258  
 
 259  10
         return info;
 260  
     }
 261  
 
 262  
     private String[] getPasswordForUser(Connection conn, String username) throws SQLException {
 263  
 
 264  
         String[] result;
 265  11
         boolean returningSeparatedSalt = false;
 266  11
         switch (saltStyle) {
 267  
         case NO_SALT:
 268  
         case CRYPT:
 269  
         case EXTERNAL:
 270  9
             result = new String[1];
 271  9
             break;
 272  
         default:
 273  2
             result = new String[2];
 274  2
             returningSeparatedSalt = true;
 275  
         }
 276  
         
 277  11
         PreparedStatement ps = null;
 278  11
         ResultSet rs = null;
 279  
         try {
 280  11
             ps = conn.prepareStatement(authenticationQuery);
 281  11
             ps.setString(1, username);
 282  
 
 283  
             // Execute query
 284  11
             rs = ps.executeQuery();
 285  
 
 286  
             // Loop over results - although we are only expecting one result, since usernames should be unique
 287  11
             boolean foundResult = false;
 288  22
             while (rs.next()) {
 289  
 
 290  
                 // Check to ensure only one row is processed
 291  12
                 if (foundResult) {
 292  1
                     throw new AuthenticationException("More than one user row found for user [" + username + "]. Usernames must be unique.");
 293  
                 }
 294  
 
 295  11
                 result[0] = rs.getString(1);
 296  11
                 if (returningSeparatedSalt) {
 297  2
                     result[1] = rs.getString(2);
 298  
                 }
 299  
 
 300  11
                 foundResult = true;
 301  
             }
 302  
         } finally {
 303  11
             JdbcUtils.closeResultSet(rs);
 304  11
             JdbcUtils.closeStatement(ps);
 305  10
         }
 306  
 
 307  10
         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  4
         if (principals == null) {
 321  0
             throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
 322  
         }
 323  
 
 324  4
         String username = (String) getAvailablePrincipal(principals);
 325  
 
 326  4
         Connection conn = null;
 327  4
         Set<String> roleNames = null;
 328  4
         Set<String> permissions = null;
 329  
         try {
 330  4
             conn = dataSource.getConnection();
 331  
 
 332  
             // Retrieve roles and permissions from database
 333  4
             roleNames = getRoleNamesForUser(conn, username);
 334  4
             if (permissionsLookupEnabled) {
 335  2
                 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  4
             JdbcUtils.closeConnection(conn);
 348  4
         }
 349  
 
 350  4
         SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roleNames);
 351  4
         info.setStringPermissions(permissions);
 352  4
         return info;
 353  
 
 354  
     }
 355  
 
 356  
     protected Set<String> getRoleNamesForUser(Connection conn, String username) throws SQLException {
 357  4
         PreparedStatement ps = null;
 358  4
         ResultSet rs = null;
 359  4
         Set<String> roleNames = new LinkedHashSet<String>();
 360  
         try {
 361  4
             ps = conn.prepareStatement(userRolesQuery);
 362  4
             ps.setString(1, username);
 363  
 
 364  
             // Execute query
 365  4
             rs = ps.executeQuery();
 366  
 
 367  
             // Loop over results and add each returned role to a set
 368  8
             while (rs.next()) {
 369  
 
 370  4
                 String roleName = rs.getString(1);
 371  
 
 372  
                 // Add the role to the list of names if it isn't null
 373  4
                 if (roleName != null) {
 374  4
                     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  4
             }
 381  
         } finally {
 382  4
             JdbcUtils.closeResultSet(rs);
 383  4
             JdbcUtils.closeStatement(ps);
 384  4
         }
 385  4
         return roleNames;
 386  
     }
 387  
 
 388  
     protected Set<String> getPermissions(Connection conn, String username, Collection<String> roleNames) throws SQLException {
 389  2
         PreparedStatement ps = null;
 390  2
         Set<String> permissions = new LinkedHashSet<String>();
 391  
         try {
 392  2
             ps = conn.prepareStatement(permissionsQuery);
 393  2
             for (String roleName : roleNames) {
 394  
 
 395  2
                 ps.setString(1, roleName);
 396  
 
 397  2
                 ResultSet rs = null;
 398  
 
 399  
                 try {
 400  
                     // Execute query
 401  2
                     rs = ps.executeQuery();
 402  
 
 403  
                     // Loop over results and add each returned role to a set
 404  4
                     while (rs.next()) {
 405  
 
 406  2
                         String permissionString = rs.getString(1);
 407  
 
 408  
                         // Add the permission to the set of permissions
 409  2
                         permissions.add(permissionString);
 410  2
                     }
 411  
                 } finally {
 412  2
                     JdbcUtils.closeResultSet(rs);
 413  2
                 }
 414  
 
 415  2
             }
 416  
         } finally {
 417  2
             JdbcUtils.closeStatement(ps);
 418  2
         }
 419  
 
 420  2
         return permissions;
 421  
     }
 422  
     
 423  
     protected String getSaltForUser(String username) {
 424  2
         return username;
 425  
     }
 426  
 
 427  
 }