View Javadoc

1   /* Copyright 2004, 2005 Acegi Technology Pty Limited
2    *
3    * Licensed under the Apache License, Version 2.0 (the "License");
4    * you may not use this file except in compliance with the License.
5    * You may obtain a copy of the License at
6    *
7    *     http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  
16  package org.acegisecurity.userdetails.jdbc;
17  
18  import java.sql.ResultSet;
19  import java.sql.SQLException;
20  import java.sql.Types;
21  import java.util.List;
22  
23  import javax.sql.DataSource;
24  
25  import org.acegisecurity.GrantedAuthority;
26  import org.acegisecurity.GrantedAuthorityImpl;
27  import org.acegisecurity.userdetails.User;
28  import org.acegisecurity.userdetails.UserDetails;
29  import org.acegisecurity.userdetails.UserDetailsService;
30  import org.acegisecurity.userdetails.UsernameNotFoundException;
31  import org.apache.commons.logging.Log;
32  import org.apache.commons.logging.LogFactory;
33  import org.springframework.context.ApplicationContextException;
34  import org.springframework.dao.DataAccessException;
35  import org.springframework.jdbc.core.SqlParameter;
36  import org.springframework.jdbc.core.support.JdbcDaoSupport;
37  import org.springframework.jdbc.object.MappingSqlQuery;
38  
39  
40  /***
41   * <p>
42   * Retrieves user details (username, password, enabled flag, and authorities)
43   * from a JDBC location.
44   * </p>
45   * 
46   * <p>
47   * A default database structure is assumed, (see {@link
48   * #DEF_USERS_BY_USERNAME_QUERY} and {@link
49   * #DEF_AUTHORITIES_BY_USERNAME_QUERY}, which most users of this class will
50   * need to override, if using an existing scheme. This may be done by setting
51   * the default query strings used. If this does not provide enough
52   * flexibility, another strategy would be to subclass this class and override
53   * the {@link MappingSqlQuery} instances used, via the {@link
54   * #initMappingSqlQueries()} extension point.
55   * </p>
56   * 
57   * <p>
58   * In order to minimise backward compatibility issues, this DAO does not
59   * recognise the expiration of user accounts or the expiration of user
60   * credentials. However, it does recognise and honour the user
61   * enabled/disabled column.
62   * </p>
63   *
64   * @author Ben Alex
65   * @author colin sampaleanu
66   * @version $Id: JdbcDaoImpl.java,v 1.17 2005/12/04 10:48:33 benalex Exp $
67   */
68  public class JdbcDaoImpl extends JdbcDaoSupport implements UserDetailsService {
69      //~ Static fields/initializers =============================================
70  
71      public static final String DEF_USERS_BY_USERNAME_QUERY = "SELECT username,password,enabled FROM users WHERE username = ?";
72      public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY = "SELECT username,authority FROM authorities WHERE username = ?";
73      private static final Log logger = LogFactory.getLog(JdbcDaoImpl.class);
74  
75      //~ Instance fields ========================================================
76  
77      protected MappingSqlQuery authoritiesByUsernameMapping;
78      protected MappingSqlQuery usersByUsernameMapping;
79      private String authoritiesByUsernameQuery;
80      private String rolePrefix = "";
81      private String usersByUsernameQuery;
82      private boolean usernameBasedPrimaryKey = true;
83  
84      //~ Constructors ===========================================================
85  
86      public JdbcDaoImpl() {
87          usersByUsernameQuery = DEF_USERS_BY_USERNAME_QUERY;
88          authoritiesByUsernameQuery = DEF_AUTHORITIES_BY_USERNAME_QUERY;
89      }
90  
91      //~ Methods ================================================================
92  
93      /***
94       * Allows the default query string used to retrieve authorities based on
95       * username to be overriden, if default table or column names need to be
96       * changed. The default query is {@link
97       * #DEF_AUTHORITIES_BY_USERNAME_QUERY}; when modifying this query, ensure
98       * that all returned columns are mapped back to the same column names as
99       * in the default query.
100      *
101      * @param queryString The query string to set
102      */
103     public void setAuthoritiesByUsernameQuery(String queryString) {
104         authoritiesByUsernameQuery = queryString;
105     }
106 
107     public String getAuthoritiesByUsernameQuery() {
108         return authoritiesByUsernameQuery;
109     }
110 
111     /***
112      * Allows a default role prefix to be specified. If this is set to a
113      * non-empty value, then it is automatically prepended to any roles read
114      * in from the db. This may for example be used to add the
115      * <code>ROLE_</code> prefix expected to exist in role names (by default)
116      * by some other Acegi Security framework classes, in the case that the
117      * prefix is not already present in the db.
118      *
119      * @param rolePrefix the new prefix
120      */
121     public void setRolePrefix(String rolePrefix) {
122         this.rolePrefix = rolePrefix;
123     }
124 
125     public String getRolePrefix() {
126         return rolePrefix;
127     }
128 
129     /***
130      * If <code>true</code> (the default), indicates the {@link
131      * #getUsersByUsernameMapping()} returns a username in response to a
132      * query. If <code>false</code>, indicates that a primary key is used
133      * instead. If set to <code>true</code>, the class will use the
134      * database-derived username in the returned <code>UserDetails</code>. If
135      * <code>false</code>, the class will use the {@link
136      * #loadUserByUsername(String)} derived username in the returned
137      * <code>UserDetails</code>.
138      *
139      * @param usernameBasedPrimaryKey <code>true</code> if the mapping queries
140      *        return the username <code>String</code>, or <code>false</code>
141      *        if the mapping returns a database primary key.
142      */
143     public void setUsernameBasedPrimaryKey(boolean usernameBasedPrimaryKey) {
144         this.usernameBasedPrimaryKey = usernameBasedPrimaryKey;
145     }
146 
147     public boolean isUsernameBasedPrimaryKey() {
148         return usernameBasedPrimaryKey;
149     }
150 
151     /***
152      * Allows the default query string used to retrieve users based on username
153      * to be overriden, if default table or column names need to be changed.
154      * The default query is {@link #DEF_USERS_BY_USERNAME_QUERY}; when
155      * modifying this query, ensure that all returned columns are mapped back
156      * to the same column names as in the default query. If the 'enabled'
157      * column does not exist in the source db, a permanent true value for this
158      * column may be returned by using a query similar to <br>
159      * <pre>
160      * "SELECT username,password,'true' as enabled FROM users WHERE username = ?"
161      * </pre>
162      *
163      * @param usersByUsernameQueryString The query string to set
164      */
165     public void setUsersByUsernameQuery(String usersByUsernameQueryString) {
166         this.usersByUsernameQuery = usersByUsernameQueryString;
167     }
168 
169     public String getUsersByUsernameQuery() {
170         return usersByUsernameQuery;
171     }
172 
173     public UserDetails loadUserByUsername(String username)
174         throws UsernameNotFoundException, DataAccessException {
175         List users = usersByUsernameMapping.execute(username);
176 
177         if (users.size() == 0) {
178             throw new UsernameNotFoundException("User not found");
179         }
180 
181         UserDetails user = (UserDetails) users.get(0); // contains no GrantedAuthority[]
182 
183         List dbAuths = authoritiesByUsernameMapping.execute(user.getUsername());
184 
185         if (dbAuths.size() == 0) {
186             throw new UsernameNotFoundException("User has no GrantedAuthority");
187         }
188 
189         GrantedAuthority[] arrayAuths = {};
190 
191         addCustomAuthorities(user.getUsername(), dbAuths);
192 
193         arrayAuths = (GrantedAuthority[]) dbAuths.toArray(arrayAuths);
194 
195         String returnUsername = user.getUsername();
196 
197         if (!usernameBasedPrimaryKey) {
198             returnUsername = username;
199         }
200 
201         return new User(returnUsername, user.getPassword(), user.isEnabled(),
202             true, true, true, arrayAuths);
203     }
204 
205     /***
206      * Allows subclasses to add their own granted authorities to the list to be
207      * returned in the <code>User</code>.
208      *
209      * @param username the username, for use by finder methods
210      * @param authorities the current granted authorities, as populated from
211      *        the <code>authoritiesByUsername</code> mapping
212      */
213     protected void addCustomAuthorities(String username, List authorities) {}
214 
215     protected void initDao() throws ApplicationContextException {
216         initMappingSqlQueries();
217     }
218 
219     /***
220      * Extension point to allow other MappingSqlQuery objects to be substituted
221      * in a subclass
222      */
223     protected void initMappingSqlQueries() {
224         this.usersByUsernameMapping = new UsersByUsernameMapping(getDataSource());
225         this.authoritiesByUsernameMapping = new AuthoritiesByUsernameMapping(getDataSource());
226     }
227 
228     //~ Inner Classes ==========================================================
229 
230     /***
231      * Query object to look up a user's authorities.
232      */
233     protected class AuthoritiesByUsernameMapping extends MappingSqlQuery {
234         protected AuthoritiesByUsernameMapping(DataSource ds) {
235             super(ds, authoritiesByUsernameQuery);
236             declareParameter(new SqlParameter(Types.VARCHAR));
237             compile();
238         }
239 
240         protected Object mapRow(ResultSet rs, int rownum)
241             throws SQLException {
242             String roleName = rolePrefix + rs.getString(2);
243             GrantedAuthorityImpl authority = new GrantedAuthorityImpl(roleName);
244 
245             return authority;
246         }
247     }
248 
249     /***
250      * Query object to look up a user.
251      */
252     protected class UsersByUsernameMapping extends MappingSqlQuery {
253         protected UsersByUsernameMapping(DataSource ds) {
254             super(ds, usersByUsernameQuery);
255             declareParameter(new SqlParameter(Types.VARCHAR));
256             compile();
257         }
258 
259         protected Object mapRow(ResultSet rs, int rownum)
260             throws SQLException {
261             String username = rs.getString(1);
262             String password = rs.getString(2);
263             boolean enabled = rs.getBoolean(3);
264             UserDetails user = new User(username, password, enabled, true,
265                     true, true,
266                     new GrantedAuthority[] {new GrantedAuthorityImpl("HOLDER")});
267 
268             return user;
269         }
270     }
271 }