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.acl.basic.jdbc;
17
18 import org.acegisecurity.acl.basic.AclObjectIdentity;
19 import org.acegisecurity.acl.basic.BasicAclDao;
20 import org.acegisecurity.acl.basic.BasicAclEntry;
21 import org.acegisecurity.acl.basic.NamedEntityObjectIdentity;
22
23 import org.apache.commons.logging.Log;
24 import org.apache.commons.logging.LogFactory;
25
26 import org.springframework.context.ApplicationContextException;
27
28 import org.springframework.jdbc.core.SqlParameter;
29 import org.springframework.jdbc.core.support.JdbcDaoSupport;
30 import org.springframework.jdbc.object.MappingSqlQuery;
31
32 import org.springframework.util.Assert;
33
34 import java.sql.ResultSet;
35 import java.sql.SQLException;
36 import java.sql.Types;
37
38 import java.util.List;
39 import java.util.Vector;
40
41 import javax.sql.DataSource;
42
43
44 /**
45 * Retrieves ACL details from a JDBC location.
46 * <p>
47 * A default database structure is assumed. This may be overridden by setting the default query strings to use.
48 * If this does not provide enough flexibility, another strategy would be to subclass this class and override the
49 * {@link MappingSqlQuery} instance used, via the {@link #initMappingSqlQueries()} extension point.
50 * </p>
51 */
52 public class JdbcDaoImpl extends JdbcDaoSupport implements BasicAclDao {
53 //~ Static fields/initializers =====================================================================================
54
55 public static final String RECIPIENT_USED_FOR_INHERITENCE_MARKER = "___INHERITENCE_MARKER_ONLY___";
56 public static final String DEF_ACLS_BY_OBJECT_IDENTITY_QUERY =
57 "SELECT RECIPIENT, MASK FROM acl_permission WHERE acl_object_identity = ?";
58 public static final String DEF_OBJECT_PROPERTIES_QUERY =
59 "SELECT CHILD.ID, "
60 + "CHILD.OBJECT_IDENTITY, "
61 + "CHILD.ACL_CLASS, "
62 + "PARENT.OBJECT_IDENTITY as PARENT_OBJECT_IDENTITY "
63 + "FROM acl_object_identity as CHILD "
64 + "LEFT OUTER JOIN acl_object_identity as PARENT ON CHILD.parent_object=PARENT.id "
65 + "WHERE CHILD.object_identity = ?";
66 private static final Log logger = LogFactory.getLog(JdbcDaoImpl.class);
67
68 //~ Instance fields ================================================================================================
69
70 protected MappingSqlQuery aclsByObjectIdentity;
71 protected MappingSqlQuery objectProperties;
72 private String aclsByObjectIdentityQuery;
73 private String objectPropertiesQuery;
74
75 //~ Constructors ===================================================================================================
76
77 public JdbcDaoImpl() {
78 aclsByObjectIdentityQuery = DEF_ACLS_BY_OBJECT_IDENTITY_QUERY;
79 objectPropertiesQuery = DEF_OBJECT_PROPERTIES_QUERY;
80 }
81
82 //~ Methods ========================================================================================================
83
84 /**
85 * Responsible for covering a <code>AclObjectIdentity</code> to a <code>String</code> that can be located
86 * in the RDBMS.
87 *
88 * @param aclObjectIdentity to locate
89 *
90 * @return the object identity as a <code>String</code>
91 */
92 protected String convertAclObjectIdentityToString(AclObjectIdentity aclObjectIdentity) {
93 // Ensure we can process this type of AclObjectIdentity
94 Assert.isInstanceOf(NamedEntityObjectIdentity.class, aclObjectIdentity,
95 "Only aclObjectIdentity of type NamedEntityObjectIdentity supported (was passed: " + aclObjectIdentity
96 + ")");
97
98 NamedEntityObjectIdentity neoi = (NamedEntityObjectIdentity) aclObjectIdentity;
99
100 // Compose the String we expect to find in the RDBMS
101 return neoi.getClassname() + ":" + neoi.getId();
102 }
103
104 /**
105 * Constructs an individual <code>BasicAclEntry</code> from the passed <code>AclDetailsHolder</code>s.<P>Guarantees
106 * to never return <code>null</code> (exceptions are thrown in the event of any issues).</p>
107 *
108 * @param propertiesInformation mandatory information about which instance to create, the object identity, and the
109 * parent object identity (<code>null</code> or empty <code>String</code>s prohibited for
110 * <code>aclClass</code> and <code>aclObjectIdentity</code>
111 * @param aclInformation optional information about the individual ACL record (if <code>null</code> only an
112 * "inheritence marker" instance is returned which will include a recipient of {@link
113 * #RECIPIENT_USED_FOR_INHERITENCE_MARKER} ; if not <code>null</code>, it is prohibited to present
114 * <code>null</code> or an empty <code>String</code> for <code>recipient</code>)
115 *
116 * @return a fully populated instance suitable for use by external objects
117 *
118 * @throws IllegalArgumentException if the indicated ACL class could not be created
119 */
120 private BasicAclEntry createBasicAclEntry(AclDetailsHolder propertiesInformation, AclDetailsHolder aclInformation) {
121 BasicAclEntry entry;
122
123 try {
124 entry = (BasicAclEntry) propertiesInformation.getAclClass().newInstance();
125 } catch (InstantiationException ie) {
126 throw new IllegalArgumentException(ie.getMessage());
127 } catch (IllegalAccessException iae) {
128 throw new IllegalArgumentException(iae.getMessage());
129 }
130
131 entry.setAclObjectIdentity(propertiesInformation.getAclObjectIdentity());
132 entry.setAclObjectParentIdentity(propertiesInformation.getAclObjectParentIdentity());
133
134 if (aclInformation == null) {
135 // this is an inheritence marker instance only
136 entry.setMask(0);
137 entry.setRecipient(RECIPIENT_USED_FOR_INHERITENCE_MARKER);
138 } else {
139 // this is an individual ACL entry
140 entry.setMask(aclInformation.getMask());
141 entry.setRecipient(aclInformation.getRecipient());
142 }
143
144 return entry;
145 }
146
147 /**
148 * Returns the ACLs associated with the requested <code>AclObjectIdentity</code>.<P>The {@link
149 * BasicAclEntry}s returned by this method will have <code>String</code>-based recipients. This will not be a
150 * problem if you are using the <code>GrantedAuthorityEffectiveAclsResolver</code>, which is the default
151 * configured against <code>BasicAclProvider</code>.</p>
152 * <P>This method will only return ACLs for requests where the <code>AclObjectIdentity</code> is of type
153 * {@link NamedEntityObjectIdentity}. Of course, you can subclass or replace this class and support your own
154 * custom <code>AclObjectIdentity</code> types.</p>
155 *
156 * @param aclObjectIdentity for which ACL information is required (cannot be <code>null</code> and must be an
157 * instance of <code>NamedEntityObjectIdentity</code>)
158 *
159 * @return the ACLs that apply (without any <code>null</code>s inside the array), or <code>null</code> if not found
160 * or if an incompatible <code>AclObjectIdentity</code> was requested
161 */
162 public BasicAclEntry[] getAcls(AclObjectIdentity aclObjectIdentity) {
163 String aclObjectIdentityString;
164
165 try {
166 aclObjectIdentityString = convertAclObjectIdentityToString(aclObjectIdentity);
167 } catch (IllegalArgumentException unsupported) {
168 return null; // pursuant to contract described in JavaDocs above
169 }
170
171 // Lookup the object's main properties from the RDBMS (guaranteed no nulls)
172 List objects = objectProperties.execute(aclObjectIdentityString);
173
174 if (objects.size() == 0) {
175 // this is an unknown object identity string
176 return null;
177 }
178
179 // Cast to an object properties holder (there should only be one record)
180 AclDetailsHolder propertiesInformation = (AclDetailsHolder) objects.get(0);
181
182 // Lookup the object's ACLs from RDBMS (guaranteed no nulls)
183 List acls = aclsByObjectIdentity.execute(propertiesInformation.getForeignKeyId());
184
185 if (acls.size() == 0) {
186 // return merely an inheritence marker (as we know about the object but it has no related ACLs)
187 return new BasicAclEntry[] {createBasicAclEntry(propertiesInformation, null)};
188 } else {
189 // return the individual ACL instances
190 AclDetailsHolder[] aclHolders = (AclDetailsHolder[]) acls.toArray(new AclDetailsHolder[] {});
191 List toReturnAcls = new Vector();
192
193 for (int i = 0; i < aclHolders.length; i++) {
194 toReturnAcls.add(createBasicAclEntry(propertiesInformation, aclHolders[i]));
195 }
196
197 return (BasicAclEntry[]) toReturnAcls.toArray(new BasicAclEntry[] {});
198 }
199 }
200
201 public MappingSqlQuery getAclsByObjectIdentity() {
202 return aclsByObjectIdentity;
203 }
204
205 public String getAclsByObjectIdentityQuery() {
206 return aclsByObjectIdentityQuery;
207 }
208
209 public String getObjectPropertiesQuery() {
210 return objectPropertiesQuery;
211 }
212
213 protected void initDao() throws ApplicationContextException {
214 initMappingSqlQueries();
215 }
216
217 /**
218 * Extension point to allow other MappingSqlQuery objects to be substituted in a subclass
219 */
220 protected void initMappingSqlQueries() {
221 setAclsByObjectIdentity(new AclsByObjectIdentityMapping(getDataSource()));
222 setObjectProperties(new ObjectPropertiesMapping(getDataSource()));
223 }
224
225 public void setAclsByObjectIdentity(MappingSqlQuery aclsByObjectIdentityQuery) {
226 this.aclsByObjectIdentity = aclsByObjectIdentityQuery;
227 }
228
229 /**
230 * Allows the default query string used to retrieve ACLs based on object identity to be overriden, if
231 * default table or column names need to be changed. The default query is {@link
232 * #DEF_ACLS_BY_OBJECT_IDENTITY_QUERY}; when modifying this query, ensure that all returned columns are mapped
233 * back to the same column names as in the default query.
234 *
235 * @param queryString The query string to set
236 */
237 public void setAclsByObjectIdentityQuery(String queryString) {
238 aclsByObjectIdentityQuery = queryString;
239 }
240
241 public void setObjectProperties(MappingSqlQuery objectPropertiesQuery) {
242 this.objectProperties = objectPropertiesQuery;
243 }
244
245 public void setObjectPropertiesQuery(String queryString) {
246 objectPropertiesQuery = queryString;
247 }
248
249 //~ Inner Classes ==================================================================================================
250
251 /**
252 * Used to hold details of a domain object instance's properties, or an individual ACL entry.<P>Not all
253 * properties will be set. The actual properties set will depend on which <code>MappingSqlQuery</code> creates the
254 * object.</p>
255 * <P>Does not enforce <code>null</code>s or empty <code>String</code>s as this is performed by the
256 * <code>MappingSqlQuery</code> objects (or preferably the backend RDBMS via schema constraints).</p>
257 */
258 protected final class AclDetailsHolder {
259 private AclObjectIdentity aclObjectIdentity;
260 private AclObjectIdentity aclObjectParentIdentity;
261 private Class aclClass;
262 private Object recipient;
263 private int mask;
264 private long foreignKeyId;
265
266 /**
267 * Record details of an individual ACL entry (usually from the
268 * ACL_PERMISSION table)
269 *
270 * @param recipient the recipient
271 * @param mask the integer to be masked
272 */
273 public AclDetailsHolder(Object recipient, int mask) {
274 this.recipient = recipient;
275 this.mask = mask;
276 }
277
278 /**
279 * Record details of a domain object instance's properties (usually
280 * from the ACL_OBJECT_IDENTITY table)
281 *
282 * @param foreignKeyId used by the
283 * <code>AclsByObjectIdentityMapping</code> to locate the
284 * individual ACL entries
285 * @param aclObjectIdentity the object identity of the domain object
286 * instance
287 * @param aclObjectParentIdentity the object identity of the domain
288 * object instance's parent
289 * @param aclClass the class of which a new instance which should be
290 * created for each individual ACL entry (or an inheritence
291 * "holder" class if there are no ACL entries)
292 */
293 public AclDetailsHolder(long foreignKeyId, AclObjectIdentity aclObjectIdentity,
294 AclObjectIdentity aclObjectParentIdentity, Class aclClass) {
295 this.foreignKeyId = foreignKeyId;
296 this.aclObjectIdentity = aclObjectIdentity;
297 this.aclObjectParentIdentity = aclObjectParentIdentity;
298 this.aclClass = aclClass;
299 }
300
301 public Class getAclClass() {
302 return aclClass;
303 }
304
305 public AclObjectIdentity getAclObjectIdentity() {
306 return aclObjectIdentity;
307 }
308
309 public AclObjectIdentity getAclObjectParentIdentity() {
310 return aclObjectParentIdentity;
311 }
312
313 public long getForeignKeyId() {
314 return foreignKeyId;
315 }
316
317 public int getMask() {
318 return mask;
319 }
320
321 public Object getRecipient() {
322 return recipient;
323 }
324 }
325
326 /**
327 * Query object to look up individual ACL entries.<P>Returns the generic <code>AclDetailsHolder</code>
328 * object.</p>
329 * <P>Guarantees to never return <code>null</code> (exceptions are thrown in the event of any issues).</p>
330 * <P>The executed SQL requires the following information be made available from the indicated
331 * placeholders: 1. RECIPIENT, 2. MASK.</p>
332 */
333 protected class AclsByObjectIdentityMapping extends MappingSqlQuery {
334 protected AclsByObjectIdentityMapping(DataSource ds) {
335 super(ds, aclsByObjectIdentityQuery);
336 declareParameter(new SqlParameter(Types.BIGINT));
337 compile();
338 }
339
340 protected Object mapRow(ResultSet rs, int rownum)
341 throws SQLException {
342 String recipient = rs.getString(1);
343 int mask = rs.getInt(2);
344 Assert.hasText(recipient, "recipient required");
345
346 return new AclDetailsHolder(recipient, mask);
347 }
348 }
349
350 /**
351 * Query object to look up properties for an object identity.<P>Returns the generic
352 * <code>AclDetailsHolder</code> object.</p>
353 * <P>Guarantees to never return <code>null</code> (exceptions are thrown in the event of any issues).</p>
354 * <P>The executed SQL requires the following information be made available from the indicated
355 * placeholders: 1. ID, 2. OBJECT_IDENTITY, 3. ACL_CLASS and 4. PARENT_OBJECT_IDENTITY.</p>
356 */
357 protected class ObjectPropertiesMapping extends MappingSqlQuery {
358 protected ObjectPropertiesMapping(DataSource ds) {
359 super(ds, objectPropertiesQuery);
360 declareParameter(new SqlParameter(Types.VARCHAR));
361 compile();
362 }
363
364 private AclObjectIdentity buildIdentity(String identity) {
365 if (identity == null) {
366 // Must be an empty parent, so return null
367 return null;
368 }
369
370 int delim = identity.lastIndexOf(":");
371 String classname = identity.substring(0, delim);
372 String id = identity.substring(delim + 1);
373
374 return new NamedEntityObjectIdentity(classname, id);
375 }
376
377 protected Object mapRow(ResultSet rs, int rownum)
378 throws SQLException {
379 long id = rs.getLong(1); // required
380 String objectIdentity = rs.getString(2); // required
381 String aclClass = rs.getString(3); // required
382 String parentObjectIdentity = rs.getString(4); // optional
383 Assert.hasText(objectIdentity,
384 "required DEF_OBJECT_PROPERTIES_QUERY value (objectIdentity) returned null or empty");
385 Assert.hasText(aclClass, "required DEF_OBJECT_PROPERTIES_QUERY value (aclClass) returned null or empty");
386
387 Class aclClazz;
388
389 try {
390 aclClazz = this.getClass().getClassLoader().loadClass(aclClass);
391 } catch (ClassNotFoundException cnf) {
392 throw new IllegalArgumentException(cnf.getMessage());
393 }
394
395 return new AclDetailsHolder(id,
396 buildIdentity(objectIdentity), buildIdentity(parentObjectIdentity), aclClazz);
397 }
398 }
399 }