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