View Javadoc

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 }