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