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