001: /* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
002: *
003: * Licensed under the Apache License, Version 2.0 (the "License");
004: * you may not use this file except in compliance with the License.
005: * You may obtain a copy of the License at
006: *
007: * http://www.apache.org/licenses/LICENSE-2.0
008: *
009: * Unless required by applicable law or agreed to in writing, software
010: * distributed under the License is distributed on an "AS IS" BASIS,
011: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012: * See the License for the specific language governing permissions and
013: * limitations under the License.
014: */
015: package org.acegisecurity.acls.jdbc;
016:
017: import org.acegisecurity.acls.AccessControlEntry;
018: import org.acegisecurity.acls.Acl;
019: import org.acegisecurity.acls.MutableAcl;
020: import org.acegisecurity.acls.NotFoundException;
021: import org.acegisecurity.acls.Permission;
022: import org.acegisecurity.acls.UnloadedSidException;
023: import org.acegisecurity.acls.domain.AccessControlEntryImpl;
024: import org.acegisecurity.acls.domain.AclAuthorizationStrategy;
025: import org.acegisecurity.acls.domain.AclImpl;
026: import org.acegisecurity.acls.domain.AuditLogger;
027: import org.acegisecurity.acls.domain.BasePermission;
028: import org.acegisecurity.acls.objectidentity.ObjectIdentity;
029: import org.acegisecurity.acls.objectidentity.ObjectIdentityImpl;
030: import org.acegisecurity.acls.sid.GrantedAuthoritySid;
031: import org.acegisecurity.acls.sid.PrincipalSid;
032: import org.acegisecurity.acls.sid.Sid;
033:
034: import org.acegisecurity.util.FieldUtils;
035:
036: import org.springframework.dao.DataAccessException;
037:
038: import org.springframework.jdbc.core.JdbcTemplate;
039: import org.springframework.jdbc.core.PreparedStatementSetter;
040: import org.springframework.jdbc.core.ResultSetExtractor;
041:
042: import org.springframework.util.Assert;
043:
044: import java.lang.reflect.Field;
045:
046: import java.sql.PreparedStatement;
047: import java.sql.ResultSet;
048: import java.sql.SQLException;
049:
050: import java.util.HashMap;
051: import java.util.HashSet;
052: import java.util.Iterator;
053: import java.util.List;
054: import java.util.Map;
055: import java.util.Set;
056:
057: import javax.sql.DataSource;
058:
059: /**
060: * Performs lookups in a manner that is compatible with ANSI SQL.<p>NB: This implementation does attempt to provide
061: * reasonably optimised lookups - within the constraints of a normalised database and standard ANSI SQL features. If
062: * you are willing to sacrifice either of these constraints (eg use a particular database feature such as hierarchical
063: * queries or materalized views, or reduce normalisation) you are likely to achieve better performance. In such
064: * situations you will need to provide your own custom <code>LookupStrategy</code>. This class does not support
065: * subclassing, as it is likely to change in future releases and therefore subclassing is unsupported.</p>
066: *
067: * @author Ben Alex
068: * @version $Id: BasicLookupStrategy.java 1784 2007-02-24 21:00:24Z luke_t $
069: */
070: public final class BasicLookupStrategy implements LookupStrategy {
071: //~ Instance fields ================================================================================================
072:
073: private AclAuthorizationStrategy aclAuthorizationStrategy;
074: private AclCache aclCache;
075: private AuditLogger auditLogger;
076: private JdbcTemplate jdbcTemplate;
077: private int batchSize = 50;
078:
079: //~ Constructors ===================================================================================================
080:
081: /**
082: * Constructor accepting mandatory arguments
083: *
084: * @param dataSource to access the database
085: * @param aclCache the cache where fully-loaded elements can be stored
086: * @param aclAuthorizationStrategy authorization strategy (required)
087: */
088: public BasicLookupStrategy(DataSource dataSource,
089: AclCache aclCache,
090: AclAuthorizationStrategy aclAuthorizationStrategy,
091: AuditLogger auditLogger) {
092: Assert.notNull(dataSource, "DataSource required");
093: Assert.notNull(aclCache, "AclCache required");
094: Assert.notNull(aclAuthorizationStrategy,
095: "AclAuthorizationStrategy required");
096: Assert.notNull(auditLogger, "AuditLogger required");
097: this .jdbcTemplate = new JdbcTemplate(dataSource);
098: this .aclCache = aclCache;
099: this .aclAuthorizationStrategy = aclAuthorizationStrategy;
100: this .auditLogger = auditLogger;
101: }
102:
103: //~ Methods ========================================================================================================
104:
105: private static String computeRepeatingSql(String repeatingSql,
106: int requiredRepetitions) {
107: Assert.isTrue(requiredRepetitions >= 1, "Must be => 1");
108:
109: String startSql = "select ACL_OBJECT_IDENTITY.OBJECT_ID_IDENTITY, ACL_ENTRY.ACE_ORDER, "
110: + "ACL_OBJECT_IDENTITY.ID as ACL_ID, "
111: + "ACL_OBJECT_IDENTITY.PARENT_OBJECT, "
112: + "ACL_OBJECT_IDENTITY,ENTRIES_INHERITING, "
113: + "ACL_ENTRY.ID as ACE_ID, ACL_ENTRY.MASK, ACL_ENTRY.GRANTING, "
114: + "ACL_ENTRY.AUDIT_SUCCESS, ACL_ENTRY.AUDIT_FAILURE, "
115: + "ACL_SID.PRINCIPAL as ACE_PRINCIPAL, ACL_SID.SID as ACE_SID, "
116: + "ACLI_SID.PRINCIPAL as ACL_PRINCIPAL, ACLI_SID.SID as ACL_SID, "
117: + "ACL_CLASS.CLASS "
118: + "from ACL_OBJECT_IDENTITY, ACL_SID ACLI_SID, ACL_CLASS "
119: + "LEFT JOIN ACL_ENTRY ON ACL_OBJECT_IDENTITY.ID = ACL_ENTRY.ACL_OBJECT_IDENTITY "
120: + "LEFT JOIN ACL_SID ON ACL_ENTRY.SID = ACL_SID.ID where ACLI_SID.ID = ACL_OBJECT_IDENTITY.OWNER_SID "
121: + "and ACL_CLASS.ID = ACL_OBJECT_IDENTITY.OBJECT_ID_CLASS "
122: + "and ( ";
123:
124: String endSql = ") order by ACL_OBJECT_IDENTITY.OBJECT_ID_IDENTITY asc, ACL_ENTRY.ACE_ORDER asc";
125:
126: StringBuffer sqlStringBuffer = new StringBuffer();
127: sqlStringBuffer.append(startSql);
128:
129: for (int i = 1; i <= requiredRepetitions; i++) {
130: sqlStringBuffer.append(repeatingSql);
131:
132: if (i != requiredRepetitions) {
133: sqlStringBuffer.append(" or ");
134: }
135: }
136:
137: sqlStringBuffer.append(endSql);
138:
139: return sqlStringBuffer.toString();
140: }
141:
142: /**
143: * The final phase of converting the <code>Map</code> of <code>AclImpl</code> instances which contain
144: * <code>StubAclParent</code>s into proper, valid <code>AclImpl</code>s with correct ACL parents.
145: *
146: * @param inputMap the unconverted <code>AclImpl</code>s
147: * @param currentIdentity the current<code>Acl</code> that we wish to convert (this may be
148: *
149: * @return
150: *
151: * @throws IllegalStateException DOCUMENT ME!
152: */
153: private AclImpl convert(Map inputMap, Long currentIdentity) {
154: Assert.notEmpty(inputMap, "InputMap required");
155: Assert.notNull(currentIdentity, "CurrentIdentity required");
156:
157: // Retrieve this Acl from the InputMap
158: Acl uncastAcl = (Acl) inputMap.get(currentIdentity);
159: Assert.isInstanceOf(AclImpl.class, uncastAcl,
160: "The inputMap contained a non-AclImpl");
161:
162: AclImpl inputAcl = (AclImpl) uncastAcl;
163:
164: Acl parent = inputAcl.getParentAcl();
165:
166: if ((parent != null) && parent instanceof StubAclParent) {
167: // Lookup the parent
168: StubAclParent stubAclParent = (StubAclParent) parent;
169: parent = convert(inputMap, stubAclParent.getId());
170: }
171:
172: // Now we have the parent (if there is one), create the true AclImpl
173: AclImpl result = new AclImpl(inputAcl.getObjectIdentity(),
174: (Long) inputAcl.getId(), aclAuthorizationStrategy,
175: auditLogger, parent, null, inputAcl
176: .isEntriesInheriting(), inputAcl.getOwner());
177:
178: // Copy the "aces" from the input to the destination
179: Field field = FieldUtils.getField(AclImpl.class, "aces");
180:
181: try {
182: field.setAccessible(true);
183: field.set(result, field.get(inputAcl));
184: } catch (IllegalAccessException ex) {
185: throw new IllegalStateException(
186: "Could not obtain or set AclImpl.ace field");
187: }
188:
189: return result;
190: }
191:
192: /**
193: * Accepts the current <code>ResultSet</code> row, and converts it into an <code>AclImpl</code> that
194: * contains a <code>StubAclParent</code>
195: *
196: * @param acls the Map we should add the converted Acl to
197: * @param rs the ResultSet focused on a current row
198: *
199: * @throws SQLException if something goes wrong converting values
200: * @throws IllegalStateException DOCUMENT ME!
201: */
202: private void convertCurrentResultIntoObject(Map acls, ResultSet rs)
203: throws SQLException {
204: Long id = new Long(rs.getLong("ACL_ID"));
205:
206: // If we already have an ACL for this ID, just create the ACE
207: AclImpl acl = (AclImpl) acls.get(id);
208:
209: if (acl == null) {
210: // Make an AclImpl and pop it into the Map
211: ObjectIdentity objectIdentity = new ObjectIdentityImpl(rs
212: .getString("CLASS"), new Long(rs
213: .getLong("OBJECT_ID_IDENTITY")));
214:
215: Acl parentAcl = null;
216: long parentAclId = rs.getLong("PARENT_OBJECT");
217:
218: if (parentAclId != 0) {
219: parentAcl = new StubAclParent(new Long(parentAclId));
220: }
221:
222: boolean entriesInheriting = rs
223: .getBoolean("ENTRIES_INHERITING");
224: Sid owner;
225:
226: if (rs.getBoolean("ACL_PRINCIPAL")) {
227: owner = new PrincipalSid(rs.getString("ACL_SID"));
228: } else {
229: owner = new GrantedAuthoritySid(rs.getString("ACL_SID"));
230: }
231:
232: acl = new AclImpl(objectIdentity, id,
233: aclAuthorizationStrategy, auditLogger, parentAcl,
234: null, entriesInheriting, owner);
235: acls.put(id, acl);
236: }
237:
238: // Add an extra ACE to the ACL (ORDER BY maintains the ACE list order)
239: // It is permissable to have no ACEs in an ACL (which is detected by a null ACE_SID)
240: if (rs.getString("ACE_SID") != null) {
241: Long aceId = new Long(rs.getLong("ACE_ID"));
242: Sid recipient;
243:
244: if (rs.getBoolean("ACE_PRINCIPAL")) {
245: recipient = new PrincipalSid(rs.getString("ACE_SID"));
246: } else {
247: recipient = new GrantedAuthoritySid(rs
248: .getString("ACE_SID"));
249: }
250:
251: Permission permission = BasePermission.buildFromMask(rs
252: .getInt("MASK"));
253: boolean granting = rs.getBoolean("GRANTING");
254: boolean auditSuccess = rs.getBoolean("AUDIT_SUCCESS");
255: boolean auditFailure = rs.getBoolean("AUDIT_FAILURE");
256:
257: AccessControlEntryImpl ace = new AccessControlEntryImpl(
258: aceId, acl, recipient, permission, granting,
259: auditSuccess, auditFailure);
260:
261: Field acesField = FieldUtils
262: .getField(AclImpl.class, "aces");
263: List aces;
264:
265: try {
266: acesField.setAccessible(true);
267: aces = (List) acesField.get(acl);
268: } catch (IllegalAccessException ex) {
269: throw new IllegalStateException(
270: "Could not obtain AclImpl.ace field: cause["
271: + ex.getMessage() + "]");
272: }
273:
274: // Add the ACE if it doesn't already exist in the ACL.aces field
275: if (!aces.contains(ace)) {
276: aces.add(ace);
277: }
278: }
279: }
280:
281: /**
282: * Looks up a batch of <code>ObjectIdentity</code>s directly from the database.<p>The caller is responsible
283: * for optimization issues, such as selecting the identities to lookup, ensuring the cache doesn't contain them
284: * already, and adding the returned elements to the cache etc.</p>
285: * <p>This subclass is required to return fully valid <code>Acl</code>s, including properly-configured
286: * parent ACLs.</p>
287: *
288: * @param objectIdentities DOCUMENT ME!
289: * @param sids DOCUMENT ME!
290: *
291: * @return DOCUMENT ME!
292: */
293: private Map lookupObjectIdentities(
294: final ObjectIdentity[] objectIdentities, Sid[] sids) {
295: Assert.notEmpty(objectIdentities,
296: "Must provide identities to lookup");
297:
298: final Map acls = new HashMap(); // contains Acls with StubAclParents
299:
300: // Make the "acls" map contain all requested objectIdentities
301: // (including markers to each parent in the hierarchy)
302: String sql = computeRepeatingSql(
303: "(ACL_OBJECT_IDENTITY.OBJECT_ID_IDENTITY = ? and ACL_CLASS.CLASS = ?)",
304: objectIdentities.length);
305:
306: jdbcTemplate.query(sql, new PreparedStatementSetter() {
307: public void setValues(PreparedStatement ps)
308: throws SQLException {
309: for (int i = 0; i < objectIdentities.length; i++) {
310: // Determine prepared statement values for this iteration
311: String javaType = objectIdentities[i].getJavaType()
312: .getName();
313: Assert
314: .isInstanceOf(
315: Long.class,
316: objectIdentities[i].getIdentifier(),
317: "This class requires ObjectIdentity.getIdentifier() to be a Long");
318:
319: long id = ((Long) objectIdentities[i]
320: .getIdentifier()).longValue();
321:
322: // Inject values
323: ps.setLong((2 * i) + 1, id);
324: ps.setString((2 * i) + 2, javaType);
325: }
326: }
327: }, new ProcessResultSet(acls, sids));
328:
329: // Finally, convert our "acls" containing StubAclParents into true Acls
330: Map resultMap = new HashMap();
331: Iterator iter = acls.values().iterator();
332:
333: while (iter.hasNext()) {
334: Acl inputAcl = (Acl) iter.next();
335: Assert.isInstanceOf(AclImpl.class, inputAcl,
336: "Map should have contained an AclImpl");
337: Assert.isInstanceOf(Long.class, ((AclImpl) inputAcl)
338: .getId(), "Acl.getId() must be Long");
339:
340: Acl result = convert(acls, (Long) ((AclImpl) inputAcl)
341: .getId());
342: resultMap.put(result.getObjectIdentity(), result);
343: }
344:
345: return resultMap;
346: }
347:
348: /**
349: * Locates the primary key IDs specified in "findNow", adding AclImpl instances with StubAclParents to the
350: * "acls" Map.
351: *
352: * @param acls the AclImpls (with StubAclParents)
353: * @param findNow Long-based primary keys to retrieve
354: * @param sids DOCUMENT ME!
355: */
356: private void lookupPrimaryKeys(final Map acls, final Set findNow,
357: final Sid[] sids) {
358: Assert.notNull(acls, "ACLs are required");
359: Assert.notEmpty(findNow, "Items to find now required");
360:
361: String sql = computeRepeatingSql(
362: "(ACL_OBJECT_IDENTITY.ID = ?)", findNow.size());
363:
364: jdbcTemplate.query(sql, new PreparedStatementSetter() {
365: public void setValues(PreparedStatement ps)
366: throws SQLException {
367: Iterator iter = findNow.iterator();
368: int i = 0;
369:
370: while (iter.hasNext()) {
371: i++;
372: ps.setLong(i, ((Long) iter.next()).longValue());
373: }
374: }
375: }, new ProcessResultSet(acls, sids));
376: }
377:
378: /**
379: * The main method.<p>WARNING: This implementation completely disregards the "sids" parameter! Every item
380: * in the cache is expected to contain all SIDs. If you have serious performance needs (eg a very large number of
381: * SIDs per object identity), you'll probably want to develop a custom {@link LookupStrategy} implementation
382: * instead.</p>
383: * <p>The implementation works in batch sizes specfied by {@link #batchSize}.</p>
384: *
385: * @param objects DOCUMENT ME!
386: * @param sids DOCUMENT ME!
387: *
388: * @return DOCUMENT ME!
389: *
390: * @throws NotFoundException DOCUMENT ME!
391: * @throws IllegalStateException DOCUMENT ME!
392: */
393: public Map readAclsById(ObjectIdentity[] objects, Sid[] sids)
394: throws NotFoundException {
395: Assert.isTrue(batchSize >= 1, "BatchSize must be >= 1");
396: Assert.notEmpty(objects, "Objects to lookup required");
397:
398: // Map<ObjectIdentity,Acl>
399: Map result = new HashMap(); // contains FULLY loaded Acl objects
400:
401: Set currentBatchToLoad = new HashSet(); // contains ObjectIdentitys
402:
403: for (int i = 0; i < objects.length; i++) {
404: // Check we don't already have this ACL in the results
405: if (result.containsKey(objects[i])) {
406: continue; // already in results, so move to next element
407: }
408:
409: // Check cache for the present ACL entry
410: Acl acl = aclCache.getFromCache(objects[i]);
411:
412: // Ensure any cached element supports all the requested SIDs
413: // (they should always, as our base impl doesn't filter on SID)
414: if (acl != null) {
415: if (acl.isSidLoaded(sids)) {
416: result.put(acl.getObjectIdentity(), acl);
417:
418: continue; // now in results, so move to next element
419: } else {
420: throw new IllegalStateException(
421: "Error: SID-filtered element detected when implementation does not perform SID filtering "
422: + "- have you added something to the cache manually?");
423: }
424: }
425:
426: // To get this far, we have no choice but to retrieve it via JDBC
427: // (although we don't do it until we get a batch of them to load)
428: currentBatchToLoad.add(objects[i]);
429:
430: // Is it time to load from JDBC the currentBatchToLoad?
431: if ((currentBatchToLoad.size() == this .batchSize)
432: || ((i + 1) == objects.length)) {
433: Map loadedBatch = lookupObjectIdentities(
434: (ObjectIdentity[]) currentBatchToLoad
435: .toArray(new ObjectIdentity[] {}), sids);
436:
437: // Add loaded batch (all elements 100% initialized) to results
438: result.putAll(loadedBatch);
439:
440: // Add the loaded batch to the cache
441: Iterator loadedAclIterator = loadedBatch.values()
442: .iterator();
443:
444: while (loadedAclIterator.hasNext()) {
445: aclCache.putInCache((AclImpl) loadedAclIterator
446: .next());
447: }
448:
449: currentBatchToLoad.clear();
450: }
451: }
452:
453: // Now we're done, check every requested object identity was found (throw NotFoundException if needed)
454: for (int i = 0; i < objects.length; i++) {
455: if (!result.containsKey(objects[i])) {
456: throw new NotFoundException(
457: "Unable to find ACL information for object identity '"
458: + objects[i].toString() + "'");
459: }
460: }
461:
462: return result;
463: }
464:
465: public void setBatchSize(int batchSize) {
466: this .batchSize = batchSize;
467: }
468:
469: //~ Inner Classes ==================================================================================================
470:
471: private class ProcessResultSet implements ResultSetExtractor {
472: private Map acls;
473: private Sid[] sids;
474:
475: public ProcessResultSet(Map acls, Sid[] sids) {
476: Assert.notNull(acls, "ACLs cannot be null");
477: this .acls = acls;
478: this .sids = sids; // can be null
479: }
480:
481: public Object extractData(ResultSet rs) throws SQLException,
482: DataAccessException {
483: Set parentIdsToLookup = new HashSet(); // Set of parent_id Longs
484:
485: while (rs.next()) {
486: // Convert current row into an Acl (albeit with a StubAclParent)
487: convertCurrentResultIntoObject(acls, rs);
488:
489: // Figure out if this row means we need to lookup another parent
490: long parentId = rs.getLong("PARENT_OBJECT");
491:
492: if (parentId != 0) {
493: // See if it's already in the "acls"
494: if (acls.containsKey(new Long(parentId))) {
495: continue; // skip this while iteration
496: }
497:
498: // Now try to find it in the cache
499: MutableAcl cached = aclCache.getFromCache(new Long(
500: parentId));
501:
502: if ((cached == null) || !cached.isSidLoaded(sids)) {
503: parentIdsToLookup.add(new Long(parentId));
504: } else {
505: // Pop into the acls map, so our convert method doesn't
506: // need to deal with an unsynchronized AclCache
507: acls.put(cached.getId(), cached);
508: }
509: }
510: }
511:
512: // Lookup parents, adding Acls (with StubAclParents) to "acl" map
513: if (parentIdsToLookup.size() > 0) {
514: lookupPrimaryKeys(acls, parentIdsToLookup, sids);
515: }
516:
517: // Return null to meet ResultSetExtractor method contract
518: return null;
519: }
520: }
521:
522: private class StubAclParent implements Acl {
523: private Long id;
524:
525: public StubAclParent(Long id) {
526: this .id = id;
527: }
528:
529: public AccessControlEntry[] getEntries() {
530: throw new UnsupportedOperationException("Stub only");
531: }
532:
533: public Long getId() {
534: return id;
535: }
536:
537: public ObjectIdentity getObjectIdentity() {
538: throw new UnsupportedOperationException("Stub only");
539: }
540:
541: public Sid getOwner() {
542: throw new UnsupportedOperationException("Stub only");
543: }
544:
545: public Acl getParentAcl() {
546: throw new UnsupportedOperationException("Stub only");
547: }
548:
549: public boolean isEntriesInheriting() {
550: throw new UnsupportedOperationException("Stub only");
551: }
552:
553: public boolean isGranted(Permission[] permission, Sid[] sids,
554: boolean administrativeMode) throws NotFoundException,
555: UnloadedSidException {
556: throw new UnsupportedOperationException("Stub only");
557: }
558:
559: public boolean isSidLoaded(Sid[] sids) {
560: throw new UnsupportedOperationException("Stub only");
561: }
562: }
563: }
|