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  package org.springframework.security.acls.jdbc;
16  
17  import org.springframework.security.Authentication;
18  
19  import org.springframework.security.acls.AccessControlEntry;
20  import org.springframework.security.acls.Acl;
21  import org.springframework.security.acls.AlreadyExistsException;
22  import org.springframework.security.acls.ChildrenExistException;
23  import org.springframework.security.acls.MutableAcl;
24  import org.springframework.security.acls.MutableAclService;
25  import org.springframework.security.acls.NotFoundException;
26  import org.springframework.security.acls.domain.AccessControlEntryImpl;
27  import org.springframework.security.acls.objectidentity.ObjectIdentity;
28  import org.springframework.security.acls.objectidentity.ObjectIdentityImpl;
29  import org.springframework.security.acls.sid.GrantedAuthoritySid;
30  import org.springframework.security.acls.sid.PrincipalSid;
31  import org.springframework.security.acls.sid.Sid;
32  
33  import org.springframework.security.context.SecurityContextHolder;
34  
35  import org.springframework.dao.DataAccessException;
36  
37  import org.springframework.jdbc.core.BatchPreparedStatementSetter;
38  
39  import org.springframework.transaction.support.TransactionSynchronizationManager;
40  
41  import org.springframework.util.Assert;
42  
43  import java.lang.reflect.Array;
44  
45  import java.sql.PreparedStatement;
46  import java.sql.SQLException;
47  
48  import java.util.List;
49  
50  import javax.sql.DataSource;
51  
52  
53  /**
54   * Provides a base implementation of {@link MutableAclService}.
55   *
56   * @author Ben Alex
57   * @author Johannes Zlattinger
58   * @version $Id: JdbcMutableAclService.java 3131 2008-06-06 02:55:53Z benalex $
59   */
60  public class JdbcMutableAclService extends JdbcAclService implements MutableAclService {
61      //~ Instance fields ================================================================================================
62  
63      private boolean foreignKeysInDatabase = true;
64      private AclCache aclCache;
65      private String deleteEntryByObjectIdentityForeignKey = "delete from acl_entry where acl_object_identity=?";
66      private String deleteObjectIdentityByPrimaryKey = "delete from acl_object_identity where id=?";
67      private String classIdentityQuery = "call identity()"; // should be overridden for postgres : select currval('acl_class_seq')
68      private String sidIdentityQuery = "call identity()"; // should be overridden for postgres : select currval('acl_siq_seq')
69      private String insertClass = "insert into acl_class (class) values (?)";
70      private String insertEntry = "insert into acl_entry "
71          + "(acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure)"
72          + "values (?, ?, ?, ?, ?, ?, ?)";
73      private String insertObjectIdentity = "insert into acl_object_identity "
74          + "(object_id_class, object_id_identity, owner_sid, entries_inheriting) " + "values (?, ?, ?, ?)";
75      private String insertSid = "insert into acl_sid (principal, sid) values (?, ?)";
76      private String selectClassPrimaryKey = "select id from acl_class where class=?";
77      private String selectObjectIdentityPrimaryKey = "select acl_object_identity.id from acl_object_identity, acl_class "
78          + "where acl_object_identity.object_id_class = acl_class.id and acl_class.class=? "
79          + "and acl_object_identity.object_id_identity = ?";
80      private String selectSidPrimaryKey = "select id from acl_sid where principal=? and sid=?";
81      private String updateObjectIdentity = "update acl_object_identity set "
82          + "parent_object = ?, owner_sid = ?, entries_inheriting = ?" + " where id = ?";
83  
84      //~ Constructors ===================================================================================================
85  
86      public JdbcMutableAclService(DataSource dataSource, LookupStrategy lookupStrategy, AclCache aclCache) {
87          super(dataSource, lookupStrategy);
88          Assert.notNull(aclCache, "AclCache required");
89          this.aclCache = aclCache;
90      }
91  
92      //~ Methods ========================================================================================================
93  
94      public MutableAcl createAcl(ObjectIdentity objectIdentity)
95          throws AlreadyExistsException {
96          Assert.notNull(objectIdentity, "Object Identity required");
97  
98          // Check this object identity hasn't already been persisted
99          if (retrieveObjectIdentityPrimaryKey(objectIdentity) != null) {
100             throw new AlreadyExistsException("Object identity '" + objectIdentity + "' already exists");
101         }
102 
103         // Need to retrieve the current principal, in order to know who "owns" this ACL (can be changed later on)
104         Authentication auth = SecurityContextHolder.getContext().getAuthentication();
105         PrincipalSid sid = new PrincipalSid(auth);
106 
107         // Create the acl_object_identity row
108         createObjectIdentity(objectIdentity, sid);
109 
110         // Retrieve the ACL via superclass (ensures cache registration, proper retrieval etc)
111         Acl acl = readAclById(objectIdentity);
112         Assert.isInstanceOf(MutableAcl.class, acl, "MutableAcl should be been returned");
113 
114         return (MutableAcl) acl;
115     }
116 
117     /**
118      * Creates a new row in acl_entry for every ACE defined in the passed MutableAcl object.
119      *
120      * @param acl containing the ACEs to insert
121      */
122     protected void createEntries(final MutableAcl acl) {
123         jdbcTemplate.batchUpdate(insertEntry,
124             new BatchPreparedStatementSetter() {
125                 public int getBatchSize() {
126                     return acl.getEntries().length;
127                 }
128 
129                 public void setValues(PreparedStatement stmt, int i)
130                         throws SQLException {
131                     AccessControlEntry entry_ = (AccessControlEntry) Array.get(acl.getEntries(), i);
132                     Assert.isTrue(entry_ instanceof AccessControlEntryImpl, "Unknown ACE class");
133 
134                     AccessControlEntryImpl entry = (AccessControlEntryImpl) entry_;
135 
136                     stmt.setLong(1, ((Long) acl.getId()).longValue());
137                     stmt.setInt(2, i);
138                     stmt.setLong(3, createOrRetrieveSidPrimaryKey(entry.getSid(), true).longValue());
139                     stmt.setInt(4, entry.getPermission().getMask());
140                     stmt.setBoolean(5, entry.isGranting());
141                     stmt.setBoolean(6, entry.isAuditSuccess());
142                     stmt.setBoolean(7, entry.isAuditFailure());
143                 }
144             });
145     }
146 
147     /**
148      * Creates an entry in the acl_object_identity table for the passed ObjectIdentity. The Sid is also
149      * necessary, as acl_object_identity has defined the sid column as non-null.
150      *
151      * @param object to represent an acl_object_identity for
152      * @param owner for the SID column (will be created if there is no acl_sid entry for this particular Sid already)
153      */
154     protected void createObjectIdentity(ObjectIdentity object, Sid owner) {
155         Long sidId = createOrRetrieveSidPrimaryKey(owner, true);
156         Long classId = createOrRetrieveClassPrimaryKey(object.getJavaType(), true);
157         jdbcTemplate.update(insertObjectIdentity,
158             new Object[] {classId, object.getIdentifier().toString(), sidId, new Boolean(true)});
159     }
160 
161     /**
162      * Retrieves the primary key from acl_class, creating a new row if needed and the allowCreate property is
163      * true.
164      *
165      * @param clazz to find or create an entry for (this implementation uses the fully-qualified class name String)
166      * @param allowCreate true if creation is permitted if not found
167      *
168      * @return the primary key or null if not found
169      */
170     protected Long createOrRetrieveClassPrimaryKey(Class clazz, boolean allowCreate) {
171         List classIds = jdbcTemplate.queryForList(selectClassPrimaryKey, new Object[] {clazz.getName()}, Long.class);
172         Long classId = null;
173 
174         if (classIds.isEmpty()) {
175             if (allowCreate) {
176                 classId = null;
177                 jdbcTemplate.update(insertClass, new Object[] {clazz.getName()});
178                 Assert.isTrue(TransactionSynchronizationManager.isSynchronizationActive(),
179                         "Transaction must be running");
180                 classId = new Long(jdbcTemplate.queryForLong(classIdentityQuery));
181             }
182         } else {
183             classId = (Long) classIds.iterator().next();
184         }
185 
186         return classId;
187     }
188 
189     /**
190      * Retrieves the primary key from acl_sid, creating a new row if needed and the allowCreate property is
191      * true.
192      *
193      * @param sid to find or create
194      * @param allowCreate true if creation is permitted if not found
195      *
196      * @return the primary key or null if not found
197      *
198      * @throws IllegalArgumentException DOCUMENT ME!
199      */
200     protected Long createOrRetrieveSidPrimaryKey(Sid sid, boolean allowCreate) {
201         Assert.notNull(sid, "Sid required");
202 
203         String sidName = null;
204         boolean principal = true;
205 
206         if (sid instanceof PrincipalSid) {
207             sidName = ((PrincipalSid) sid).getPrincipal();
208         } else if (sid instanceof GrantedAuthoritySid) {
209             sidName = ((GrantedAuthoritySid) sid).getGrantedAuthority();
210             principal = false;
211         } else {
212             throw new IllegalArgumentException("Unsupported implementation of Sid");
213         }
214 
215         List sidIds = jdbcTemplate.queryForList(selectSidPrimaryKey, new Object[] {new Boolean(principal), sidName},
216                 Long.class);
217         Long sidId = null;
218 
219         if (sidIds.isEmpty()) {
220             if (allowCreate) {
221                 sidId = null;
222                 jdbcTemplate.update(insertSid, new Object[] {new Boolean(principal), sidName});
223                 Assert.isTrue(TransactionSynchronizationManager.isSynchronizationActive(),
224                         "Transaction must be running");
225                 sidId = new Long(jdbcTemplate.queryForLong(sidIdentityQuery));
226             }
227         } else {
228             sidId = (Long) sidIds.iterator().next();
229         }
230 
231         return sidId;
232     }
233 
234     public void deleteAcl(ObjectIdentity objectIdentity, boolean deleteChildren)
235         throws ChildrenExistException {
236         Assert.notNull(objectIdentity, "Object Identity required");
237         Assert.notNull(objectIdentity.getIdentifier(), "Object Identity doesn't provide an identifier");
238 
239         if (deleteChildren) {
240             ObjectIdentity[] children = findChildren(objectIdentity);
241             if (children != null) {
242                 for (int i = 0; i < children.length; i++) {
243                     deleteAcl(children[i], true);
244                 }
245             }
246         } else {
247             if (!foreignKeysInDatabase) {
248                 // We need to perform a manual verification for what a FK would normally do
249                 // We generally don't do this, in the interests of deadlock management
250                 ObjectIdentity[] children = findChildren(objectIdentity);
251                 if (children != null) {
252                     throw new ChildrenExistException("Cannot delete '" + objectIdentity + "' (has " + children.length
253                             + " children)");
254                 }
255             }
256         }
257 
258         Long oidPrimaryKey = retrieveObjectIdentityPrimaryKey(objectIdentity);
259         
260         // Delete this ACL's ACEs in the acl_entry table
261         deleteEntries(oidPrimaryKey);
262 
263         // Delete this ACL's acl_object_identity row
264         deleteObjectIdentity(oidPrimaryKey);
265 
266         // Clear the cache
267         aclCache.evictFromCache(objectIdentity);
268     }
269 
270     /**
271      * Deletes all ACEs defined in the acl_entry table belonging to the presented ObjectIdentity primary key.
272      *
273      * @param oidPrimaryKey the rows in acl_entry to delete
274      */
275     protected void deleteEntries(Long oidPrimaryKey) {
276         jdbcTemplate.update(deleteEntryByObjectIdentityForeignKey,
277                 new Object[] {oidPrimaryKey});
278     }
279 
280     /**
281      * Deletes a single row from acl_object_identity that is associated with the presented ObjectIdentity primary key.
282      * 
283      * <p>
284      * We do not delete any entries from acl_class, even if no classes are using that class any longer. This is a
285      * deadlock avoidance approach.
286      * </p>
287      *
288      * @param oidPrimaryKey to delete the acl_object_identity
289      */
290     protected void deleteObjectIdentity(Long oidPrimaryKey) {
291         // Delete the acl_object_identity row
292         jdbcTemplate.update(deleteObjectIdentityByPrimaryKey, new Object[] {oidPrimaryKey});
293     }
294 
295     /**
296      * Retrieves the primary key from the acl_object_identity table for the passed ObjectIdentity. Unlike some
297      * other methods in this implementation, this method will NOT create a row (use {@link
298      * #createObjectIdentity(ObjectIdentity, Sid)} instead).
299      *
300      * @param oid to find
301      *
302      * @return the object identity or null if not found
303      */
304     protected Long retrieveObjectIdentityPrimaryKey(ObjectIdentity oid) {
305         try {
306             return new Long(jdbcTemplate.queryForLong(selectObjectIdentityPrimaryKey,
307                     new Object[] {oid.getJavaType().getName(), oid.getIdentifier()}));
308         } catch (DataAccessException notFound) {
309             return null;
310         }
311     }
312 
313     /**
314      * This implementation will simply delete all ACEs in the database and recreate them on each invocation of
315      * this method. A more comprehensive implementation might use dirty state checking, or more likely use ORM
316      * capabilities for create, update and delete operations of {@link MutableAcl}.
317      *
318      * @param acl DOCUMENT ME!
319      *
320      * @return DOCUMENT ME!
321      *
322      * @throws NotFoundException DOCUMENT ME!
323      */
324     public MutableAcl updateAcl(MutableAcl acl) throws NotFoundException {
325         Assert.notNull(acl.getId(), "Object Identity doesn't provide an identifier");
326 
327         // Delete this ACL's ACEs in the acl_entry table
328         deleteEntries(retrieveObjectIdentityPrimaryKey(acl.getObjectIdentity()));
329 
330         // Create this ACL's ACEs in the acl_entry table
331         createEntries(acl);
332 
333         // Change the mutable columns in acl_object_identity
334         updateObjectIdentity(acl);
335 
336         // Clear the cache, including children
337         clearCacheIncludingChildren(acl.getObjectIdentity());
338 
339         // Retrieve the ACL via superclass (ensures cache registration, proper retrieval etc)
340         return (MutableAcl) super.readAclById(acl.getObjectIdentity());
341     }
342     
343     private void clearCacheIncludingChildren(ObjectIdentity objectIdentity) {
344         Assert.notNull(objectIdentity, "ObjectIdentity required");
345         ObjectIdentity[] children = findChildren(objectIdentity);
346         if (children != null) {
347             for (int i = 0; i < children.length; i++) {
348                 clearCacheIncludingChildren(children[i]);
349             }
350         }
351         aclCache.evictFromCache(objectIdentity);
352     }
353 
354     /**
355      * Updates an existing acl_object_identity row, with new information presented in the passed MutableAcl
356      * object. Also will create an acl_sid entry if needed for the Sid that owns the MutableAcl.
357      *
358      * @param acl to modify (a row must already exist in acl_object_identity)
359      *
360      * @throws NotFoundException DOCUMENT ME!
361      */
362     protected void updateObjectIdentity(MutableAcl acl) {
363         Long parentId = null;
364 
365         if (acl.getParentAcl() != null) {
366             Assert.isInstanceOf(ObjectIdentityImpl.class, acl.getParentAcl().getObjectIdentity(),
367                 "Implementation only supports ObjectIdentityImpl");
368 
369             ObjectIdentityImpl oii = (ObjectIdentityImpl) acl.getParentAcl().getObjectIdentity();
370             parentId = retrieveObjectIdentityPrimaryKey(oii);
371         }
372 
373         Assert.notNull(acl.getOwner(), "Owner is required in this implementation");
374 
375         Long ownerSid = createOrRetrieveSidPrimaryKey(acl.getOwner(), true);
376         int count = jdbcTemplate.update(updateObjectIdentity,
377                 new Object[] {parentId, ownerSid, new Boolean(acl.isEntriesInheriting()), acl.getId()});
378 
379         if (count != 1) {
380             throw new NotFoundException("Unable to locate ACL to update");
381         }
382     }
383 
384     public void setClassIdentityQuery(String identityQuery) {
385         Assert.hasText(identityQuery, "New identity query is required");
386         this.classIdentityQuery = identityQuery;
387     }
388 
389     public void setSidIdentityQuery(String identityQuery) {
390         Assert.hasText(identityQuery, "New identity query is required");
391         this.sidIdentityQuery = identityQuery;
392     }
393     /**
394      * @param foreignKeysInDatabase if false this class will perform additional FK constrain checking, which may
395      * cause deadlocks (the default is true, so deadlocks are avoided but the database is expected to enforce FKs)
396      */
397     public void setForeignKeysInDatabase(boolean foreignKeysInDatabase) {
398         this.foreignKeysInDatabase = foreignKeysInDatabase;
399     }
400 }