瀏覽代碼

Initial commit of ACL capabilities.

Ben Alex 21 年之前
父節點
當前提交
56829872b6
共有 35 個文件被更改,包括 4084 次插入3 次删除
  1. 25 0
      core/src/main/java/org/acegisecurity/acl/AclEntry.java
  2. 58 0
      core/src/main/java/org/acegisecurity/acl/AclManager.java
  3. 82 0
      core/src/main/java/org/acegisecurity/acl/AclProvider.java
  4. 159 0
      core/src/main/java/org/acegisecurity/acl/AclProviderManager.java
  5. 279 0
      core/src/main/java/org/acegisecurity/acl/basic/AbstractBasicAclEntry.java
  6. 67 0
      core/src/main/java/org/acegisecurity/acl/basic/AclObjectIdentity.java
  7. 42 0
      core/src/main/java/org/acegisecurity/acl/basic/AclObjectIdentityAware.java
  8. 58 0
      core/src/main/java/org/acegisecurity/acl/basic/BasicAclDao.java
  9. 126 0
      core/src/main/java/org/acegisecurity/acl/basic/BasicAclEntry.java
  10. 61 0
      core/src/main/java/org/acegisecurity/acl/basic/BasicAclEntryCache.java
  11. 341 0
      core/src/main/java/org/acegisecurity/acl/basic/BasicAclProvider.java
  12. 65 0
      core/src/main/java/org/acegisecurity/acl/basic/EffectiveAclsResolver.java
  13. 102 0
      core/src/main/java/org/acegisecurity/acl/basic/GrantedAuthorityEffectiveAclsResolver.java
  14. 161 0
      core/src/main/java/org/acegisecurity/acl/basic/NamedEntityObjectIdentity.java
  15. 112 0
      core/src/main/java/org/acegisecurity/acl/basic/SimpleAclEntry.java
  16. 78 0
      core/src/main/java/org/acegisecurity/acl/basic/cache/BasicAclEntryHolder.java
  17. 135 0
      core/src/main/java/org/acegisecurity/acl/basic/cache/EhCacheBasedAclEntryCache.java
  18. 56 0
      core/src/main/java/org/acegisecurity/acl/basic/cache/NullAclEntryCache.java
  19. 5 0
      core/src/main/java/org/acegisecurity/acl/basic/cache/package.html
  20. 256 0
      core/src/main/java/org/acegisecurity/acl/basic/jdbc/JdbcDaoImpl.java
  21. 5 0
      core/src/main/java/org/acegisecurity/acl/basic/jdbc/package.html
  22. 5 0
      core/src/main/java/org/acegisecurity/acl/basic/package.html
  23. 15 0
      core/src/main/java/org/acegisecurity/acl/package.html
  24. 207 0
      core/src/test/java/org/acegisecurity/acl/AclProviderManagerTests.java
  25. 365 0
      core/src/test/java/org/acegisecurity/acl/basic/BasicAclProviderTests.java
  26. 118 0
      core/src/test/java/org/acegisecurity/acl/basic/GrantedAuthorityEffectiveAclsResolverTests.java
  27. 28 0
      core/src/test/java/org/acegisecurity/acl/basic/MockAclObjectIdentity.java
  28. 135 0
      core/src/test/java/org/acegisecurity/acl/basic/NamedEntityObjectIdentityTests.java
  29. 183 0
      core/src/test/java/org/acegisecurity/acl/basic/SimpleAclEntryTests.java
  30. 38 0
      core/src/test/java/org/acegisecurity/acl/basic/SomeDomain.java
  31. 66 0
      core/src/test/java/org/acegisecurity/acl/basic/cache/BasicAclEntryHolderTests.java
  32. 95 0
      core/src/test/java/org/acegisecurity/acl/basic/cache/EhCacheBasedAclEntryCacheTests.java
  33. 58 0
      core/src/test/java/org/acegisecurity/acl/basic/cache/NullAclEntryCacheTests.java
  34. 121 0
      core/src/test/java/org/acegisecurity/acl/basic/jdbc/JdbcDaoImplTests.java
  35. 377 3
      docs/reference/src/index.xml

+ 25 - 0
core/src/main/java/org/acegisecurity/acl/AclEntry.java

@@ -0,0 +1,25 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl;
+
+/**
+ * Marker interface representing an access control list entry associated with a
+ * specific domain object instance.
+ * 
+ * @author Ben Alex
+ * @version $Id$
+ */
+public interface AclEntry {}

+ 58 - 0
core/src/main/java/org/acegisecurity/acl/AclManager.java

@@ -0,0 +1,58 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl;
+
+import net.sf.acegisecurity.Authentication;
+
+
+/**
+ * Obtains the <code>AclEntry</code> instances that apply to a particular
+ * domain object instance.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public interface AclManager {
+    //~ Methods ================================================================
+
+    /**
+     * Obtains the ACLs that apply to the specified domain instance.
+     *
+     * @param domainInstance the instance for which ACL information is required
+     *        (never <code>null</code>)
+     *
+     * @return the ACLs that apply, or <code>null</code> if no ACLs apply to
+     *         the specified domain instance
+     */
+    public AclEntry[] getAcls(Object domainInstance);
+
+    /**
+     * Obtains the ACLs that apply to the specified domain instance, but only
+     * including those ACLs which have been granted to the presented
+     * <code>Authentication</code> object
+     *
+     * @param domainInstance the instance for which ACL information is required
+     *        (never <code>null</code>)
+     * @param authentication the prncipal for which ACL information should be
+     *        filtered (never <code>null</code>)
+     *
+     * @return only those ACLs applying to the domain instance that have been
+     *         granted to the principal (or <code>null</code>) if no such ACLs
+     *         are found
+     */
+    public AclEntry[] getAcls(Object domainInstance,
+        Authentication authentication);
+}

+ 82 - 0
core/src/main/java/org/acegisecurity/acl/AclProvider.java

@@ -0,0 +1,82 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl;
+
+import net.sf.acegisecurity.Authentication;
+
+/**
+ * Indicates a class can process a given domain object instance and
+ * authoritatively return the ACLs that apply.
+ * 
+ * <P>
+ * Implementations are typically called from the {@link AclProviderManager}.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public interface AclProvider {
+    //~ Methods ================================================================
+
+    /**
+     * Obtains the ACLs that apply to the specified domain instance.
+     * 
+     * <P>
+     * Will never be called unless the {@link #supports(Object)} method
+     * returned <code>true</code>.
+     * </p>
+     *
+     * @param domainInstance the instance for which ACL information is required
+     *        (never <code>null</code>)
+     *
+     * @return the ACLs that apply, or <code>null</code> if no ACLs apply to
+     *         the specified domain instance
+     */
+    public AclEntry[] getAcls(Object domainInstance);
+
+    /**
+     * Obtains the ACLs that apply to the specified domain instance
+     * and presented <code>Authentication</code> object.
+     *
+     * <P>
+     * Will never be called unless the {@link #supports(Object)} method
+     * returned <code>true</code>.
+     * </p>
+     * 
+     * @param domainInstance the instance for which ACL information is required
+     *        (never <code>null</code>)
+     * @param authentication the prncipal for which ACL information should be
+     *        filtered (never <code>null</code>)
+     *
+     * @return only those ACLs applying to the domain instance that have been
+     *         granted to the principal (or <code>null</code>) if no such ACLs
+     *         are found
+     */
+    public AclEntry[] getAcls(Object domainInstance,
+        Authentication authentication);
+
+    /**
+     * Indicates whether this <code>AclProvider</code> can authoritatively
+     * return ACL information for the specified domain object instance.
+     *
+     * @param domainInstance the instance for which ACL information is required
+     *        (never <code>null</code>)
+     *
+     * @return <code>true</code> if this provider is authoritative for the
+     *         specified domain object instance, <code>false</code> otherwise
+     */
+    public boolean supports(Object domainInstance);
+}

+ 159 - 0
core/src/main/java/org/acegisecurity/acl/AclProviderManager.java

@@ -0,0 +1,159 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl;
+
+import net.sf.acegisecurity.Authentication;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.beans.factory.InitializingBean;
+
+import java.util.Iterator;
+import java.util.List;
+
+
+/**
+ * Iterates through a list of {@link AclProvider}s to locate the ACLs that
+ * apply to a given domain object instance.
+ * 
+ * <P>
+ * If no compatible provider is found, it is assumed that no ACLs apply for the
+ * specified domain object instance and <code>null</code> is returned.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class AclProviderManager implements AclManager, InitializingBean {
+    //~ Static fields/initializers =============================================
+
+    private static final Log logger = LogFactory.getLog(AclProviderManager.class);
+
+    //~ Instance fields ========================================================
+
+    private List providers;
+
+    //~ Methods ================================================================
+
+    public AclEntry[] getAcls(Object domainInstance) {
+        if (domainInstance == null) {
+            throw new IllegalArgumentException(
+                "domainInstance is null - violating interface contract");
+        }
+
+        Iterator iter = providers.iterator();
+
+        while (iter.hasNext()) {
+            AclProvider provider = (AclProvider) iter.next();
+
+            if (provider.supports(domainInstance)) {
+                if (logger.isDebugEnabled()) {
+                    logger.debug("ACL lookup using "
+                        + provider.getClass().getName());
+                }
+
+                return provider.getAcls(domainInstance);
+            }
+        }
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("No AclProvider found for "
+                + domainInstance.toString());
+        }
+
+        return null;
+    }
+
+	public AclEntry[] getAcls(Object domainInstance,
+			Authentication authentication) {
+        if (domainInstance == null) {
+            throw new IllegalArgumentException(
+                "domainInstance is null - violating interface contract");
+        }
+        if (authentication == null) {
+            throw new IllegalArgumentException(
+                "authentication is null - violating interface contract");
+        }
+
+        Iterator iter = providers.iterator();
+
+        while (iter.hasNext()) {
+            AclProvider provider = (AclProvider) iter.next();
+
+            if (provider.supports(domainInstance)) {
+                if (logger.isDebugEnabled()) {
+                    logger.debug("ACL lookup using "
+                        + provider.getClass().getName());
+                }
+
+                return provider.getAcls(domainInstance, authentication);
+            }
+        }
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("No AclProvider found for "
+                + domainInstance.toString());
+        }
+
+        return null;
+	}
+	
+    /**
+     * Sets the {@link AclProvider} objects to be used for ACL determinations.
+     *
+     * @param newList that should be used for ACL determinations
+     *
+     * @throws IllegalArgumentException if an invalid provider was included in
+     *         the list
+     */
+    public void setProviders(List newList) {
+        checkIfValidList(newList);
+
+        Iterator iter = newList.iterator();
+
+        while (iter.hasNext()) {
+            Object currentObject = null;
+
+            try {
+                currentObject = iter.next();
+
+                AclProvider attemptToCast = (AclProvider) currentObject;
+            } catch (ClassCastException cce) {
+                throw new IllegalArgumentException("AclProvider "
+                    + currentObject.getClass().getName()
+                    + " must implement AclProvider");
+            }
+        }
+
+        this.providers = newList;
+    }
+
+    public List getProviders() {
+        return this.providers;
+    }
+
+    public void afterPropertiesSet() throws Exception {
+        checkIfValidList(this.providers);
+    }
+
+    private void checkIfValidList(List listToCheck) {
+        if ((listToCheck == null) || (listToCheck.size() == 0)) {
+            throw new IllegalArgumentException(
+                "A list of AclManagers is required");
+        }
+    }
+}

+ 279 - 0
core/src/main/java/org/acegisecurity/acl/basic/AbstractBasicAclEntry.java

@@ -0,0 +1,279 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl.basic;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.util.Arrays;
+
+
+/**
+ * Abstract implementation of {@link BasicAclEntry}.
+ * 
+ * <P>
+ * Provides core bit mask handling methods.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public abstract class AbstractBasicAclEntry implements BasicAclEntry {
+    //~ Static fields/initializers =============================================
+
+    private static final Log logger = LogFactory.getLog(AbstractBasicAclEntry.class);
+
+    //~ Instance fields ========================================================
+
+    private AclObjectIdentity aclObjectIdentity;
+    private AclObjectIdentity aclObjectParentIdentity;
+    private Object recipient;
+    private int[] validPermissions;
+    private int mask = 0; // default means no permissions
+
+    //~ Constructors ===========================================================
+
+    public AbstractBasicAclEntry(Object recipient,
+        AclObjectIdentity aclObjectIdentity,
+        AclObjectIdentity aclObjectParentIdentity, int mask) {
+        if (recipient == null) {
+            throw new IllegalArgumentException("recipient cannot be null");
+        }
+
+        if (aclObjectIdentity == null) {
+            throw new IllegalArgumentException(
+                "aclObjectIdentity cannot be null");
+        }
+
+        validPermissions = getValidPermissions();
+        Arrays.sort(validPermissions);
+
+        for (int i = 0; i < validPermissions.length; i++) {
+            if (logger.isDebugEnabled()) {
+                logger.debug("Valid permission:   "
+                    + printPermissionsBlock(validPermissions[i]) + " "
+                    + printBinary(validPermissions[i]) + " ("
+                    + validPermissions[i] + ")");
+            }
+        }
+
+        this.recipient = recipient;
+        this.aclObjectIdentity = aclObjectIdentity;
+        this.aclObjectParentIdentity = aclObjectParentIdentity;
+        this.mask = mask;
+    }
+
+    protected AbstractBasicAclEntry() {}
+
+    //~ Methods ================================================================
+
+    public void setAclObjectIdentity(AclObjectIdentity aclObjectIdentity) {
+        this.aclObjectIdentity = aclObjectIdentity;
+    }
+
+    public AclObjectIdentity getAclObjectIdentity() {
+        return this.aclObjectIdentity;
+    }
+
+    public void setAclObjectParentIdentity(
+        AclObjectIdentity aclObjectParentIdentity) {
+        this.aclObjectParentIdentity = aclObjectParentIdentity;
+    }
+
+    public AclObjectIdentity getAclObjectParentIdentity() {
+        return this.aclObjectParentIdentity;
+    }
+
+    /**
+     * Subclasses must indicate the permissions they support. Each base
+     * permission should be an integer with a base 2. ie: the first permission
+     * is 2^^0 (1), the second permission is 2^^1 (1), the third permission is
+     * 2^^2 (4) etc. Each base permission should be exposed by the subclass as
+     * a <code>public static final int</code>. It is further recommended that
+     * valid combinations of permissions are also exposed as <code>public
+     * static final int</code>s.
+     * 
+     * <P>
+     * This method returns all permission integers that are allowed to be used
+     * together. <B>This must include any combinations of valid
+     * permissions</b>. So if the permissions indicated by 2^^2 (4) and 2^^1
+     * (2) can be used together, one of the integers returned by this method
+     * must be 6 (4 + 2). Otherwise attempts to set the permission will be
+     * rejected, as the final resulting mask will be rejected.
+     * </p>
+     * 
+     * <P>
+     * Whilst it may seem unduly time onerous to return every valid permission
+     * <B>combination</B>, doing so delivers maximum flexibility in ensuring
+     * ACLs only reflect logical combinations. For example, it would be
+     * inappropriate to grant a "read" and "write" permission along with an
+     * "unrestricted" permission, as the latter implies the former
+     * permissions.
+     * </p>
+     *
+     * @return <b>every</b> valid combination of permissions
+     */
+    public abstract int[] getValidPermissions();
+
+    /**
+     * Outputs the permissions in a human-friendly format. For example, this
+     * method may return "CR-D" to indicate the passed integer permits create,
+     * permits read, does not permit update, and permits delete.
+     *
+     * @param i the integer containing the mask which should be printed
+     *
+     * @return the human-friend formatted block
+     */
+    public abstract String printPermissionsBlock(int i);
+
+    public void setMask(int mask) {
+        this.mask = mask;
+    }
+
+    public int getMask() {
+        return this.mask;
+    }
+
+    public boolean isPermitted(int permissionToCheck) {
+        return isPermitted(this.mask, permissionToCheck);
+    }
+
+    public void setRecipient(Object recipient) {
+        this.recipient = recipient;
+    }
+
+    public Object getRecipient() {
+        return this.recipient;
+    }
+
+    public int addPermission(int permissionToAdd) {
+        return addPermissions(new int[] {permissionToAdd});
+    }
+
+    public int addPermissions(int[] permissionsToAdd) {
+        if (logger.isDebugEnabled()) {
+            logger.debug("BEFORE Permissions: " + printPermissionsBlock(mask)
+                + " " + printBinary(mask) + " (" + mask + ")");
+        }
+
+        for (int i = 0; i < permissionsToAdd.length; i++) {
+            if (logger.isDebugEnabled()) {
+                logger.debug("Add     permission: "
+                    + printPermissionsBlock(permissionsToAdd[i]) + " "
+                    + printBinary(permissionsToAdd[i]) + " ("
+                    + permissionsToAdd[i] + ")");
+            }
+
+            this.mask |= permissionsToAdd[i];
+        }
+
+        if (Arrays.binarySearch(validPermissions, this.mask) < 0) {
+            throw new IllegalArgumentException(
+                "Resulting permission set will be invalid.");
+        } else {
+            if (logger.isDebugEnabled()) {
+                logger.debug("AFTER  Permissions: "
+                    + printPermissionsBlock(mask) + " " + printBinary(mask)
+                    + " (" + mask + ")");
+            }
+
+            return this.mask;
+        }
+    }
+
+    public int deletePermission(int permissionToDelete) {
+        return deletePermissions(new int[] {permissionToDelete});
+    }
+
+    public int deletePermissions(int[] permissionsToDelete) {
+        if (logger.isDebugEnabled()) {
+            logger.debug("BEFORE Permissions: " + printPermissionsBlock(mask)
+                + " " + printBinary(mask) + " (" + mask + ")");
+        }
+
+        for (int i = 0; i < permissionsToDelete.length; i++) {
+            if (logger.isDebugEnabled()) {
+                logger.debug("Delete  permission: "
+                    + printPermissionsBlock(permissionsToDelete[i]) + " "
+                    + printBinary(permissionsToDelete[i]) + " ("
+                    + permissionsToDelete[i] + ")");
+            }
+
+            this.mask &= ~permissionsToDelete[i];
+        }
+
+        if (Arrays.binarySearch(validPermissions, this.mask) < 0) {
+            throw new IllegalArgumentException(
+                "Resulting permission set will be invalid.");
+        } else {
+            if (logger.isDebugEnabled()) {
+                logger.debug("AFTER  Permissions: "
+                    + printPermissionsBlock(mask) + " " + printBinary(mask)
+                    + " (" + mask + ")");
+            }
+
+            return this.mask;
+        }
+    }
+
+    /**
+     * Outputs the permissions in human-friendly format for the current
+     * <code>AbstractBasicAclEntry</code>'s mask.
+     *
+     * @return the human-friendly formatted block for this instance
+     */
+    public String printPermissionsBlock() {
+        return printPermissionsBlock(this.mask);
+    }
+
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+        sb.append(getClass().getName());
+        sb.append("[").append(aclObjectIdentity).append(",").append(recipient);
+        sb.append("=").append(printPermissionsBlock(mask)).append(" ");
+        sb.append(printBinary(mask)).append(" (");
+        sb.append(mask).append(")").append("]");
+
+        return sb.toString();
+    }
+
+    public int togglePermission(int permissionToToggle) {
+        this.mask ^= permissionToToggle;
+
+        if (Arrays.binarySearch(validPermissions, this.mask) < 0) {
+            throw new IllegalArgumentException(
+                "Resulting permission set will be invalid.");
+        } else {
+            return this.mask;
+        }
+    }
+
+    protected boolean isPermitted(int maskToCheck, int permissionToCheck) {
+        return ((maskToCheck & permissionToCheck) == permissionToCheck);
+    }
+
+    private String printBinary(int i) {
+        String s = Integer.toString(i, 2);
+
+        String pattern = "................................";
+
+        String temp1 = pattern.substring(0, pattern.length() - s.length());
+
+        String temp2 = temp1 + s;
+
+        return temp2.replace('0', '.');
+    }
+}

+ 67 - 0
core/src/main/java/org/acegisecurity/acl/basic/AclObjectIdentity.java

@@ -0,0 +1,67 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl.basic;
+
+import java.io.Serializable;
+
+
+/**
+ * Interface representing the identity of an individual domain object instance.
+ * 
+ * <P>
+ * It should be noted that <code>AclObjectIdentity</code> instances are created
+ * in various locations throughout the package. As
+ * <code>AclObjectIdentity</code>s are used as the key for caching, it is
+ * essential that implementations provide methods so that object-equality
+ * rather than reference-equality can be relied upon by caches. In other
+ * words, a cache can consider two <code>AclObjectIdentity</code>s equal if
+ * <code>identity1.equals(identity2)</code>, rather than reference-equality of
+ * <code>identity1==identity2</code>.
+ * </p>
+ * 
+ * <P>
+ * In practical terms this means you must implement the standard
+ * <code>java.lang.Object</code> methods shown below. Depending on your
+ * cache's internal structure, you may also need to implement special
+ * interfaces such as <code>java.util.Comparator</code> or
+ * <code>java.lang.Comparable</code>.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public interface AclObjectIdentity extends Serializable {
+    //~ Methods ================================================================
+
+    /**
+     * Refer to the <code>java.lang.Object</code> documentation for the
+     * interface contract.
+     *
+     * @param obj to be compared
+     *
+     * @return <code>true</code> if the objects are equal, <code>false</code>
+     *         otherwise
+     */
+    public boolean equals(Object obj);
+
+    /**
+     * Refer to the <code>java.lang.Object</code> documentation for the
+     * interface contract.
+     *
+     * @return a hash code representation of this object
+     */
+    public int hashCode();
+}

+ 42 - 0
core/src/main/java/org/acegisecurity/acl/basic/AclObjectIdentityAware.java

@@ -0,0 +1,42 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl.basic;
+
+/**
+ * Indicates a domain object instance is able to provide {@link
+ * AclObjectIdentity} information.
+ * 
+ * <P>
+ * Domain objects must implement this interface if they wish to provide an
+ * <code>AclObjectIdentity</code> rather than it being determined by relying
+ * classes. Specifically, the {@link BasicAclProvider} detects and uses this
+ * interface.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public interface AclObjectIdentityAware {
+    //~ Methods ================================================================
+
+    /**
+     * Retrieves the <code>AclObjectIdentity</code> for this instance.
+     *
+     * @return the ACL object identity for this instance (can never be
+     *         <code>null</code>)
+     */
+    public AclObjectIdentity getAclObjectIdentity();
+}

+ 58 - 0
core/src/main/java/org/acegisecurity/acl/basic/BasicAclDao.java

@@ -0,0 +1,58 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl.basic;
+
+/**
+ * Represents a data access object that can return the {@link BasicAclEntry}s
+ * applying to a given ACL object identity.
+ * 
+ * <P>
+ * <code>BasicAclDao</code> implementations are responsible for interpreting a
+ * given {@link AclObjectIdentity} and being able to lookup and return the
+ * corresponding {@link BasicAclEntry}[]s.
+ * </p>
+ * 
+ * <P>
+ * <code>BasicAclDao</code>s many, but are not required to, allow the backend
+ * ACL repository to specify the class of <code>BasicAclEntry</code>
+ * implementations that should be returned.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public interface BasicAclDao {
+    //~ Methods ================================================================
+
+    /**
+     * Obtains the ACLs that apply to the specified domain instance.
+     * 
+     * <P>
+     * Does <b>not</b> perform caching, include ACLs from any inheritance
+     * hierarchy or filter returned objects based on effective permissions.
+     * Implementations are solely responsible for returning ACLs found in the
+     * ACL repository for the specified object identity.
+     * </p>
+     *
+     * @param aclObjectIdentity the domain object instance that ACL information
+     *        is being requested for (never <code>null</code>)
+     *
+     * @return the ACLs that apply (no <code>null</code>s are permitted in the
+     *         array), or <code>null</code> if no ACLs could be found for the
+     *         specified ACL object identity
+     */
+    public BasicAclEntry[] getAcls(AclObjectIdentity aclObjectIdentity);
+}

+ 126 - 0
core/src/main/java/org/acegisecurity/acl/basic/BasicAclEntry.java

@@ -0,0 +1,126 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl.basic;
+
+import net.sf.acegisecurity.acl.AclEntry;
+
+
+/**
+ * Represents an entry in an access control list.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public interface BasicAclEntry extends AclEntry {
+    //~ Methods ================================================================
+
+    /**
+     * This setter should <B>only</B> be used by DAO implementations.
+     *
+     * @param aclObjectIdentity an object which can be used to uniquely
+     *        identify the domain object instance subject of this ACL entry
+     */
+    public void setAclObjectIdentity(AclObjectIdentity aclObjectIdentity);
+
+    /**
+     * Indicates the domain object instance that is subject of this
+     * <code>BasicAclEntry</code>. This information may be of interest to
+     * relying classes (voters and business methods) that wish to know the
+     * actual origination of the ACL entry (so as to distinguish individual
+     * ACL entries from others contributed by the inheritance hierarchy).
+     *
+     * @return the ACL object identity that is subject of this ACL entry (never
+     *         <code>null</code>)
+     */
+    public AclObjectIdentity getAclObjectIdentity();
+
+    /**
+     * This setter should <B>only</B> be used by DAO implementations.
+     *
+     * @param aclObjectParentIdentity an object which represents the parent of
+     *        the domain object instance subject of this ACL entry, or
+     *        <code>null</code> if either the domain object instance has no
+     *        parent or its parent should be not used to compute an
+     *        inheritance hierarchy
+     */
+    public void setAclObjectParentIdentity(
+        AclObjectIdentity aclObjectParentIdentity);
+
+    /**
+     * Indicates any ACL parent of the domain object instance. This is used by
+     * <code>BasicAclProvider</code> to walk the inheritance hierarchy. An
+     * domain object instance need <b>not</b> have a parent.
+     *
+     * @return the ACL object identity that is the parent of this ACL entry
+     *         (may be <code>null</code> if no parent should be consulted)
+     */
+    public AclObjectIdentity getAclObjectParentIdentity();
+
+    /**
+     * This setter should <B>only</B> be used by DAO implementations.
+     *
+     * @param mask the integer representing the permissions bit mask
+     */
+    public void setMask(int mask);
+
+    /**
+     * Access control lists in this package are based on bit masking. The
+     * integer value of the bit mask can be obtained from this method.
+     *
+     * @return the bit mask applicable to this ACL entry (zero indicates a bit
+     *         mask where no permissions have been granted)
+     */
+    public int getMask();
+
+    /**
+     * This setter should <B>only</B> be used by DAO implementations.
+     *
+     * @param recipient a representation of the recipient of this ACL entry
+     *        that makes sense to an <code>EffectiveAclsResolver</code>
+     *        implementation
+     */
+    public void setRecipient(Object recipient);
+
+    /**
+     * A domain object instance will usually have multiple
+     * <code>BasicAclEntry</code>s. Each separate <code>BasicAclEntry</code>
+     * applies to a particular "recipient". Typical examples of recipients
+     * include (but do not necessarily have to include) usernames, role names,
+     * complex granted authorities etc.
+     * 
+     * <P>
+     * <B>It is essential that only one <code>BasicAclEntry</code> exists for a
+     * given recipient</B>. Otherwise conflicts as to the mask that should
+     * apply to a given recipient will occur.
+     * </p>
+     * 
+     * <P>
+     * This method indicates which recipient this <code>BasicAclEntry</code>
+     * applies to. The returned object type will vary depending on the type of
+     * recipient. For instance, it might be a <code>String</code> containing a
+     * username, or a <code>GrantedAuthorityImpl</code> containing a complex
+     * granted authority that is being granted the permissions contained in
+     * this access control entry. The {@link EffectiveAclsResolver} and {@link
+     * BasicAclProvider#getAcls(Object, Authentication)} can process the
+     * different recipient types and return only those that apply to a
+     * specified <code>Authentication</code> object.
+     * </p>
+     *
+     * @return the recipient of this access control list entry (never
+     *         <code>null</code>)
+     */
+    public Object getRecipient();
+}

+ 61 - 0
core/src/main/java/org/acegisecurity/acl/basic/BasicAclEntryCache.java

@@ -0,0 +1,61 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl.basic;
+
+/**
+ * Provides a cache of {@link BasicAclEntry} objects.
+ * 
+ * <P>
+ * Implementations should provide appropriate methods to set their cache
+ * parameters (eg time-to-live) and/or force removal of entities before their
+ * normal expiration. These are not part of the
+ * <code>BasicAclEntryCache</code> interface contract because they vary
+ * depending on the type of caching system used (eg in-memory vs disk vs
+ * cluster vs hybrid).
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public interface BasicAclEntryCache {
+    //~ Methods ================================================================
+
+    /**
+     * Obtains an array of {@link BasicAclEntry}s from the cache.
+     *
+     * @param aclObjectIdentity which should be obtained from the cache
+     *
+     * @return any applicable <code>BasicAclEntry</code>s (no
+     *         <code>null</code>s are permitted in the returned array) or
+     *         <code>null</code> if the object identity could not be found or
+     *         if the cache entry has expired
+     */
+    public BasicAclEntry[] getEntriesFromCache(
+        AclObjectIdentity aclObjectIdentity);
+
+    /**
+     * Places an array of {@link BasicAclEntry}s in the cache.
+     * 
+     * <P>
+     * No <code>null</code>s are allowed in the passed array. If any
+     * <code>null</code> is passed, the implementation may throw an exception.
+     * </p>
+     *
+     * @param basicAclEntry the ACL entries to cache (the key will be extracted
+     *        from the {@link BasicAclEntry#getAclObjectIdentity()} method
+     */
+    public void putEntriesInCache(BasicAclEntry[] basicAclEntry);
+}

+ 341 - 0
core/src/main/java/org/acegisecurity/acl/basic/BasicAclProvider.java

@@ -0,0 +1,341 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl.basic;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.acl.AclEntry;
+import net.sf.acegisecurity.acl.AclProvider;
+import net.sf.acegisecurity.acl.basic.cache.NullAclEntryCache;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.beans.factory.InitializingBean;
+
+import java.lang.reflect.Constructor;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * <P>
+ * Retrieves access control lists (ACL) entries for domain object instances
+ * from a data access object (DAO).
+ * </p>
+ * 
+ * <P>
+ * This implementation will provide ACL lookup services for any object that it
+ * can determine the {@link AclObjectIdentity} for by calling the {@link
+ * #obtainIdentity(Object)} method. Subclasses can override this method if
+ * they only want the <code>BasicAclProvider</code> responding to particular
+ * domain object instances.
+ * </p>
+ * 
+ * <P>
+ * <code>BasicAclProvider</code> will walk an inheritance hierarchy if a
+ * <code>BasicAclEntry</code> returned by the DAO indicates it has a parent.
+ * NB: inheritance occurs at a <I>domain instance object</I> level. It does
+ * not occur at an ACL recipient level. This means
+ * <B>all</B><code>BasicAclEntry</code>s for a given domain instance object
+ * <B>must</B> have the <B>same</B> parent identity, or
+ * <B>all</B><code>BasicAclEntry</code>s must have <code>null</code> as their
+ * parent identity.
+ * </p>
+ * 
+ * <P>
+ * A cache should be used. This is provided by the {@link BasicAclEntryCache}.
+ * <code>BasicAclProvider</code> by default is setup to use the {@link
+ * NullAclEntryCache}, which performs no caching.
+ * </p>
+ * 
+ * <P>
+ * To implement the {@link #getAcls(Object, Authentication)} method,
+ * <code>BasicAclProvider</code> requires a {@link EffectiveAclsResolver} to
+ * be configured against it. By default the {@link
+ * GrantedAuthorityEffectiveAclsResolver} is used.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class BasicAclProvider implements AclProvider, InitializingBean {
+    //~ Static fields/initializers =============================================
+
+    private static final Log logger = LogFactory.getLog(BasicAclProvider.class);
+
+    /**
+     * Marker added to the cache to indicate an AclObjectIdentity has no
+     * corresponding BasicAclEntry[]s
+     */
+    private static String RECIPIENT_FOR_CACHE_EMPTY = "RESERVED_RECIPIENT_NOBODY";
+
+    //~ Instance fields ========================================================
+
+    /**
+     * Must be set to an appropriate data access object. Defaults to
+     * <code>null</code>.
+     */
+    private BasicAclDao basicAclDao;
+    private BasicAclEntryCache basicAclEntryCache = new NullAclEntryCache();
+    private Class defaultAclObjectIdentityClass = NamedEntityObjectIdentity.class;
+    private EffectiveAclsResolver effectiveAclsResolver = new GrantedAuthorityEffectiveAclsResolver();
+
+    //~ Methods ================================================================
+
+    public AclEntry[] getAcls(Object domainInstance) {
+        Map map = new HashMap();
+
+        AclObjectIdentity aclIdentity = obtainIdentity(domainInstance);
+
+        if (aclIdentity == null) {
+            throw new IllegalArgumentException(
+                "domainInstance is not supported by this provider");
+        }
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("Looking up: " + aclIdentity.toString());
+        }
+
+        BasicAclEntry[] instanceAclEntries = lookup(aclIdentity);
+
+        // Exit if there is no ACL information or parent for this instance
+        if (instanceAclEntries == null) {
+            return null;
+        }
+
+        // Add the leaf objects to the Map, keyed on recipient
+        for (int i = 0; i < instanceAclEntries.length; i++) {
+            if (logger.isDebugEnabled()) {
+                logger.debug("Explicit add: "
+                    + instanceAclEntries[i].toString());
+            }
+
+            map.put(instanceAclEntries[i].getRecipient(), instanceAclEntries[i]);
+        }
+
+        AclObjectIdentity parent = instanceAclEntries[0]
+            .getAclObjectParentIdentity();
+
+        while (parent != null) {
+            BasicAclEntry[] parentAclEntries = lookup(parent);
+
+            if (logger.isDebugEnabled()) {
+                logger.debug("Parent lookup: " + parent.toString());
+            }
+
+            // Exit loop if parent couldn't be found (unexpected condition)
+            if (parentAclEntries == null) {
+                if (logger.isDebugEnabled()) {
+                    logger.debug("Parent could not be found in ACL repository");
+                }
+
+                break;
+            }
+
+            // Now add each _NEW_ recipient to the list
+            for (int i = 0; i < parentAclEntries.length; i++) {
+                if (!map.containsKey(parentAclEntries[i].getRecipient())) {
+                    if (logger.isDebugEnabled()) {
+                        logger.debug("Added parent to map: "
+                            + parentAclEntries[i].toString());
+                    }
+
+                    map.put(parentAclEntries[i].getRecipient(),
+                        parentAclEntries[i]);
+                } else {
+                    if (logger.isDebugEnabled()) {
+                        logger.debug("Did NOT add parent to map: "
+                            + parentAclEntries[i].toString());
+                    }
+                }
+            }
+
+            // Prepare for next iteration of while loop
+            parent = parentAclEntries[0].getAclObjectParentIdentity();
+        }
+
+        Collection collection = map.values();
+
+        return (AclEntry[]) collection.toArray(new AclEntry[] {});
+    }
+
+    public AclEntry[] getAcls(Object domainInstance,
+        Authentication authentication) {
+        AclEntry[] allAcls = (AclEntry[]) this.getAcls(domainInstance);
+
+        return this.effectiveAclsResolver.resolveEffectiveAcls(allAcls,
+            authentication);
+    }
+
+    public void setBasicAclDao(BasicAclDao basicAclDao) {
+        this.basicAclDao = basicAclDao;
+    }
+
+    public BasicAclDao getBasicAclDao() {
+        return basicAclDao;
+    }
+
+    public void setBasicAclEntryCache(BasicAclEntryCache basicAclEntryCache) {
+        this.basicAclEntryCache = basicAclEntryCache;
+    }
+
+    public BasicAclEntryCache getBasicAclEntryCache() {
+        return basicAclEntryCache;
+    }
+
+    /**
+     * Allows selection of the <code>AclObjectIdentity</code> class that an
+     * attempt should be made to construct if the passed object does not
+     * implement <code>AclObjectIdentityAware</code>.
+     * 
+     * <P>
+     * NB: Any <code>defaultAclObjectIdentityClass</code><b>must</b> provide a
+     * public constructor that accepts an <code>Object</code>. Otherwise it is
+     * not possible for the <code>BasicAclProvider</code> to try to create the
+     * <code>AclObjectIdentity</code> instance at runtime.
+     * </p>
+     *
+     * @param defaultAclObjectIdentityClass
+     */
+    public void setDefaultAclObjectIdentityClass(
+        Class defaultAclObjectIdentityClass) {
+        this.defaultAclObjectIdentityClass = defaultAclObjectIdentityClass;
+    }
+
+    public Class getDefaultAclObjectIdentityClass() {
+        return defaultAclObjectIdentityClass;
+    }
+
+    public void setEffectiveAclsResolver(
+        EffectiveAclsResolver effectiveAclsResolver) {
+        this.effectiveAclsResolver = effectiveAclsResolver;
+    }
+
+    public EffectiveAclsResolver getEffectiveAclsResolver() {
+        return effectiveAclsResolver;
+    }
+
+    public void afterPropertiesSet() {
+        if (basicAclDao == null) {
+            throw new IllegalArgumentException("basicAclDao required");
+        }
+
+        if (basicAclEntryCache == null) {
+            throw new IllegalArgumentException("basicAclEntryCache required");
+        }
+
+        if (effectiveAclsResolver == null) {
+            throw new IllegalArgumentException("effectiveAclsResolver required");
+        }
+
+        if ((defaultAclObjectIdentityClass == null)
+            || (!AclObjectIdentity.class.isAssignableFrom(
+                this.defaultAclObjectIdentityClass))) {
+            throw new IllegalArgumentException(
+                "defaultAclObjectIdentityClass that implements AclObjectIdentity is required");
+        }
+
+        try {
+            Constructor constructor = defaultAclObjectIdentityClass
+                .getConstructor(new Class[] {Object.class});
+        } catch (NoSuchMethodException nsme) {
+            throw new IllegalArgumentException(
+                "defaultAclObjectIdentityClass must provide a constructor that accepts the domain object instance!");
+        }
+    }
+
+    /**
+     * Indicates support for the passed object if it an
+     * <code>AclObjectIdentity</code> is returned by {@link
+     * #obtainIdentity(Object)}.
+     *
+     * @param domainInstance the instance to check
+     *
+     * @return <code>true</code> if this provider supports the passed object,
+     *         <code>false</code> otherwise
+     */
+    public boolean supports(Object domainInstance) {
+        if (obtainIdentity(domainInstance) == null) {
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * This method looks up the <code>AclObjectIdentity</code> of a passed
+     * domain object instance.
+     * 
+     * <P>
+     * This implementation attempts to obtain the
+     * <code>AclObjectIdentity</code> via reflection inspection of the class
+     * for the {@link AclObjectIdentityAware} interface. If this fails, an
+     * attempt is made to construct a {@link
+     * #getDefaultAclObjectIdentityClass()} object by passing the domain
+     * instance object into its constructor.
+     * </p>
+     *
+     * @param domainInstance the domain object instance (never
+     *        <code>null</code>)
+     *
+     * @return an ACL object identity, or <code>null</code> if one could not be
+     *         obtained
+     */
+    protected AclObjectIdentity obtainIdentity(Object domainInstance) {
+        if (domainInstance instanceof AclObjectIdentityAware) {
+            AclObjectIdentityAware aclObjectIdentityAware = (AclObjectIdentityAware) domainInstance;
+
+            return aclObjectIdentityAware.getAclObjectIdentity();
+        }
+
+        try {
+            Constructor constructor = defaultAclObjectIdentityClass
+                .getConstructor(new Class[] {Object.class});
+
+            return (AclObjectIdentity) constructor.newInstance(new Object[] {domainInstance});
+        } catch (Exception ex) {
+            return null;
+        }
+    }
+
+    private BasicAclEntry[] lookup(AclObjectIdentity aclObjectIdentity) {
+        BasicAclEntry[] result = basicAclEntryCache.getEntriesFromCache(aclObjectIdentity);
+
+        if (result != null) {
+            if (result[0].getRecipient().equals(RECIPIENT_FOR_CACHE_EMPTY)) {
+                return null;
+            } else {
+                return result;
+            }
+        }
+
+        result = basicAclDao.getAcls(aclObjectIdentity);
+
+        if (result == null) {
+            SimpleAclEntry[] emptyAclEntries = {new SimpleAclEntry(RECIPIENT_FOR_CACHE_EMPTY,
+                        aclObjectIdentity, null, 0)};
+            basicAclEntryCache.putEntriesInCache(emptyAclEntries);
+
+            return null;
+        }
+
+        basicAclEntryCache.putEntriesInCache(result);
+
+        return result;
+    }
+}

+ 65 - 0
core/src/main/java/org/acegisecurity/acl/basic/EffectiveAclsResolver.java

@@ -0,0 +1,65 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl.basic;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.acl.AclEntry;
+
+
+/**
+ * Determines the ACLs that are effective for a given
+ * <code>Authentication</code> object.
+ * 
+ * <P>
+ * Implementations will vary depending on their ability to interpret the
+ * "recipient" object types contained in {@link BasicAclEntry} instances, and
+ * how those recipient object types correspond to
+ * <code>Authentication</code>-presented principals and granted authorities.
+ * </p>
+ * 
+ * <P>
+ * Implementations should not filter the resulting ACL list from lower-order
+ * permissions. So if a resulting ACL list grants a "read" permission, an
+ * "unlimited" permission and a "zero" permission (due to the effective ACLs
+ * for different granted authorities held by the <code>Authentication</code>
+ * object), all three permissions would be returned as distinct
+ * <code>BasicAclEntry</code> instances. It is the responsibility of the
+ * relying classes (voters and business methods) to ignore or handle
+ * lower-order permissions in a business logic dependent manner.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public interface EffectiveAclsResolver {
+    //~ Methods ================================================================
+
+    /**
+     * Determines the ACLs that apply to the presented
+     * <code>Authentication</code> object.
+     *
+     * @param allAcls every ACL assigned to a domain object instance
+     * @param filteredBy the principal (populated with
+     *        <code>GrantedAuthority</code>s along with any other members that
+     *        relate to role or group membership) that effective ACLs should
+     *        be returned for
+     *
+     * @return the ACLs that apply to the presented principal, or
+     *         <code>null</code> if there are none after filtering
+     */
+    public AclEntry[] resolveEffectiveAcls(AclEntry[] allAcls,
+        Authentication filteredBy);
+}

+ 102 - 0
core/src/main/java/org/acegisecurity/acl/basic/GrantedAuthorityEffectiveAclsResolver.java

@@ -0,0 +1,102 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl.basic;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.GrantedAuthority;
+import net.sf.acegisecurity.acl.AclEntry;
+
+import java.util.List;
+import java.util.Vector;
+
+
+/**
+ * Simple implementation of {@link EffectiveAclsResolver}.
+ * 
+ * <P>
+ * This implementation does not need to understand the "recipient" types
+ * presented in a <code>BasicAclEntry</code> because it merely delegates to
+ * the detected {@link Authentication#getPrincipal()} or {@link
+ * Authentication#getAuthorities()}. The principal object or granted
+ * authorities object has its <code>Object.equals(recipient)</code> method
+ * called to make the decision as to whether the recipient in the
+ * <code>BasicAclEntry</code> is the same as the principal or granted
+ * authority.
+ * </p>
+ * 
+ * <P>
+ * This class should prove an adequate ACLs resolver if you're using standard
+ * Acegi Security classes. This is because the typical
+ * <code>Authentication</code> token is
+ * <code>UsernamePasswordAuthenticationToken</code>, which for its
+ * <code>principal</code> is usually a <code>String</code>. The
+ * <code>GrantedAuthorityImpl</code> is typically used for granted
+ * authorities, which tests for equality based on a <code>String</code>. This
+ * means <code>BasicAclDao</code>s simply need to return a <code>String</code>
+ * to represent the recipient. If you use non-<code>String</code> objects, you
+ * will probably require an alternative <code>EffectiveAclsResolver</code>.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class GrantedAuthorityEffectiveAclsResolver
+    implements EffectiveAclsResolver {
+    //~ Methods ================================================================
+
+    public AclEntry[] resolveEffectiveAcls(AclEntry[] allAcls,
+        Authentication filteredBy) {
+        List list = new Vector();
+
+        for (int i = 0; i < allAcls.length; i++) {
+            if (!(allAcls[i] instanceof BasicAclEntry)) {
+                continue;
+            }
+
+            Object recipient = ((BasicAclEntry) allAcls[i]).getRecipient();
+
+            // Allow the Authentication's getPrincipal to decide whether
+            // the presented recipient is "equal" (allows BasicAclDaos to
+            // return Strings rather than proper objects in simple cases)
+            if (filteredBy.getPrincipal().equals(recipient)) {
+                list.add(allAcls[i]);
+            } else {
+                // No direct match against principal; try each authority.
+                // As with the principal, allow each of the Authentication's
+                // granted authorities to decide whether the presented
+                // recipient is "equal"
+                GrantedAuthority[] authorities = filteredBy.getAuthorities();
+
+                if ((authorities == null) || (authorities.length == 0)) {
+                    continue;
+                }
+
+                for (int k = 0; k < authorities.length; k++) {
+                    if (authorities[k].equals(recipient)) {
+                        list.add(allAcls[i]);
+                    }
+                }
+            }
+        }
+
+        // return null if appropriate (as per interface contract)
+        if (list.size() > 0) {
+            return (BasicAclEntry[]) list.toArray(new BasicAclEntry[] {});
+        } else {
+            return null;
+        }
+    }
+}

+ 161 - 0
core/src/main/java/org/acegisecurity/acl/basic/NamedEntityObjectIdentity.java

@@ -0,0 +1,161 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl.basic;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+
+/**
+ * Simple implementation of {@link AclObjectIdentity}.
+ * 
+ * <P>
+ * Uses <code>String</code>s to store the identity of the domain object
+ * instance. Also offers a constructor that uses reflection to build the
+ * identity information.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class NamedEntityObjectIdentity implements AclObjectIdentity {
+    //~ Instance fields ========================================================
+
+    private String classname;
+    private String id;
+
+    //~ Constructors ===========================================================
+
+    public NamedEntityObjectIdentity(String classname, String id) {
+        if ((classname == null) || "".equals(classname)) {
+            throw new IllegalArgumentException("classname required");
+        }
+
+        if ((id == null) || "".equals(id)) {
+            throw new IllegalArgumentException("id required");
+        }
+
+        this.classname = classname;
+        this.id = id;
+    }
+
+    /**
+     * Creates the <code>NamedEntityObjectIdentity</code> based on the passed
+     * object instance. The passed object must provide a <code>getId()</code>
+     * method, otherwise an exception will be thrown.
+     *
+     * @param object the domain object instance to create an identity for
+     *
+     * @throws IllegalAccessException
+     * @throws InvocationTargetException
+     * @throws IllegalArgumentException
+     */
+    public NamedEntityObjectIdentity(Object object)
+        throws IllegalAccessException, InvocationTargetException {
+        if (object == null) {
+            throw new IllegalArgumentException("object cannot be null");
+        }
+
+        this.classname = object.getClass().getName();
+
+        Class clazz = object.getClass();
+
+        try {
+            Method id = clazz.getMethod("getId", null);
+            Object result = id.invoke(object, null);
+            this.id = result.toString();
+        } catch (NoSuchMethodException nsme) {
+            throw new IllegalArgumentException(
+                "object does not provide a getId() method");
+        }
+    }
+
+    protected NamedEntityObjectIdentity() {
+        throw new IllegalArgumentException("Cannot use default constructor");
+    }
+
+    //~ Methods ================================================================
+
+    /**
+     * Indicates the classname portion of the object identity.
+     *
+     * @return the classname (never <code>null</code>)
+     */
+    public String getClassname() {
+        return classname;
+    }
+
+    /**
+     * Indicates the instance identity portion of the object identity.
+     *
+     * @return the instance identity (never <code>null</code>)
+     */
+    public String getId() {
+        return id;
+    }
+
+    /**
+     * Important so caching operates properly.
+     * 
+     * <P>
+     * Considers an object of the same class equal if it has the same
+     * <code>classname</code> and <code>id</code> properties.
+     * </p>
+     *
+     * @param arg0 object to compare
+     *
+     * @return <code>true</code> if the presented object matches this object
+     */
+    public boolean equals(Object arg0) {
+        if (arg0 == null) {
+            return false;
+        }
+
+        if (!(arg0 instanceof NamedEntityObjectIdentity)) {
+            return false;
+        }
+
+        NamedEntityObjectIdentity other = (NamedEntityObjectIdentity) arg0;
+
+        if (this.getId().equals(other.getId())
+            && this.getClassname().equals(other.getClassname())) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Important so caching operates properly.
+     *
+     * @return the hash of the classname and id
+     */
+    public int hashCode() {
+        StringBuffer sb = new StringBuffer();
+        sb.append(this.classname).append(this.id);
+
+        return sb.toString().hashCode();
+    }
+
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+        sb.append(this.getClass().getName()).append("[");
+        sb.append("Classname: ").append(this.classname);
+        sb.append("; Identity: ").append(this.id).append("]");
+
+        return sb.toString();
+    }
+}

+ 112 - 0
core/src/main/java/org/acegisecurity/acl/basic/SimpleAclEntry.java

@@ -0,0 +1,112 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl.basic;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+
+/**
+ * Stores some privileges typical of a domain object.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class SimpleAclEntry extends AbstractBasicAclEntry {
+    //~ Static fields/initializers =============================================
+
+    private static final Log logger = LogFactory.getLog(SimpleAclEntry.class);
+
+    // Base permissions we permit
+    public static final int NOTHING = 0;
+    public static final int ADMINISTRATION = (int) Math.pow(2, 0);
+    public static final int READ = (int) Math.pow(2, 1);
+    public static final int WRITE = (int) Math.pow(2, 2);
+    public static final int CREATE = (int) Math.pow(2, 3);
+    public static final int DELETE = (int) Math.pow(2, 4);
+
+    // Combinations of base permissions we permit
+    public static final int READ_WRITE_CREATE_DELETE = READ | WRITE | CREATE
+        | DELETE;
+    public static final int READ_WRITE_CREATE = READ | WRITE | CREATE;
+    public static final int READ_WRITE = READ | WRITE;
+    public static final int READ_WRITE_DELETE = READ | WRITE | DELETE;
+
+    // Array required by the abstract superclass via getValidPermissions()
+    private static final int[] validPermissions = {NOTHING, ADMINISTRATION, READ, WRITE, CREATE, DELETE, READ_WRITE_CREATE_DELETE, READ_WRITE_CREATE, READ_WRITE, READ_WRITE_DELETE};
+
+    //~ Constructors ===========================================================
+
+    /**
+     * Allows {@link BasicAclDao} implementations to construct this object
+     * using <code>newInstance()</code>.
+     * 
+     * <P>
+     * Normal classes should <B>not</B> use this default constructor.
+     * </p>
+     */
+    public SimpleAclEntry() {
+        super();
+    }
+
+    public SimpleAclEntry(Object recipient,
+        AclObjectIdentity aclObjectIdentity,
+        AclObjectIdentity aclObjectParentIdentity, int mask) {
+        super(recipient, aclObjectIdentity, aclObjectParentIdentity, mask);
+    }
+
+    //~ Methods ================================================================
+
+    public int[] getValidPermissions() {
+        return validPermissions;
+    }
+
+    public String printPermissionsBlock(int i) {
+        StringBuffer sb = new StringBuffer();
+
+        if (isPermitted(i, ADMINISTRATION)) {
+            sb.append('A');
+        } else {
+            sb.append('-');
+        }
+
+        if (isPermitted(i, READ)) {
+            sb.append('R');
+        } else {
+            sb.append('-');
+        }
+
+        if (isPermitted(i, WRITE)) {
+            sb.append('W');
+        } else {
+            sb.append('-');
+        }
+
+        if (isPermitted(i, CREATE)) {
+            sb.append('C');
+        } else {
+            sb.append('-');
+        }
+
+        if (isPermitted(i, DELETE)) {
+            sb.append('D');
+        } else {
+            sb.append('-');
+        }
+
+        return sb.toString();
+    }
+}

+ 78 - 0
core/src/main/java/org/acegisecurity/acl/basic/cache/BasicAclEntryHolder.java

@@ -0,0 +1,78 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl.basic.cache;
+
+import net.sf.acegisecurity.acl.basic.BasicAclEntry;
+
+import java.io.Serializable;
+
+
+/**
+ * Used by {@link EhCacheBasedAclEntryCache} to store the array of
+ * <code>BasicAclEntry</code>s in the cache.
+ * 
+ * <P>
+ * This is necessary because caches store a single object per key, not an
+ * array.
+ * </p>
+ * 
+ * <P>
+ * This class uses value object semantics. ie: construction-based
+ * initialisation without any setters for the properties.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class BasicAclEntryHolder implements Serializable {
+    //~ Instance fields ========================================================
+
+    private BasicAclEntry[] basicAclEntries;
+
+    //~ Constructors ===========================================================
+
+    /**
+     * Constructs the <code>BasicAclEntryHolder</code>.
+     *
+     * @param aclEntries to cache (any <code>null</code>s will cause an
+     *        exception, which should not be a problem as the contract for
+     *        <code>BasicAclEntryCache</code> allows exceptions if
+     *        <code>null</code>s are presented)
+     *
+     * @throws IllegalArgumentException if a <code>null</code> exists anywhere
+     *         in the <code>aclEntries</code> or if a <code>null</code> is
+     *         passed to the constructor
+     */
+    public BasicAclEntryHolder(BasicAclEntry[] aclEntries) {
+        if (aclEntries == null) {
+            throw new IllegalArgumentException("aclEntries cannot be null");
+        }
+
+        for (int i = 0; i < aclEntries.length; i++) {
+            if (aclEntries[i] == null) {
+                throw new IllegalArgumentException("aclEntries cannot be null");
+            }
+        }
+
+        this.basicAclEntries = aclEntries;
+    }
+
+    //~ Methods ================================================================
+
+    public BasicAclEntry[] getBasicAclEntries() {
+        return basicAclEntries;
+    }
+}

+ 135 - 0
core/src/main/java/org/acegisecurity/acl/basic/cache/EhCacheBasedAclEntryCache.java

@@ -0,0 +1,135 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl.basic.cache;
+
+import net.sf.acegisecurity.acl.basic.AclObjectIdentity;
+import net.sf.acegisecurity.acl.basic.BasicAclEntry;
+import net.sf.acegisecurity.acl.basic.BasicAclEntryCache;
+
+import net.sf.ehcache.Cache;
+import net.sf.ehcache.CacheException;
+import net.sf.ehcache.CacheManager;
+import net.sf.ehcache.Element;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.InitializingBean;
+
+import org.springframework.dao.DataRetrievalFailureException;
+
+
+/**
+ * Caches <code>BasicAclEntry</code>s using  <A
+ * HREF="http://ehcache.sourceforge.net">EHCACHE</a>.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class EhCacheBasedAclEntryCache implements BasicAclEntryCache,
+    InitializingBean, DisposableBean {
+    //~ Static fields/initializers =============================================
+
+    private static final Log logger = LogFactory.getLog(EhCacheBasedAclEntryCache.class);
+    private static final String CACHE_NAME = "ehCacheBasedAclEntryCache";
+
+    //~ Instance fields ========================================================
+
+    private Cache cache;
+    private CacheManager manager;
+    private int minutesToIdle = 5;
+
+    //~ Methods ================================================================
+
+    public BasicAclEntry[] getEntriesFromCache(
+        AclObjectIdentity aclObjectIdentity) {
+        Element element = null;
+
+        try {
+            element = cache.get(aclObjectIdentity);
+        } catch (CacheException cacheException) {
+            throw new DataRetrievalFailureException("Cache failure: "
+                + cacheException.getMessage());
+        }
+
+        // Return null if cache element has expired or not found
+        if (element == null) {
+            if (logger.isDebugEnabled()) {
+                logger.debug("Cache miss: " + aclObjectIdentity);
+            }
+
+            return null;
+        }
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("Cache hit: " + (element != null) + "; object: "
+                + aclObjectIdentity);
+        }
+
+        BasicAclEntryHolder holder = (BasicAclEntryHolder) element.getValue();
+
+        return holder.getBasicAclEntries();
+    }
+
+    public void setMinutesToIdle(int minutesToIdle) {
+        this.minutesToIdle = minutesToIdle;
+    }
+
+    /**
+     * Specifies how many minutes an entry will remain in the cache from when
+     * it was last accessed.
+     * 
+     * <P>
+     * Defaults to 5 minutes.
+     * </p>
+     *
+     * @return Returns the minutes an element remains in the cache
+     */
+    public int getMinutesToIdle() {
+        return minutesToIdle;
+    }
+
+    public void afterPropertiesSet() throws Exception {
+        if (CacheManager.getInstance().cacheExists(CACHE_NAME)) {
+            // don’t remove the cache
+        } else {
+            manager = CacheManager.create();
+
+            // Cache name, max memory, overflowToDisk, eternal, timeToLive, timeToIdle
+            cache = new Cache(CACHE_NAME, Integer.MAX_VALUE, false, false,
+                    minutesToIdle * 60, minutesToIdle * 60);
+
+            manager.addCache(cache);
+        }
+    }
+
+    public void destroy() throws Exception {
+        manager.removeCache(CACHE_NAME);
+    }
+
+    public void putEntriesInCache(BasicAclEntry[] basicAclEntry) {
+        BasicAclEntryHolder holder = new BasicAclEntryHolder(basicAclEntry);
+        Element element = new Element(basicAclEntry[0].getAclObjectIdentity(),
+                holder);
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("Cache put: " + element.getKey());
+        }
+
+        cache.put(element);
+    }
+}

+ 56 - 0
core/src/main/java/org/acegisecurity/acl/basic/cache/NullAclEntryCache.java

@@ -0,0 +1,56 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl.basic.cache;
+
+import net.sf.acegisecurity.acl.basic.AclObjectIdentity;
+import net.sf.acegisecurity.acl.basic.BasicAclEntry;
+import net.sf.acegisecurity.acl.basic.BasicAclEntryCache;
+
+
+/**
+ * Does not perform any caching.
+ * 
+ * <P>
+ * <B>Do not use in production settings</B>, as ACL queries are likely to be
+ * extensive.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class NullAclEntryCache implements BasicAclEntryCache {
+    //~ Methods ================================================================
+
+    /**
+     * As nothing ever stored in the cache, will always return
+     * <code>null</code>.
+     *
+     * @param aclObjectIdentity ignored
+     *
+     * @return always <code>null</code>
+     */
+    public BasicAclEntry[] getEntriesFromCache(
+        AclObjectIdentity aclObjectIdentity) {
+        return null;
+    }
+
+    /**
+     * Meets method signature but doesn't store in any cache.
+     *
+     * @param basicAclEntry ignored
+     */
+    public void putEntriesInCache(BasicAclEntry[] basicAclEntry) {}
+}

+ 5 - 0
core/src/main/java/org/acegisecurity/acl/basic/cache/package.html

@@ -0,0 +1,5 @@
+<html>
+<body>
+Caches ACL information for the <code>BasicAclProvider</code>.
+</body>
+</html>

+ 256 - 0
core/src/main/java/org/acegisecurity/acl/basic/jdbc/JdbcDaoImpl.java

@@ -0,0 +1,256 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl.basic.jdbc;
+
+import net.sf.acegisecurity.acl.basic.AclObjectIdentity;
+import net.sf.acegisecurity.acl.basic.BasicAclDao;
+import net.sf.acegisecurity.acl.basic.BasicAclEntry;
+import net.sf.acegisecurity.acl.basic.NamedEntityObjectIdentity;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.context.ApplicationContextException;
+
+import org.springframework.jdbc.core.SqlParameter;
+import org.springframework.jdbc.core.support.JdbcDaoSupport;
+import org.springframework.jdbc.object.MappingSqlQuery;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Types;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Vector;
+
+import javax.sql.DataSource;
+
+
+/**
+ * <p>
+ * Retrieves ACL details from a JDBC location.
+ * </p>
+ * 
+ * <p>
+ * A default database structure is assumed (see {@link
+ * #DEF_ACLS_BY_OBJECT_IDENTITY_QUERY}). This may be overridden by setting the
+ * default query strings to use. If this does not provide enough flexibility,
+ * another strategy would be to subclass this class and override the {@link
+ * MappingSqlQuery} instance used, via the {@link #initMappingSqlQueries()}
+ * extension point.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class JdbcDaoImpl extends JdbcDaoSupport implements BasicAclDao {
+    //~ Static fields/initializers =============================================
+
+    public static final String DEF_ACLS_BY_OBJECT_IDENTITY_QUERY = "SELECT OBJECT_IDENTITY, RECIPIENT, PARENT_OBJECT_IDENTITY, MASK, ACL_CLASS FROM acls WHERE object_identity = ?";
+    private static final Log logger = LogFactory.getLog(JdbcDaoSupport.class);
+
+    //~ Instance fields ========================================================
+
+    private MappingSqlQuery aclsByObjectIdentity;
+    private String aclsByObjectIdentityQuery;
+
+    //~ Constructors ===========================================================
+
+    public JdbcDaoImpl() {
+        aclsByObjectIdentityQuery = DEF_ACLS_BY_OBJECT_IDENTITY_QUERY;
+    }
+
+    //~ Methods ================================================================
+
+    /**
+     * Returns the ACLs associated with the requested
+     * <code>AclObjectIdentity</code>.
+     * 
+     * <P>
+     * The {@link BasicAclEntry}s returned by this method will have
+     * <code>String</code>-based recipients. This will not be a problem if you
+     * are using the <code>GrantedAuthorityEffectiveAclsResolver</code>, which
+     * is the default configured against <code>BasicAclProvider</code>.
+     * </p>
+     * 
+     * <P>
+     * This method will only return ACLs for requests where the
+     * <code>AclObjectIdentity</code> is of type {@link
+     * NamedEntityObjectIdentity}. Of course, you can subclass or replace this
+     * class and support your own custom <code>AclObjectIdentity</code> types.
+     * </p>
+     *
+     * @param aclObjectIdentity for which ACL information is required (cannot
+     *        be <code>null</code> and must be an instance of
+     *        <code>NamedEntityObjectIdentity</code>)
+     *
+     * @return the ACLs that apply (without any <code>null</code>s inside the
+     *         array), or <code>null</code> if not found or if an incompatible
+     *         <code>AclObjectIdentity</code> was requested
+     */
+    public BasicAclEntry[] getAcls(AclObjectIdentity aclObjectIdentity) {
+        // Ensure we can process this type of AclObjectIdentity
+        if (!(aclObjectIdentity instanceof NamedEntityObjectIdentity)) {
+            return null;
+        }
+
+        NamedEntityObjectIdentity neoi = (NamedEntityObjectIdentity) aclObjectIdentity;
+
+        // Compose the String we expect to find in the RDBMS
+        String aclObjectIdentityString = neoi.getClassname() + ":"
+            + neoi.getId();
+
+        // Lookup the BasicAclEntrys from RDBMS (may include null responses)
+        List acls = aclsByObjectIdentity.execute(aclObjectIdentityString);
+
+        // Now prune list of null responses (to meet interface contract)
+        List toReturnAcls = new Vector();
+        Iterator iter = acls.iterator();
+
+        while (iter.hasNext()) {
+            Object object = iter.next();
+
+            if (object != null) {
+                toReturnAcls.add(object);
+            }
+        }
+
+        // Return null if nothing of use found (to meet interface contract)
+        if (toReturnAcls.size() > 0) {
+            return (BasicAclEntry[]) toReturnAcls.toArray(new BasicAclEntry[] {});
+        } else {
+            return null;
+        }
+    }
+
+    public void setAclsByObjectIdentity(
+        MappingSqlQuery aclsByObjectIdentityQuery) {
+        this.aclsByObjectIdentity = aclsByObjectIdentityQuery;
+    }
+
+    public MappingSqlQuery getAclsByObjectIdentity() {
+        return aclsByObjectIdentity;
+    }
+
+    /**
+     * Allows the default query string used to retrieve ACLs based on object
+     * identity to be overriden, if default table or column names need to be
+     * changed. The default query is {@link
+     * #DEF_ACLS_BY_OBJECT_IDENTITY_QUERY}; when modifying this query, ensure
+     * that all returned columns are mapped back to the same column names as
+     * in the default query.
+     *
+     * @param queryString The query string to set
+     */
+    public void setAclsByObjectIdentityQuery(String queryString) {
+        aclsByObjectIdentityQuery = queryString;
+    }
+
+    public String getAclsByObjectIdentityQuery() {
+        return aclsByObjectIdentityQuery;
+    }
+
+    protected void initDao() throws ApplicationContextException {
+        initMappingSqlQueries();
+    }
+
+    /**
+     * Extension point to allow other MappingSqlQuery objects to be substituted
+     * in a subclass
+     */
+    protected void initMappingSqlQueries() {
+        setAclsByObjectIdentity(new AclsByObjectIdentityMapping(getDataSource()));
+    }
+
+    //~ Inner Classes ==========================================================
+
+    /**
+     * Query object to look up ACL entries.
+     * 
+     * <P>
+     * The executed SQL requires the following information be made available
+     * from the indicated placeholders: 1. OBJECT_IDENTITY, 2. RECIPIENT, 3.
+     * PARENT_OBJECT_IDENTITY, 4. MASK, and 5. ACL_CLASS
+     * </p>
+     */
+    protected class AclsByObjectIdentityMapping extends MappingSqlQuery {
+        protected AclsByObjectIdentityMapping(DataSource ds) {
+            super(ds, aclsByObjectIdentityQuery);
+            declareParameter(new SqlParameter(Types.VARCHAR));
+            compile();
+        }
+
+        protected Object mapRow(ResultSet rs, int rownum)
+            throws SQLException {
+            String objectIdentity = rs.getString(1);
+            String recipient = rs.getString(2);
+            String parentObjectIdentity = rs.getString(3);
+            int mask = rs.getInt(4);
+            String aclClass = rs.getString(5);
+
+            // Try to create the indicated BasicAclEntry class
+            BasicAclEntry entry;
+
+            try {
+                Class aclClazz = this.getClass().getClassLoader().loadClass(aclClass);
+                entry = (BasicAclEntry) aclClazz.newInstance();
+            } catch (ClassNotFoundException cnf) {
+                logger.error(cnf);
+
+                return null;
+            } catch (InstantiationException ie) {
+                logger.error(ie);
+
+                return null;
+            } catch (IllegalAccessException iae) {
+                logger.error(iae);
+
+                return null;
+            }
+
+            // Now set each of the ACL's properties
+            entry.setAclObjectIdentity(buildIdentity(objectIdentity));
+            entry.setAclObjectParentIdentity(buildIdentity(parentObjectIdentity));
+            entry.setRecipient(recipient);
+            entry.setMask(mask);
+
+            if ((entry.getRecipient() == null)
+                || (entry.getAclObjectIdentity() == null)) {
+                // Problem with retrieval of ACL 
+                // (shouldn't happen if DB schema defined NOT NULL columns)
+                logger.error("recipient or aclObjectIdentity is null");
+
+                return null;
+            }
+
+            return entry;
+        }
+
+        private AclObjectIdentity buildIdentity(String identity) {
+            if (identity == null) {
+                // Must be an empty parent, so return null
+                return null;
+            }
+
+            int delim = identity.lastIndexOf(":");
+            String classname = identity.substring(0, delim);
+            String id = identity.substring(delim + 1);
+
+            return new NamedEntityObjectIdentity(classname, id);
+        }
+    }
+}

+ 5 - 0
core/src/main/java/org/acegisecurity/acl/basic/jdbc/package.html

@@ -0,0 +1,5 @@
+<html>
+<body>
+JDBC-based data access object for ACL information.
+</body>
+</html>

+ 5 - 0
core/src/main/java/org/acegisecurity/acl/basic/package.html

@@ -0,0 +1,5 @@
+<html>
+<body>
+Access control list implementation based on integer bit masks.
+</body>
+</html>

+ 15 - 0
core/src/main/java/org/acegisecurity/acl/package.html

@@ -0,0 +1,15 @@
+<html>
+<body>
+Enables retrieval of access control lists (ACLs) for domain object instances.
+
+<P>The goal of this package is to locate the <code>AclEntry</code>s
+that apply to a given domain object instance.
+</P>
+
+<P>
+An <code>AclManager</code> has ultimate resposibility for obtaining the
+<code>AclEntry</code>s instances, with a provider-based implementation
+available via the <code>AclProviderManager</code> class (and
+its <code>AclProvider</code> interface.</P> 
+</body>
+</html>

+ 207 - 0
core/src/test/java/org/acegisecurity/acl/AclProviderManagerTests.java

@@ -0,0 +1,207 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.GrantedAuthority;
+import net.sf.acegisecurity.GrantedAuthorityImpl;
+import net.sf.acegisecurity.acl.basic.NamedEntityObjectIdentity;
+import net.sf.acegisecurity.acl.basic.SimpleAclEntry;
+import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken;
+
+import java.util.List;
+import java.util.Vector;
+
+
+/**
+ * Tests {@link AclProviderManager}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class AclProviderManagerTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public AclProviderManagerTests() {
+        super();
+    }
+
+    public AclProviderManagerTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(AclProviderManagerTests.class);
+    }
+
+    public void testAclLookupFails() {
+        AclProviderManager mgr = makeProviderManager();
+        assertNull(mgr.getAcls(new Integer(5)));
+    }
+
+    public void testAclLookupForGivenAuthenticationSuccess() {
+        AclProviderManager mgr = makeProviderManager();
+        assertNotNull(mgr.getAcls("STRING",
+                new UsernamePasswordAuthenticationToken("marissa", "not used")));
+    }
+
+    public void testAclLookupSuccess() {
+        AclProviderManager mgr = makeProviderManager();
+        assertNotNull(mgr.getAcls("STRING"));
+    }
+
+    public void testRejectsNulls() {
+        AclProviderManager mgr = new AclProviderManager();
+
+        try {
+            mgr.getAcls(null);
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+
+        try {
+            mgr.getAcls(null,
+                new UsernamePasswordAuthenticationToken("marissa", "not used"));
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+
+        try {
+            mgr.getAcls("SOME_DOMAIN_INSTANCE", null);
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testReturnsNullIfNoSupportingProvider() {
+        AclProviderManager mgr = makeProviderManager();
+        assertNull(mgr.getAcls(new Integer(4),
+                new UsernamePasswordAuthenticationToken("marissa", "not used")));
+        assertNull(mgr.getAcls(new Integer(4)));
+    }
+
+    public void testStartupFailsIfProviderListNotContainingProviders()
+        throws Exception {
+        List providers = new Vector();
+        providers.add("THIS_IS_NOT_A_PROVIDER");
+
+        AclProviderManager mgr = new AclProviderManager();
+
+        try {
+            mgr.setProviders(providers);
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testStartupFailsIfProviderListNotSet()
+        throws Exception {
+        AclProviderManager mgr = new AclProviderManager();
+
+        try {
+            mgr.afterPropertiesSet();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testStartupFailsIfProviderListNull() throws Exception {
+        AclProviderManager mgr = new AclProviderManager();
+
+        try {
+            mgr.setProviders(null);
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testSuccessfulStartup() throws Exception {
+        AclProviderManager mgr = makeProviderManager();
+        mgr.afterPropertiesSet();
+        assertTrue(true);
+        assertEquals(1, mgr.getProviders().size());
+    }
+
+    private AclProviderManager makeProviderManager() {
+        MockProvider provider1 = new MockProvider();
+        List providers = new Vector();
+        providers.add(provider1);
+
+        AclProviderManager mgr = new AclProviderManager();
+        mgr.setProviders(providers);
+
+        return mgr;
+    }
+
+    //~ Inner Classes ==========================================================
+
+    private class MockProvider implements AclProvider {
+        private UsernamePasswordAuthenticationToken marissa = new UsernamePasswordAuthenticationToken("marissa",
+                "not used",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_FOO"), new GrantedAuthorityImpl("ROLE_BAR")});
+        private SimpleAclEntry entry100Marissa = new SimpleAclEntry(marissa
+                .getPrincipal(),
+                new NamedEntityObjectIdentity("OBJECT", "100"), null, 2);
+        private UsernamePasswordAuthenticationToken scott = new UsernamePasswordAuthenticationToken("scott",
+                "not used",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_FOO"), new GrantedAuthorityImpl("ROLE_MANAGER")});
+        private SimpleAclEntry entry100Scott = new SimpleAclEntry(scott
+                .getPrincipal(),
+                new NamedEntityObjectIdentity("OBJECT", "100"), null, 4);
+
+        public AclEntry[] getAcls(Object domainInstance,
+            Authentication authentication) {
+            if (authentication.getPrincipal().equals(scott.getPrincipal())) {
+                return new AclEntry[] {entry100Scott};
+            }
+
+            if (authentication.getPrincipal().equals(marissa.getPrincipal())) {
+                return new AclEntry[] {entry100Marissa};
+            }
+
+            return null;
+        }
+
+        public AclEntry[] getAcls(Object domainInstance) {
+            return new AclEntry[] {entry100Marissa, entry100Scott};
+        }
+
+        /**
+         * Only supports <code>Object</code>s of type <code>String</code>
+         *
+         * @param domainInstance DOCUMENT ME!
+         *
+         * @return DOCUMENT ME!
+         */
+        public boolean supports(Object domainInstance) {
+            return (domainInstance instanceof String);
+        }
+    }
+}

+ 365 - 0
core/src/test/java/org/acegisecurity/acl/basic/BasicAclProviderTests.java

@@ -0,0 +1,365 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl.basic;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.PopulatedDatabase;
+import net.sf.acegisecurity.acl.AclEntry;
+import net.sf.acegisecurity.acl.basic.cache.BasicAclEntryHolder;
+import net.sf.acegisecurity.acl.basic.cache.NullAclEntryCache;
+import net.sf.acegisecurity.acl.basic.jdbc.JdbcDaoImpl;
+import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken;
+
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * Tests {@link BasicAclProvider}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class BasicAclProviderTests extends TestCase {
+    //~ Static fields/initializers =============================================
+
+    public static final String OBJECT_IDENTITY = "net.sf.acegisecurity.acl.DomainObject";
+
+    //~ Constructors ===========================================================
+
+    public BasicAclProviderTests() {
+        super();
+    }
+
+    public BasicAclProviderTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(BasicAclProviderTests.class);
+    }
+
+    public void testCachingUsedProperly() throws Exception {
+        BasicAclProvider provider = new BasicAclProvider();
+        provider.setBasicAclDao(makePopulatedJdbcDao());
+
+        MockCache cache = new MockCache();
+        provider.setBasicAclEntryCache(cache);
+
+        assertEquals(0, cache.getGets());
+        assertEquals(0, cache.getGetsHits());
+        assertEquals(0, cache.getPuts());
+        assertEquals(0, cache.getBackingMap().size());
+
+        Object object = new MockDomain(1); // has no parents
+        provider.getAcls(object);
+
+        assertEquals(1, cache.getGets());
+        assertEquals(0, cache.getGetsHits());
+        assertEquals(1, cache.getPuts());
+        assertEquals(1, cache.getBackingMap().size());
+
+        provider.getAcls(object);
+
+        assertEquals(2, cache.getGets());
+        assertEquals(1, cache.getGetsHits());
+        assertEquals(1, cache.getPuts());
+        assertEquals(1, cache.getBackingMap().size());
+
+        object = new MockDomain(1000); // does not exist
+
+        provider.getAcls(object);
+
+        assertEquals(3, cache.getGets());
+        assertEquals(1, cache.getGetsHits());
+        assertEquals(2, cache.getPuts());
+        assertEquals(2, cache.getBackingMap().size());
+
+        provider.getAcls(object);
+
+        assertEquals(4, cache.getGets());
+        assertEquals(2, cache.getGetsHits());
+        assertEquals(2, cache.getPuts());
+        assertEquals(2, cache.getBackingMap().size());
+
+        provider.getAcls(object);
+
+        assertEquals(5, cache.getGets());
+        assertEquals(3, cache.getGetsHits());
+        assertEquals(2, cache.getPuts());
+        assertEquals(2, cache.getBackingMap().size());
+    }
+
+    public void testExceptionThrownIfUnsupportedObjectIsSubmitted()
+        throws Exception {
+        BasicAclProvider provider = new BasicAclProvider();
+        provider.setBasicAclDao(makePopulatedJdbcDao());
+
+        // this one should NOT be supported, as it has no getId() method
+        assertFalse(provider.supports(new Integer(34)));
+
+        // try anyway
+        try {
+            provider.getAcls(new Integer(34));
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testGetAclsForInstanceNotFound() throws Exception {
+        BasicAclProvider provider = new BasicAclProvider();
+        provider.setBasicAclDao(makePopulatedJdbcDao());
+
+        Object object = new MockDomain(546464646);
+        AclEntry[] acls = provider.getAcls(object);
+        assertNull(acls);
+    }
+
+    public void testGetAclsForInstanceWithParentLevels()
+        throws Exception {
+        BasicAclProvider provider = new BasicAclProvider();
+        provider.setBasicAclDao(makePopulatedJdbcDao());
+
+        Object object = new MockDomain(6);
+        AclEntry[] acls = provider.getAcls(object);
+        assertEquals(2, acls.length);
+
+        assertEquals("scott", ((BasicAclEntry) acls[0]).getRecipient());
+        assertEquals("ROLE_SUPERVISOR", ((BasicAclEntry) acls[1]).getRecipient());
+    }
+
+    public void testGetAclsForInstanceWithoutParent() throws Exception {
+        BasicAclProvider provider = new BasicAclProvider();
+        provider.setBasicAclDao(makePopulatedJdbcDao());
+
+        Object object = new MockDomain(7);
+        AclEntry[] acls = provider.getAcls(object);
+        assertEquals(1, acls.length);
+    }
+
+    public void testGetAclsWithAuthentication() throws Exception {
+        BasicAclProvider provider = new BasicAclProvider();
+        provider.setBasicAclDao(makePopulatedJdbcDao());
+
+        Authentication scott = new UsernamePasswordAuthenticationToken("scott",
+                "unused");
+
+        Object object = new MockDomain(6);
+        AclEntry[] acls = provider.getAcls(object, scott);
+
+        assertEquals(1, acls.length);
+        assertEquals("scott", ((BasicAclEntry) acls[0]).getRecipient());
+    }
+
+    public void testGettersSetters() {
+        BasicAclProvider provider = new BasicAclProvider();
+        assertEquals(NullAclEntryCache.class,
+            provider.getBasicAclEntryCache().getClass());
+        assertEquals(NamedEntityObjectIdentity.class,
+            provider.getDefaultAclObjectIdentityClass());
+        assertEquals(GrantedAuthorityEffectiveAclsResolver.class,
+            provider.getEffectiveAclsResolver().getClass());
+
+        provider.setBasicAclEntryCache(null);
+        assertNull(provider.getBasicAclEntryCache());
+
+        provider.setDefaultAclObjectIdentityClass(null);
+        assertNull(provider.getDefaultAclObjectIdentityClass());
+
+        provider.setEffectiveAclsResolver(null);
+        assertNull(provider.getEffectiveAclsResolver());
+
+        provider.setBasicAclDao(new MockDao());
+        assertNotNull(provider.getBasicAclDao());
+    }
+
+    public void testStartupFailsIfNullAclDao() throws Exception {
+        BasicAclProvider provider = new BasicAclProvider();
+
+        try {
+            provider.afterPropertiesSet();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testStartupFailsIfNullEffectiveAclsResolver()
+        throws Exception {
+        BasicAclProvider provider = new BasicAclProvider();
+        provider.setBasicAclDao(makePopulatedJdbcDao());
+
+        provider.setEffectiveAclsResolver(null);
+
+        try {
+            provider.afterPropertiesSet();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testStartupFailsIfNullEntryCache() throws Exception {
+        BasicAclProvider provider = new BasicAclProvider();
+        provider.setBasicAclDao(makePopulatedJdbcDao());
+
+        provider.setBasicAclEntryCache(null);
+
+        try {
+            provider.afterPropertiesSet();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testStartupFailsIfProblemWithAclObjectIdentityClass()
+        throws Exception {
+        BasicAclProvider provider = new BasicAclProvider();
+        provider.setBasicAclDao(makePopulatedJdbcDao());
+
+        // check nulls rejected
+        provider.setDefaultAclObjectIdentityClass(null);
+
+        try {
+            provider.afterPropertiesSet();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+
+        // check non-AclObjectIdentity classes are also rejected
+        provider.setDefaultAclObjectIdentityClass(String.class);
+
+        try {
+            provider.afterPropertiesSet();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+
+        // check AclObjectIdentity class without constructor accepting a
+        // domain object is also rejected
+        provider.setDefaultAclObjectIdentityClass(MockAclObjectIdentity.class);
+
+        try {
+            provider.afterPropertiesSet();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertEquals("defaultAclObjectIdentityClass must provide a constructor that accepts the domain object instance!",
+                expected.getMessage());
+        }
+    }
+
+    public void testSupports() throws Exception {
+        BasicAclProvider provider = new BasicAclProvider();
+        provider.setBasicAclDao(makePopulatedJdbcDao());
+
+        // this one should NOT be supported, as it has no getId() method
+        assertFalse(provider.supports(new Integer(34)));
+
+        // this one SHOULD be supported, as it has a getId() method
+        assertTrue(provider.supports(new SomeDomain()));
+
+        // this one SHOULD be supported, as it implements AclObjectIdentityAware
+        assertTrue(provider.supports(new MockDomain(4)));
+    }
+
+    private JdbcDaoImpl makePopulatedJdbcDao() throws Exception {
+        JdbcDaoImpl dao = new JdbcDaoImpl();
+        dao.setDataSource(PopulatedDatabase.getDataSource());
+        dao.afterPropertiesSet();
+
+        return dao;
+    }
+
+    //~ Inner Classes ==========================================================
+
+    private class MockCache implements BasicAclEntryCache {
+        private Map map = new HashMap();
+        private int gets = 0;
+        private int getsHits = 0;
+        private int puts = 0;
+
+        public Map getBackingMap() {
+            return map;
+        }
+
+        public BasicAclEntry[] getEntriesFromCache(
+            AclObjectIdentity aclObjectIdentity) {
+            gets++;
+
+            Object result = map.get(aclObjectIdentity);
+
+            if (result == null) {
+                return null;
+            }
+
+            getsHits++;
+
+            BasicAclEntryHolder holder = (BasicAclEntryHolder) result;
+
+            return holder.getBasicAclEntries();
+        }
+
+        public int getGets() {
+            return gets;
+        }
+
+        public int getGetsHits() {
+            return getsHits;
+        }
+
+        public int getPuts() {
+            return puts;
+        }
+
+        public void putEntriesInCache(BasicAclEntry[] basicAclEntry) {
+            puts++;
+
+            BasicAclEntryHolder holder = new BasicAclEntryHolder(basicAclEntry);
+            map.put(basicAclEntry[0].getAclObjectIdentity(), holder);
+        }
+    }
+
+    private class MockDao implements BasicAclDao {
+        public BasicAclEntry[] getAcls(AclObjectIdentity aclObjectIdentity) {
+            return null;
+        }
+    }
+
+    private class MockDomain implements AclObjectIdentityAware {
+        private int id;
+
+        public MockDomain(int id) {
+            this.id = id;
+        }
+
+        public AclObjectIdentity getAclObjectIdentity() {
+            return new NamedEntityObjectIdentity(OBJECT_IDENTITY,
+                new Integer(id).toString());
+        }
+    }
+}

+ 118 - 0
core/src/test/java/org/acegisecurity/acl/basic/GrantedAuthorityEffectiveAclsResolverTests.java

@@ -0,0 +1,118 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl.basic;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.GrantedAuthority;
+import net.sf.acegisecurity.GrantedAuthorityImpl;
+import net.sf.acegisecurity.acl.AclEntry;
+import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken;
+
+
+/**
+ * Tests {@link GrantedAuthorityEffectiveAclsResolver}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class GrantedAuthorityEffectiveAclsResolverTests extends TestCase {
+    //~ Instance fields ========================================================
+
+    private SimpleAclEntry entry100RoleEverybody = new SimpleAclEntry("ROLE_EVERYBODY",
+            new NamedEntityObjectIdentity("OBJECT", "100"), null, 14);
+    private SimpleAclEntry entry100RoleOne = new SimpleAclEntry("ROLE_ONE",
+            new NamedEntityObjectIdentity("OBJECT", "100"), null, 0);
+    private SimpleAclEntry entry100RoleTwo = new SimpleAclEntry("ROLE_TWO",
+            new NamedEntityObjectIdentity("OBJECT", "100"), null, 2);
+    private UsernamePasswordAuthenticationToken scott = new UsernamePasswordAuthenticationToken("scott",
+            "not used",
+            new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_EVERYBODY"), new GrantedAuthorityImpl(
+                    "ROLE_TWO")});
+    private SimpleAclEntry entry100Scott = new SimpleAclEntry(scott
+            .getPrincipal(), new NamedEntityObjectIdentity("OBJECT", "100"),
+            null, 4);
+    private UsernamePasswordAuthenticationToken dianne = new UsernamePasswordAuthenticationToken("dianne",
+            "not used");
+    private UsernamePasswordAuthenticationToken marissa = new UsernamePasswordAuthenticationToken("marissa",
+            "not used",
+            new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_EVERYBODY"), new GrantedAuthorityImpl("ROLE_ONE")});
+    private SimpleAclEntry entry100Marissa = new SimpleAclEntry(marissa
+            .getPrincipal(), new NamedEntityObjectIdentity("OBJECT", "100"),
+            null, 2);
+
+    // convenience group
+    private SimpleAclEntry[] acls = {entry100Marissa, entry100Scott, entry100RoleEverybody, entry100RoleOne, entry100RoleTwo};
+
+    //~ Constructors ===========================================================
+
+    public GrantedAuthorityEffectiveAclsResolverTests() {
+        super();
+    }
+
+    public GrantedAuthorityEffectiveAclsResolverTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(GrantedAuthorityEffectiveAclsResolverTests.class);
+    }
+
+    public void testResolveAclsForDianneWhoHasANullForAuthorities() {
+        GrantedAuthorityEffectiveAclsResolver resolver = new GrantedAuthorityEffectiveAclsResolver();
+        assertNull(resolver.resolveEffectiveAcls(acls, dianne));
+    }
+
+    public void testResolveAclsForMarissa() {
+        GrantedAuthorityEffectiveAclsResolver resolver = new GrantedAuthorityEffectiveAclsResolver();
+        assertEquals(3, resolver.resolveEffectiveAcls(acls, marissa).length);
+        assertEquals(entry100Marissa,
+            resolver.resolveEffectiveAcls(acls, marissa)[0]);
+        assertEquals(entry100RoleEverybody,
+            resolver.resolveEffectiveAcls(acls, marissa)[1]);
+        assertEquals(entry100RoleOne,
+            resolver.resolveEffectiveAcls(acls, marissa)[2]);
+    }
+
+    public void testResolveAclsForScott() {
+        GrantedAuthorityEffectiveAclsResolver resolver = new GrantedAuthorityEffectiveAclsResolver();
+        assertEquals(3, resolver.resolveEffectiveAcls(acls, scott).length);
+        assertEquals(entry100Scott,
+            resolver.resolveEffectiveAcls(acls, scott)[0]);
+        assertEquals(entry100RoleEverybody,
+            resolver.resolveEffectiveAcls(acls, scott)[1]);
+        assertEquals(entry100RoleTwo,
+            resolver.resolveEffectiveAcls(acls, scott)[2]);
+    }
+
+    public void testSkipsNonBasicAclEntryObjects() {
+        GrantedAuthorityEffectiveAclsResolver resolver = new GrantedAuthorityEffectiveAclsResolver();
+        AclEntry[] basicAcls = {entry100Marissa, entry100Scott, entry100RoleEverybody, entry100RoleOne, new MockAcl(), entry100RoleTwo};
+        assertEquals(3, resolver.resolveEffectiveAcls(basicAcls, marissa).length);
+    }
+
+    //~ Inner Classes ==========================================================
+
+    private class MockAcl implements AclEntry {
+        // does nothing
+    }
+}

+ 28 - 0
core/src/test/java/org/acegisecurity/acl/basic/MockAclObjectIdentity.java

@@ -0,0 +1,28 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl.basic;
+
+/**
+ * Implements <code>AclObjectIdentity</code> but is incompatible with
+ * <code>BasicAclProvider</code> because it cannot be constructed by passing
+ * in a domain object instance.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class MockAclObjectIdentity implements AclObjectIdentity {
+    // has no "public MockAclObjectIdentity(Object object)" constructor!
+}

+ 135 - 0
core/src/test/java/org/acegisecurity/acl/basic/NamedEntityObjectIdentityTests.java

@@ -0,0 +1,135 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl.basic;
+
+import junit.framework.TestCase;
+
+
+/**
+ * Tests {@link NamedEntityObjectIdentity}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class NamedEntityObjectIdentityTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public NamedEntityObjectIdentityTests() {
+        super();
+    }
+
+    public NamedEntityObjectIdentityTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(NamedEntityObjectIdentityTests.class);
+    }
+
+    public void testConstructionViaReflection() throws Exception {
+        SomeDomain domainObject = new SomeDomain();
+        domainObject.setId(34);
+
+        NamedEntityObjectIdentity name = new NamedEntityObjectIdentity(domainObject);
+        assertEquals("34", name.getId());
+        assertEquals(domainObject.getClass().getName(), name.getClassname());
+        name.toString();
+    }
+
+    public void testConstructionViaReflectionFailsIfNoGetIdMethod()
+        throws Exception {
+        try {
+            new NamedEntityObjectIdentity(new Integer(45));
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testConstructionViaReflectionFailsIfNullPassed()
+        throws Exception {
+        try {
+            new NamedEntityObjectIdentity(null);
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testDefaultConstructorRejected() {
+        try {
+            new NamedEntityObjectIdentity();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testEquality() {
+        NamedEntityObjectIdentity original = new NamedEntityObjectIdentity("foo",
+                "12");
+        assertFalse(original.equals(null));
+        assertFalse(original.equals(new Integer(354)));
+        assertFalse(original.equals(
+                new NamedEntityObjectIdentity("foo", "23232")));
+        assertTrue(original.equals(new NamedEntityObjectIdentity("foo", "12")));
+        assertTrue(original.equals(original));
+    }
+
+    public void testNormalConstructionRejectedIfInvalidArguments()
+        throws Exception {
+        try {
+            new NamedEntityObjectIdentity(null, "12");
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+
+        try {
+            new NamedEntityObjectIdentity("classname", null);
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+
+        try {
+            new NamedEntityObjectIdentity("", "12");
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+
+        try {
+            new NamedEntityObjectIdentity("classname", "");
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testNormalOperation() {
+        NamedEntityObjectIdentity name = new NamedEntityObjectIdentity("domain",
+                "id");
+        assertEquals("domain", name.getClassname());
+        assertEquals("id", name.getId());
+    }
+}

+ 183 - 0
core/src/test/java/org/acegisecurity/acl/basic/SimpleAclEntryTests.java

@@ -0,0 +1,183 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl.basic;
+
+import junit.framework.TestCase;
+
+
+/**
+ * Tests {@link SimpleAclEntry}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class SimpleAclEntryTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public SimpleAclEntryTests() {
+        super();
+    }
+
+    public SimpleAclEntryTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(SimpleAclEntryTests.class);
+    }
+
+    public void testCorrectOperation() {
+        String recipient = "marissa";
+        AclObjectIdentity objectIdentity = new NamedEntityObjectIdentity("domain",
+                "12");
+        SimpleAclEntry acl = new SimpleAclEntry(recipient, objectIdentity,
+                null, 0);
+
+        assertFalse(acl.isPermitted(SimpleAclEntry.ADMINISTRATION));
+        acl.addPermission(SimpleAclEntry.ADMINISTRATION);
+        assertTrue(acl.isPermitted(SimpleAclEntry.ADMINISTRATION));
+        assertFalse(acl.isPermitted(SimpleAclEntry.CREATE));
+        assertFalse(acl.isPermitted(SimpleAclEntry.DELETE));
+        assertFalse(acl.isPermitted(SimpleAclEntry.READ));
+        assertFalse(acl.isPermitted(SimpleAclEntry.WRITE));
+        assertEquals("A----", acl.printPermissionsBlock());
+        acl.deletePermission(SimpleAclEntry.ADMINISTRATION);
+        assertFalse(acl.isPermitted(SimpleAclEntry.ADMINISTRATION));
+        assertEquals("-----", acl.printPermissionsBlock());
+
+        acl.addPermissions(new int[] {SimpleAclEntry.READ, SimpleAclEntry.WRITE});
+        acl.addPermission(SimpleAclEntry.CREATE);
+        assertFalse(acl.isPermitted(SimpleAclEntry.ADMINISTRATION));
+        assertTrue(acl.isPermitted(SimpleAclEntry.CREATE));
+        assertFalse(acl.isPermitted(SimpleAclEntry.DELETE));
+        assertTrue(acl.isPermitted(SimpleAclEntry.READ));
+        assertTrue(acl.isPermitted(SimpleAclEntry.WRITE));
+        assertEquals("-RWC-", acl.printPermissionsBlock());
+
+        acl.deletePermission(SimpleAclEntry.CREATE);
+        acl.deletePermissions(new int[] {SimpleAclEntry.READ, SimpleAclEntry.WRITE});
+        assertEquals("-----", acl.printPermissionsBlock());
+
+        acl.togglePermission(SimpleAclEntry.CREATE);
+        assertTrue(acl.isPermitted(SimpleAclEntry.CREATE));
+        assertFalse(acl.isPermitted(SimpleAclEntry.ADMINISTRATION));
+        acl.togglePermission(SimpleAclEntry.CREATE);
+        assertFalse(acl.isPermitted(SimpleAclEntry.CREATE));
+    }
+
+    public void testDetectsNullOnMainConstructor() {
+        String recipient = "marissa";
+        AclObjectIdentity objectIdentity = new NamedEntityObjectIdentity("domain",
+                "12");
+
+        try {
+            new SimpleAclEntry(recipient, null, null, 2);
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+
+        try {
+            new SimpleAclEntry(null, objectIdentity, null, 2);
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testGettersSetters() {
+        SimpleAclEntry acl = new SimpleAclEntry();
+
+        AclObjectIdentity objectIdentity = new NamedEntityObjectIdentity("domain",
+                "693");
+        acl.setAclObjectIdentity(objectIdentity);
+        assertEquals(objectIdentity, acl.getAclObjectIdentity());
+
+        AclObjectIdentity parentObjectIdentity = new NamedEntityObjectIdentity("domain",
+                "13");
+        acl.setAclObjectParentIdentity(parentObjectIdentity);
+        assertEquals(parentObjectIdentity, acl.getAclObjectParentIdentity());
+
+        acl.setMask(2);
+        assertEquals(2, acl.getMask());
+
+        acl.setRecipient("scott");
+        assertEquals("scott", acl.getRecipient());
+    }
+
+    public void testRejectsInvalidMasksInAddMethod() {
+        String recipient = "marissa";
+        AclObjectIdentity objectIdentity = new NamedEntityObjectIdentity("domain",
+                "12");
+        SimpleAclEntry acl = new SimpleAclEntry(recipient, objectIdentity,
+                null, 4);
+
+        try {
+            acl.addPermission(Integer.MAX_VALUE);
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testRejectsInvalidMasksInDeleteMethod() {
+        String recipient = "marissa";
+        AclObjectIdentity objectIdentity = new NamedEntityObjectIdentity("domain",
+                "12");
+        SimpleAclEntry acl = new SimpleAclEntry(recipient, objectIdentity,
+                null, 0);
+        acl.addPermissions(new int[] {SimpleAclEntry.READ, SimpleAclEntry.WRITE, SimpleAclEntry.CREATE});
+
+        try {
+            acl.deletePermission(SimpleAclEntry.READ); // can't write if we can't read
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testRejectsInvalidMasksInTogglePermissionMethod() {
+        String recipient = "marissa";
+        AclObjectIdentity objectIdentity = new NamedEntityObjectIdentity("domain",
+                "12");
+        SimpleAclEntry acl = new SimpleAclEntry(recipient, objectIdentity,
+                null, 0);
+        acl.addPermissions(new int[] {SimpleAclEntry.READ, SimpleAclEntry.WRITE, SimpleAclEntry.CREATE});
+
+        try {
+            acl.togglePermission(SimpleAclEntry.READ); // can't write if we can't read
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testToString() {
+        String recipient = "marissa";
+        AclObjectIdentity objectIdentity = new NamedEntityObjectIdentity("domain",
+                "12");
+        SimpleAclEntry acl = new SimpleAclEntry(recipient, objectIdentity,
+                null, 0);
+        acl.addPermissions(new int[] {SimpleAclEntry.READ, SimpleAclEntry.WRITE, SimpleAclEntry.CREATE});
+        assertTrue(acl.toString().endsWith("marissa=-RWC- ............................111. (14)]"));
+    }
+}

+ 38 - 0
core/src/test/java/org/acegisecurity/acl/basic/SomeDomain.java

@@ -0,0 +1,38 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl.basic;
+
+/**
+ * Simple object to use when testing <code>NamedEntityObjectIdentity</code>.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class SomeDomain {
+    //~ Instance fields ========================================================
+
+    private int id;
+
+    //~ Methods ================================================================
+
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    public int getId() {
+        return id;
+    }
+}

+ 66 - 0
core/src/test/java/org/acegisecurity/acl/basic/cache/BasicAclEntryHolderTests.java

@@ -0,0 +1,66 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl.basic.cache;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.acl.basic.BasicAclEntry;
+import net.sf.acegisecurity.acl.basic.SimpleAclEntry;
+
+
+/**
+ * Tests {@link BasicAclEntryHolder}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class BasicAclEntryHolderTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public BasicAclEntryHolderTests() {
+        super();
+    }
+
+    public BasicAclEntryHolderTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(BasicAclEntryHolderTests.class);
+    }
+
+    public void testRejectsNull() throws Exception {
+        try {
+            new BasicAclEntryHolder(null);
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+
+        try {
+            new BasicAclEntryHolder(new BasicAclEntry[] {new SimpleAclEntry(), null, new SimpleAclEntry()});
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+}

+ 95 - 0
core/src/test/java/org/acegisecurity/acl/basic/cache/EhCacheBasedAclEntryCacheTests.java

@@ -0,0 +1,95 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl.basic.cache;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.acl.basic.AclObjectIdentity;
+import net.sf.acegisecurity.acl.basic.BasicAclEntry;
+import net.sf.acegisecurity.acl.basic.NamedEntityObjectIdentity;
+import net.sf.acegisecurity.acl.basic.SimpleAclEntry;
+
+
+/**
+ * Tests {@link EhCacheBasedAclEntryCache}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class EhCacheBasedAclEntryCacheTests extends TestCase {
+    //~ Static fields/initializers =============================================
+
+    private static final AclObjectIdentity OBJECT_100 = new NamedEntityObjectIdentity("OBJECT",
+            "100");
+    private static final AclObjectIdentity OBJECT_200 = new NamedEntityObjectIdentity("OBJECT",
+            "200");
+    private static final BasicAclEntry OBJECT_100_MARISSA = new SimpleAclEntry("marissa",
+            OBJECT_100, null, 2);
+    private static final BasicAclEntry OBJECT_100_SCOTT = new SimpleAclEntry("scott",
+            OBJECT_100, null, 4);
+    private static final BasicAclEntry OBJECT_200_PETER = new SimpleAclEntry("peter",
+            OBJECT_200, null, 4);
+
+    //~ Constructors ===========================================================
+
+    public EhCacheBasedAclEntryCacheTests() {
+        super();
+    }
+
+    public EhCacheBasedAclEntryCacheTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(EhCacheBasedAclEntryCacheTests.class);
+    }
+
+    public void testCacheOperation() throws Exception {
+        EhCacheBasedAclEntryCache cache = new EhCacheBasedAclEntryCache();
+        cache.afterPropertiesSet();
+
+        // execute a second time to test detection of existing instance
+        cache.afterPropertiesSet();
+
+        cache.putEntriesInCache(new BasicAclEntry[] {OBJECT_100_SCOTT, OBJECT_100_MARISSA});
+        cache.putEntriesInCache(new BasicAclEntry[] {OBJECT_200_PETER});
+
+        // Check we can get them from cache again
+        assertEquals(OBJECT_100_SCOTT,
+            cache.getEntriesFromCache(
+                new NamedEntityObjectIdentity("OBJECT", "100"))[0]);
+        assertEquals(OBJECT_100_MARISSA,
+            cache.getEntriesFromCache(
+                new NamedEntityObjectIdentity("OBJECT", "100"))[1]);
+        assertEquals(OBJECT_200_PETER,
+            cache.getEntriesFromCache(
+                new NamedEntityObjectIdentity("OBJECT", "200"))[0]);
+
+        cache.destroy();
+    }
+
+    public void testGettersSetters() {
+        EhCacheBasedAclEntryCache cache = new EhCacheBasedAclEntryCache();
+        cache.setMinutesToIdle(15);
+        assertEquals(15, cache.getMinutesToIdle());
+    }
+}

+ 58 - 0
core/src/test/java/org/acegisecurity/acl/basic/cache/NullAclEntryCacheTests.java

@@ -0,0 +1,58 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl.basic.cache;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.acl.basic.BasicAclEntry;
+import net.sf.acegisecurity.acl.basic.NamedEntityObjectIdentity;
+import net.sf.acegisecurity.acl.basic.SimpleAclEntry;
+
+
+/**
+ * Tests {@link NullAclEntryCache}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class NullAclEntryCacheTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public NullAclEntryCacheTests() {
+        super();
+    }
+
+    public NullAclEntryCacheTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(NullAclEntryCacheTests.class);
+    }
+
+    public void testCacheOperation() throws Exception {
+        NullAclEntryCache cache = new NullAclEntryCache();
+        cache.putEntriesInCache(new BasicAclEntry[] {new SimpleAclEntry()});
+        cache.getEntriesFromCache(new NamedEntityObjectIdentity("not_used",
+                "not_used"));
+    }
+}

+ 121 - 0
core/src/test/java/org/acegisecurity/acl/basic/jdbc/JdbcDaoImplTests.java

@@ -0,0 +1,121 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.acl.basic.jdbc;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.PopulatedDatabase;
+import net.sf.acegisecurity.acl.basic.AclObjectIdentity;
+import net.sf.acegisecurity.acl.basic.BasicAclEntry;
+import net.sf.acegisecurity.acl.basic.NamedEntityObjectIdentity;
+
+import org.springframework.jdbc.object.MappingSqlQuery;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+
+/**
+ * Tests {@link JdbcDaoImpl}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class JdbcDaoImplTests extends TestCase {
+    //~ Static fields/initializers =============================================
+
+    public static final String OBJECT_IDENTITY = "net.sf.acegisecurity.acl.DomainObject";
+
+    //~ Constructors ===========================================================
+
+    public JdbcDaoImplTests() {
+        super();
+    }
+
+    public JdbcDaoImplTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(JdbcDaoImplTests.class);
+    }
+
+    public void testGetsAclsWhichExistInDatabase() throws Exception {
+        JdbcDaoImpl dao = makePopulatedJdbcDao();
+        AclObjectIdentity identity = new NamedEntityObjectIdentity(OBJECT_IDENTITY,
+                "2");
+        BasicAclEntry[] acls = dao.getAcls(identity);
+        assertEquals(2, acls.length);
+    }
+
+    public void testGettersSetters() throws Exception {
+        JdbcDaoImpl dao = makePopulatedJdbcDao();
+        dao.setAclsByObjectIdentity(new MockMappingSqlQuery());
+        assertNotNull(dao.getAclsByObjectIdentity());
+
+        dao.setAclsByObjectIdentityQuery("foo");
+        assertEquals("foo", dao.getAclsByObjectIdentityQuery());
+    }
+
+    public void testNullReturnedIfBasicAclEntryClassNotFound()
+        throws Exception {
+        JdbcDaoImpl dao = makePopulatedJdbcDao();
+        AclObjectIdentity identity = new NamedEntityObjectIdentity(OBJECT_IDENTITY,
+                "8");
+        BasicAclEntry[] result = dao.getAcls(identity);
+        assertNull(result);
+    }
+
+    public void testNullReturnedIfEntityNotFound() throws Exception {
+        JdbcDaoImpl dao = makePopulatedJdbcDao();
+        AclObjectIdentity identity = new NamedEntityObjectIdentity(OBJECT_IDENTITY,
+                "NOT_VALID_ID");
+        BasicAclEntry[] result = dao.getAcls(identity);
+        assertNull(result);
+    }
+
+    public void testRejectsNonNamedEntityObjectIdentity()
+        throws Exception {
+        JdbcDaoImpl dao = new JdbcDaoImpl();
+        AclObjectIdentity identity = new AclObjectIdentity() {}
+        ;
+
+        assertNull(dao.getAcls(identity));
+    }
+
+    private JdbcDaoImpl makePopulatedJdbcDao() throws Exception {
+        JdbcDaoImpl dao = new JdbcDaoImpl();
+        dao.setDataSource(PopulatedDatabase.getDataSource());
+        dao.afterPropertiesSet();
+
+        return dao;
+    }
+
+    //~ Inner Classes ==========================================================
+
+    private class MockMappingSqlQuery extends MappingSqlQuery {
+        protected Object mapRow(ResultSet arg0, int arg1)
+            throws SQLException {
+            return null;
+        }
+    }
+}

+ 377 - 3
docs/reference/src/index.xml

@@ -69,7 +69,7 @@
       <sect2 id="security-high-level-design-key-components">
       <sect2 id="security-high-level-design-key-components">
         <title>Key Components</title>
         <title>Key Components</title>
 
 
-        <para>The Acegi Security System for Spring essentially comprises six
+        <para>The Acegi Security System for Spring essentially comprises seven
         key functional parts:</para>
         key functional parts:</para>
 
 
         <itemizedlist spacing="compact">
         <itemizedlist spacing="compact">
@@ -109,6 +109,11 @@
             authentication, authorization, run-as replacement and execution of
             authentication, authorization, run-as replacement and execution of
             a given operation.</para>
             a given operation.</para>
           </listitem>
           </listitem>
+
+          <listitem>
+            <para>An acess control list (ACL) management package, which can be
+            used to obtain ACLs for domain object instances.</para>
+          </listitem>
         </itemizedlist>
         </itemizedlist>
 
 
         <para>Secure objects refer to any type of object that can have
         <para>Secure objects refer to any type of object that can have
@@ -134,8 +139,8 @@
         <literal>FilterInterceptor</literal>) with complete
         <literal>FilterInterceptor</literal>) with complete
         transparency.</para>
         transparency.</para>
 
 
-        <para>Each of the six key parts is discussed in detail throughout this
-        document.</para>
+        <para>Each of the seven key parts is discussed in detail throughout
+        this document.</para>
       </sect2>
       </sect2>
 
 
       <sect2 id="security-high-level-design-supported-secure-objects">
       <sect2 id="security-high-level-design-supported-secure-objects">
@@ -2985,6 +2990,370 @@ $CATALINA_HOME/bin/startup.sh</programlisting></para>
       </sect2>
       </sect2>
     </sect1>
     </sect1>
 
 
+    <sect1 id="acls">
+      <title>Instance-Based Access Control List (ACL) Security</title>
+
+      <sect2 id="acls-overview">
+        <title>Overview</title>
+
+        <para>THIS FEATURE WAS ADDED IN VERSION 0.6. WE WELCOME YOUR COMMENTS
+        AND IMPROVEMENTS.</para>
+
+        <para>Complex applications often will find the need to define access
+        permissions not simply at a web request or method invocation level.
+        Instead, security decisions need to comprise both who
+        (<literal>Authentication</literal>), where
+        (<literal>MethodInvocation</literal>) and what
+        (<literal>SomeDomainObject</literal>). In other words, authorization
+        decisions also need to consider the actual domain object instance
+        subject of a method invocation.</para>
+
+        <para>Imagine you're designing an application for a pet clinic. There
+        will be two main groups of users of your Spring-based application:
+        staff of the pet clinic, as well as the pet clinic's customers. The
+        staff will have access to all of the data, whilst your customers will
+        only be able to see their own customer records. To make it a little
+        more interesting, your customers can allow other users to see their
+        customer records, such as their "puppy preschool "mentor or president
+        of their local "Pony Club". Using Acegi Security System for Spring as
+        the foundation, you have several approaches that can be
+        used:<orderedlist>
+            <listitem>
+              <para>Write your business methods to enforce the security. You
+              could consult a collection within the
+              <literal>Customer</literal> domain object instance to determine
+              which users have access. By using the
+              <literal>ContextHolder.getContext()</literal> and casting it to
+              <literal>SecureContext</literal>, you'll be able to access the
+              <literal>Authentication</literal> object.</para>
+            </listitem>
+
+            <listitem>
+              <para>Write an <literal>AccessDecisionVoter</literal> to enforce
+              the security from the <literal>GrantedAuthority[]</literal>s
+              stored in the <literal>Authentication</literal> object. This
+              would mean your <literal>AuthenticationManager</literal> would
+              need to populate the <literal>Authentication</literal> with
+              custom <literal>GrantedAuthority</literal>[]s representing each
+              of the <literal>Customer</literal> domain object instances the
+              principal has access to.</para>
+            </listitem>
+
+            <listitem>
+              <para>Write an <literal>AccessDecisionVoter</literal> to enforce
+              the security and open the target <literal>Customer</literal>
+              domain object directly. This would mean your voter needs access
+              to a DAO that allows it to retrieve the
+              <literal>Customer</literal> object. It would then access the
+              <literal>Customer</literal> object's collection of approved
+              users and make the appropriate decision.</para>
+            </listitem>
+          </orderedlist></para>
+
+        <para>Each one of these approaches is perfectly legitimate. However,
+        the first couples your authorization checking to your business code.
+        The main problems with this include the enhanced difficulty of unit
+        testing and the fact it would be more difficult to reuse the
+        <literal>Customer</literal> authorization logic elsewhere. Obtaining
+        the <literal>GrantedAuthority[]</literal>s from the
+        <literal>Authentication</literal> object is also fine, but will not
+        scale to large numbers of <literal>Customer</literal>s. If a user
+        might be able to access 5,000 <literal>Customer</literal>s (unlikely
+        in this case, but imagine if it were a popular vet for a large Pony
+        Club!) the amount of memory consumed and time required to construct
+        the <literal>Authentication</literal> object would be undesirable. The
+        final method, opening the <literal>Customer</literal> directly from
+        external code, is probably the best of the three. It achieves
+        separation of concerns, and doesn't misuse memory or CPU cycles, but
+        it is still inefficient in that both the
+        <literal>AccessDecisionVoter</literal> and the eventual business
+        method itself will perform a call to the DAO responsible for
+        retrieving the <literal>Customer</literal> object. Two accesses per
+        method invocation is clearly undesirable. In addition, with every
+        approach listed you'll need to write your own access control list
+        (ACL) persistence and business logic from scratch.</para>
+
+        <para>Fortunately, there is another alternative, which we'll talk
+        about below.</para>
+      </sect2>
+
+      <sect2 id="acls-acl-package">
+        <title>The net.sf.acegisecurity.acl Package</title>
+
+        <para>The <literal>net.sf.acegisecurity.acl</literal> package is very
+        simple, comprising only a handful of interfaces and a single class. It
+        provides the basic foundation for access control list (ACL) lookups.
+        The central interface is <literal>AclManager</literal>, which is
+        defined by two methods:</para>
+
+        <para><programlisting>public AclEntry[] getAcls(java.lang.Object domainInstance);
+public AclEntry[] getAcls(java.lang.Object domainInstance, Authentication authentication);</programlisting></para>
+
+        <para><literal>AclManager</literal> is intended to be used as a
+        collaborator against your business objects, or, more desirably,
+        <literal>AccessDecisionVoter</literal>s. This means you use Spring's
+        normal <literal>ApplicationContext</literal> features to wire up your
+        <literal>AccessDecisionVoter</literal> (or business method) with an
+        <literal>AclManager</literal>. Consideration was given to placing the
+        ACL information in the <literal>ContextHolder</literal>, but it was
+        felt this would be inefficient both in terms of memory usage as well
+        as the time spent loading potentially unused ACL information. The
+        trade-off of needing to wire up a collaborator for those objects
+        requiring ACL information is rather minor, particularly in a
+        Spring-managed application.</para>
+
+        <para>The first method of the <literal>AclManager</literal> will
+        return all ACLs applying to the domain object instance passed to it.
+        The second method does the same, but only returns those ACLs which
+        apply to the passed <literal>Authentication</literal> object.</para>
+
+        <para>The <literal>AclEntry</literal> interface returned by
+        <literal>AclManager</literal> is merely a marker interface. You will
+        need to provide an implementation that reflects that ACL permissions
+        for your application.</para>
+
+        <para>Rounding out the <literal>net.sf.acegisecurity.acl</literal>
+        package is an <literal>AclProviderManager</literal> class, with a
+        corresponding <literal>AclProvider</literal> interface.
+        <literal>AclProviderManager</literal> is a concrete implementation of
+        <literal>AclManager</literal>, which iterates through registered
+        <literal>AclProvider</literal>s. The first
+        <literal>AclProvider</literal> that indicates it can authoritatively
+        provide ACL information for the presented domain object instance will
+        be used. This is very similar to the
+        <literal>AuthenticationProvider</literal> interface used for
+        authentication.</para>
+
+        <para>With this background, let's now look at a usable ACL
+        implementation.</para>
+      </sect2>
+
+      <sect2 id="acls-masking">
+        <title>Integer Masked ACLs</title>
+
+        <para>Acegi Security System for Spring includes a production-quality
+        ACL provider implementation. The implementation is based on integer
+        masking, which is commonly used for ACL permissions given its
+        flexibility and speed. Anyone who has used Unix's
+        <literal>chmod</literal> command will know all about this type of
+        permission masking (eg <literal>chmod 777</literal>). You'll find the
+        classes and interfaces for the integer masking ACL package under
+        <literal>net.sf.acegisecurity.acl.basic</literal>.</para>
+
+        <para>Extending the <literal>AclEntry</literal> interface is a
+        <literal>BasicAclEntry</literal> interface, with the main methods
+        shown below:</para>
+
+        <para><programlisting>public AclObjectIdentity getAclObjectIdentity();
+public AclObjectIdentity getAclObjectParentIdentity();
+public int getMask();
+public java.lang.Object getRecipient();</programlisting></para>
+
+        <para>As shown, each <literal>BasicAclEntry</literal> has four main
+        properties. The <literal>mask</literal> is the integer that represents
+        the permissions granted to the <literal>recipient</literal>. The
+        <literal>aclObjectIdentity</literal> is able to identify the domain
+        object instance for which the ACL applies, and the
+        <literal>aclObjectParentIdentity</literal> optionally specifies the
+        parent of the domain object instance. Multiple
+        <literal>BasicAclEntry</literal>s usually exist against a single
+        domain object instance, and as suggested by the parent identity
+        property, permissions granted higher in the object hierarchy will
+        trickle down and be inherited (unless blocked by integer zero).</para>
+
+        <para><literal>BasicAclEntry</literal> implementations typically
+        provide convenience methods, such as
+        <literal>isReadAllowed()</literal>, to avoid application classes
+        needing to perform bit masking themselves. The
+        <literal>SimpleAclEntry</literal> and
+        <literal>AbstractBasicAclEntry</literal> demonstrate and provide much
+        of this bit masking logic.</para>
+
+        <para>The <literal>AclObjectIdentity</literal> itself is merely a
+        marker interface, so you need to provide implementations for your
+        domain objects. However, the package does include a
+        <literal>NamedEntityObjectIdentity</literal> implementation which will
+        suit many needs. The <literal>NamedEntityObjectIdentity</literal>
+        identifies a given domain object instance by the classname of the
+        instance and the identity of the instance. A
+        <literal>NamedEntityObjectIdentity</literal> can be constructed
+        manually (by calling the constructor and providing the classname and
+        identity <literal>String</literal>s), or by passing in any domain
+        object that contains a <literal>getId()</literal> method.</para>
+
+        <para>The actual <literal>AclProvider</literal> implementation is
+        named <literal>BasicAclProvider</literal>. It has adopted a similar
+        design to that used by the authentication-related
+        <literal>DaoAuthenticationProvder</literal>. Specifically, you define
+        a <literal>BasicAclDao</literal> against the provider, so different
+        ACL repository types can be accessed in a pluggable manner. The
+        <literal>BasicAclProvider</literal> also supports pluggable cache
+        providers (with Acegi Security System for Spring including an
+        implementation that fronts EH-CACHE).</para>
+
+        <para>The <literal>BasicAclDao</literal> interface is very simple to
+        implement:</para>
+
+        <para><programlisting>public BasicAclEntry[] getAcls(AclObjectIdentity aclObjectIdentity);</programlisting></para>
+
+        <para>A <literal>BasicAclDao</literal> implementation needs to
+        understand the presented <literal>AclObjectIdentity</literal> and how
+        it maps to a storage repository, find the relevant records, and create
+        appropriate <literal>BasicAclEntry</literal> objects and return
+        them.</para>
+
+        <para>Acegi Security includes a single <literal>BasicAclDao</literal>
+        implementation called <literal>JdbcDaoImpl</literal>. As implied by
+        the name, it accesses ACL information from a JDBC database. The
+        default database schema and some sample data will aid in understanding
+        its function:</para>
+
+        <para><programlisting>CREATE TABLE acls (
+  object_identity VARCHAR_IGNORECASE(250) NOT NULL,
+  recipient VARCHAR_IGNORECASE(100) NOT NULL,
+  parent_object_identity VARCHAR_IGNORECASE(250),
+  mask INTEGER NOT NULL,
+  acl_class VARCHAR_IGNORECASE(250) NOT NULL,
+  CONSTRAINT pk_acls PRIMARY KEY(object_identity, recipient)
+);
+
+INSERT INTO acls VALUES ('corp.DomainObject:1', 'ROLE_SUPERVISOR', null, 1, 'net.sf.acegisecurity.acl.basic.SimpleAclEntry');
+INSERT INTO acls VALUES ('corp.DomainObject:2', 'ROLE_SUPERVISOR', 'corp.DomainObject:1', 0, 'net.sf.acegisecurity.acl.basic.SimpleAclEntry');
+INSERT INTO acls VALUES ('corp.DomainObject:2', 'marissa', 'corp.DomainObject:1', 2, 'net.sf.acegisecurity.acl.basic.SimpleAclEntry');
+INSERT INTO acls VALUES ('corp.DomainObject:3', 'scott', 'corp.DomainObject:1', 14, 'net.sf.acegisecurity.acl.basic.SimpleAclEntry');
+INSERT INTO acls VALUES ('corp.DomainObject:4', 'inheritance_marker_only', 'corp.DomainObject:1', 0, 'net.sf.acegisecurity.acl.basic.SimpleAclEntry');
+INSERT INTO acls VALUES ('corp.DomainObject:5', 'inheritance_marker_only', 'corp.DomainObject:3', 0, 'net.sf.acegisecurity.acl.basic.SimpleAclEntry');
+INSERT INTO acls VALUES ('corp.DomainObject:6', 'scott', 'corp.DomainObject:3', 1, 'net.sf.acegisecurity.acl.basic.SimpleAclEntry');
+INSERT INTO acls VALUES ('corp.DomainObject:7', 'scott', 'some.invalid.parent:1', 2, 'net.sf.acegisecurity.acl.basic.SimpleAclEntry');</programlisting></para>
+
+        <para>The <literal>JdbcDaoImpl</literal> will only respond to requests
+        for <literal>NamedEntityObjectIdentity</literal>s. It converts such
+        identities into a single <literal>String</literal>, comprising
+        the<literal> NamedEntityObjectIdentity.getClassname()</literal> +
+        <literal>":"</literal> +
+        <literal>NamedEntityObjectIdentity.getId()</literal>. This yields the
+        type of <literal>object_identity</literal> values shown above. As
+        indicated by the sample data, each database row corresponds to a
+        single <literal>BasicAclEntry</literal>. As stated earlier and
+        demonstrated by <literal>corp.DomainObject:2</literal> in the above
+        sample data, each domain object instance will often have multiple
+        <literal>BasicAclEntry</literal>[]s.</para>
+
+        <para>As <literal>JdbcDaoImpl</literal> is required to return concrete
+        <literal>BasicAclEntry</literal> classes, it needs to know which
+        <literal>BasicAclEntry</literal> implementation it is to create and
+        populate. This is the role of the <literal>acl_class</literal> column.
+        <literal>JdbcDaoImpl</literal> will create the indicated class and set
+        its <literal>mask</literal>, <literal>recipient</literal>,
+        <literal>aclObjectIdentity</literal> and
+        <literal>aclObjectParentIdentity</literal> properties.</para>
+
+        <para>As you can probably tell from the sample data, the
+        <literal>parent_object_identity</literal> value can either be null or
+        in the same format as the <literal>object_identity</literal>. If
+        non-null, <literal>JdbcDaoImpl</literal> will create a
+        <literal>NamedEntityObjectIdentity</literal> to place inside the
+        returned <literal>BasicAclEntry</literal> class.</para>
+
+        <para>Returning to the <literal>BasicAclProvider</literal>, before it
+        can poll the <literal>BasicAclDao</literal> implementation it needs to
+        convert the domain object instance it was passed into an
+        <literal>AclObjectIdentity</literal>.
+        <literal>BasicAclProvider</literal> has a <literal>protected
+        AclObjectIdentity obtainIdentity(Object domainInstance)</literal>
+        method that is responsible for this. As a protected method, it enables
+        subclasses to easily override. The normal implementation checks
+        whether the passed domain object instance implements the
+        <literal>AclObjectIdentityAware</literal> interface, which is merely a
+        getter for an <literal>AclObjectIdentity</literal>. If the domain
+        object does implement this interface, that is the identity returned.
+        If the domain object does not implement this interface, the method
+        will attempt to create an <literal>AclObjectIdentity</literal> by
+        passing the domain object instance to the constructor of a class
+        defined by the
+        <literal>BasicAclProvider.getDefaultAclObjectIdentity()</literal>
+        method. By default the defined class is
+        <literal>NamedEntityObjectIdentity</literal>, which was described in
+        more detail above. Therefore, you will need to either (i) provide a
+        <literal>getId()</literal> method on your domain objects, (ii)
+        implement <literal>AclObjectIdentityAware</literal> on your domain
+        objects, (iii) provide an alternative
+        <literal>AclObjectIdentity</literal> implementation that will accept
+        your domain object in its constructor, or (iv) override the
+        <literal>obtainIdentity(Object)</literal> method.</para>
+
+        <para>Once the <literal>AclObjectIdentity</literal> of the domain
+        object instance is determined, the <literal>BasicAclProvider</literal>
+        will poll the DAO to obtain its <literal>BasicAclEntry</literal>[]s.
+        If any of the entries returned by the DAO indicate there is a parent,
+        that parent will be polled, and the process will repeat until there is
+        no further parent. The permissions assigned to a
+        <literal>recipient</literal> closest to the domain object instance
+        will always take priority and override any inherited permissions. From
+        the sample data above, the following inherited permissions would
+        apply:</para>
+
+        <para><programlisting>--- Mask integer 0  = no permissions
+--- Mask integer 1  = administer
+--- Mask integer 2  = read
+--- Mask integer 6  = read and write permissions
+--- Mask integer 14 = read and write and create permissions
+
+---------------------------------------------------------------------
+--- *** INHERITED RIGHTS FOR DIFFERENT INSTANCES AND RECIPIENTS ***
+--- INSTANCE  RECIPIENT         PERMISSION(S) (COMMENT #INSTANCE)
+---------------------------------------------------------------------
+---    1      ROLE_SUPERVISOR   Administer
+---    2      ROLE_SUPERVISOR   None (overrides parent #1)
+---           marissa           Read
+---    3      ROLE_SUPERVISOR   Administer (from parent #1)
+---           scott             Read, Write, Create
+---    4      ROLE_SUPERVISOR   Administer (from parent #1)
+---    5      ROLE_SUPERVISOR   Administer (from parent #3)
+---           scott             Read, Write, Create (from parent #3)
+---    6      ROLE_SUPERVISOR   Administer (from parent #3)
+---           scott             Administer (overrides parent #3)
+---    7      scott             Read (invalid parent ignored)</programlisting></para>
+
+        <para>So the above explains how a domain object instance has its
+        <literal>AclObjectIdentity</literal> discovered, and the
+        <literal>BasicAclDao</literal> will be polled successively until an
+        array of inherited permissions is constructed for the domain object
+        instance. The final step is to determine the
+        <literal>BasicAclEntry</literal>[]s that are actually applicable to a
+        given <literal>Authentication</literal> object.</para>
+
+        <para>As you would recall, the <literal>AclManager</literal> (and all
+        delegates, up to and including <literal>BasicAclProvider</literal>)
+        provides a method which returns only those
+        <literal>BasicAclEntry</literal>[]s applying to a passed
+        <literal>Authentication</literal> object.
+        <literal>BasicAclProvider</literal> delivers this functionality by
+        delegating the filtering operation to an
+        <literal>EffectiveAclsResolver</literal> implementation. The default
+        implementation,
+        <literal>GrantedAuthorityEffectiveAclsResolver</literal>, will iterate
+        through the <literal>BasicAclEntry</literal>[]s and include only those
+        where the <literal>recipient</literal> is equal to either the
+        <literal>Authentication</literal>'s <literal>principal</literal> or
+        any of the <literal>Authentication</literal>'s
+        <literal>GrantedAuthority</literal>[]s. Please refer to the JavaDocs
+        for more information.</para>
+      </sect2>
+
+      <sect2 id="acls-conclusion">
+        <title>Conclusion</title>
+
+        <para>Acegi Security's instance-specific ACL packages shield you from
+        much of the complexity of developing your own ACL approach. The
+        interfaces and classes detailed above provide a scalable, customisable
+        ACL solution that is decoupled from your application code. Whilst the
+        reference documentation may suggest complexity, the basic
+        implementation is able to support most typical applications
+        out-of-the-box.</para>
+      </sect2>
+    </sect1>
+
     <sect1 id="security-filters">
     <sect1 id="security-filters">
       <title>Filters</title>
       <title>Filters</title>
 
 
@@ -3081,6 +3450,11 @@ $CATALINA_HOME/bin/startup.sh</programlisting></para>
         <para>All of the above filters use
         <para>All of the above filters use
         <literal>FilterToBeanProxy</literal>, which is discussed in the
         <literal>FilterToBeanProxy</literal>, which is discussed in the
         previous section.</para>
         previous section.</para>
+
+        <para>If you're using SiteMesh, ensure the Acegi Security filters
+        execute before the SiteMesh filters are called. This enables the
+        <literal>ContextHolder</literal> to be populated in time for use by
+        SiteMesh decorators.</para>
       </sect2>
       </sect2>
     </sect1>
     </sect1>