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.lang.codec.Base64;
032import org.apache.shiro.config.ConfigurationException;
033import org.apache.shiro.realm.AuthorizingRealm;
034import org.apache.shiro.subject.PrincipalCollection;
035import org.apache.shiro.lang.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 * Realm that allows authentication and authorization via JDBC calls.  The default queries suggest a potential schema
051 * for retrieving the user's password for authentication, and querying for a user's roles and permissions.  The
052 * default queries can be overridden by setting the query properties of the realm.
053 * <p/>
054 * If the default implementation
055 * of authentication and authorization cannot handle your schema, this class can be subclassed and the
056 * appropriate methods overridden. (usually {@link #doGetAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)},
057 * {@link #getRoleNamesForUser(java.sql.Connection, String)},
058 * 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    /*--------------------------------------------
067    |             C O N S T A N T S             |
068    ============================================*/
069    /**
070     * The default query used to retrieve account data for the user.
071     */
072    protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?";
073
074    /**
075     * The default query used to retrieve account data for the user when {@link #saltStyle} is COLUMN.
076     */
077    protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY
078            = "select password, password_salt from users where username = ?";
079
080    /**
081     * The default query used to retrieve the roles that apply to a user.
082     */
083    protected static final String DEFAULT_USER_ROLES_QUERY = "select role_name from user_roles where username = ?";
084
085    /**
086     * The default query used to retrieve permissions that apply to a particular role.
087     */
088    protected static final String DEFAULT_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ?";
089
090    private static final Logger LOGGER = LoggerFactory.getLogger(JdbcRealm.class);
091
092    /**
093     * Password hash salt configuration. <ul>
094     * <li>NO_SALT - password hashes are not salted.</li>
095     * <li>CRYPT - password hashes are stored in unix crypt format.</li>
096     * <li>COLUMN - salt is in a separate column in the database.</li>
097     * <li>EXTERNAL - salt is not stored in the database. {@link #getSaltForUser(String)} will be called
098     * to get the salt</li></ul>
099     */
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}