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    }