From 56829872b61f8c8d4477963d25aad913e053930f Mon Sep 17 00:00:00 2001 From: Ben Alex Date: Thu, 29 Jul 2004 07:51:22 +0000 Subject: [PATCH] Initial commit of ACL capabilities. --- .../java/org/acegisecurity/acl/AclEntry.java | 25 ++ .../org/acegisecurity/acl/AclManager.java | 58 +++ .../org/acegisecurity/acl/AclProvider.java | 82 ++++ .../acegisecurity/acl/AclProviderManager.java | 159 ++++++++ .../acl/basic/AbstractBasicAclEntry.java | 279 +++++++++++++ .../acl/basic/AclObjectIdentity.java | 67 +++ .../acl/basic/AclObjectIdentityAware.java | 42 ++ .../acegisecurity/acl/basic/BasicAclDao.java | 58 +++ .../acl/basic/BasicAclEntry.java | 126 ++++++ .../acl/basic/BasicAclEntryCache.java | 61 +++ .../acl/basic/BasicAclProvider.java | 341 ++++++++++++++++ .../acl/basic/EffectiveAclsResolver.java | 65 +++ ...GrantedAuthorityEffectiveAclsResolver.java | 102 +++++ .../acl/basic/NamedEntityObjectIdentity.java | 161 ++++++++ .../acl/basic/SimpleAclEntry.java | 112 ++++++ .../acl/basic/cache/BasicAclEntryHolder.java | 78 ++++ .../cache/EhCacheBasedAclEntryCache.java | 135 +++++++ .../acl/basic/cache/NullAclEntryCache.java | 56 +++ .../acl/basic/cache/package.html | 5 + .../acl/basic/jdbc/JdbcDaoImpl.java | 256 ++++++++++++ .../acegisecurity/acl/basic/jdbc/package.html | 5 + .../org/acegisecurity/acl/basic/package.html | 5 + .../java/org/acegisecurity/acl/package.html | 15 + .../acl/AclProviderManagerTests.java | 207 ++++++++++ .../acl/basic/BasicAclProviderTests.java | 365 +++++++++++++++++ ...edAuthorityEffectiveAclsResolverTests.java | 118 ++++++ .../acl/basic/MockAclObjectIdentity.java | 28 ++ .../basic/NamedEntityObjectIdentityTests.java | 135 +++++++ .../acl/basic/SimpleAclEntryTests.java | 183 +++++++++ .../acegisecurity/acl/basic/SomeDomain.java | 38 ++ .../basic/cache/BasicAclEntryHolderTests.java | 66 +++ .../cache/EhCacheBasedAclEntryCacheTests.java | 95 +++++ .../basic/cache/NullAclEntryCacheTests.java | 58 +++ .../acl/basic/jdbc/JdbcDaoImplTests.java | 121 ++++++ docs/reference/src/index.xml | 380 +++++++++++++++++- 35 files changed, 4084 insertions(+), 3 deletions(-) create mode 100644 core/src/main/java/org/acegisecurity/acl/AclEntry.java create mode 100644 core/src/main/java/org/acegisecurity/acl/AclManager.java create mode 100644 core/src/main/java/org/acegisecurity/acl/AclProvider.java create mode 100644 core/src/main/java/org/acegisecurity/acl/AclProviderManager.java create mode 100644 core/src/main/java/org/acegisecurity/acl/basic/AbstractBasicAclEntry.java create mode 100644 core/src/main/java/org/acegisecurity/acl/basic/AclObjectIdentity.java create mode 100644 core/src/main/java/org/acegisecurity/acl/basic/AclObjectIdentityAware.java create mode 100644 core/src/main/java/org/acegisecurity/acl/basic/BasicAclDao.java create mode 100644 core/src/main/java/org/acegisecurity/acl/basic/BasicAclEntry.java create mode 100644 core/src/main/java/org/acegisecurity/acl/basic/BasicAclEntryCache.java create mode 100644 core/src/main/java/org/acegisecurity/acl/basic/BasicAclProvider.java create mode 100644 core/src/main/java/org/acegisecurity/acl/basic/EffectiveAclsResolver.java create mode 100644 core/src/main/java/org/acegisecurity/acl/basic/GrantedAuthorityEffectiveAclsResolver.java create mode 100644 core/src/main/java/org/acegisecurity/acl/basic/NamedEntityObjectIdentity.java create mode 100644 core/src/main/java/org/acegisecurity/acl/basic/SimpleAclEntry.java create mode 100644 core/src/main/java/org/acegisecurity/acl/basic/cache/BasicAclEntryHolder.java create mode 100644 core/src/main/java/org/acegisecurity/acl/basic/cache/EhCacheBasedAclEntryCache.java create mode 100644 core/src/main/java/org/acegisecurity/acl/basic/cache/NullAclEntryCache.java create mode 100644 core/src/main/java/org/acegisecurity/acl/basic/cache/package.html create mode 100644 core/src/main/java/org/acegisecurity/acl/basic/jdbc/JdbcDaoImpl.java create mode 100644 core/src/main/java/org/acegisecurity/acl/basic/jdbc/package.html create mode 100644 core/src/main/java/org/acegisecurity/acl/basic/package.html create mode 100644 core/src/main/java/org/acegisecurity/acl/package.html create mode 100644 core/src/test/java/org/acegisecurity/acl/AclProviderManagerTests.java create mode 100644 core/src/test/java/org/acegisecurity/acl/basic/BasicAclProviderTests.java create mode 100644 core/src/test/java/org/acegisecurity/acl/basic/GrantedAuthorityEffectiveAclsResolverTests.java create mode 100644 core/src/test/java/org/acegisecurity/acl/basic/MockAclObjectIdentity.java create mode 100644 core/src/test/java/org/acegisecurity/acl/basic/NamedEntityObjectIdentityTests.java create mode 100644 core/src/test/java/org/acegisecurity/acl/basic/SimpleAclEntryTests.java create mode 100644 core/src/test/java/org/acegisecurity/acl/basic/SomeDomain.java create mode 100644 core/src/test/java/org/acegisecurity/acl/basic/cache/BasicAclEntryHolderTests.java create mode 100644 core/src/test/java/org/acegisecurity/acl/basic/cache/EhCacheBasedAclEntryCacheTests.java create mode 100644 core/src/test/java/org/acegisecurity/acl/basic/cache/NullAclEntryCacheTests.java create mode 100644 core/src/test/java/org/acegisecurity/acl/basic/jdbc/JdbcDaoImplTests.java diff --git a/core/src/main/java/org/acegisecurity/acl/AclEntry.java b/core/src/main/java/org/acegisecurity/acl/AclEntry.java new file mode 100644 index 0000000000..b6c2af521c --- /dev/null +++ b/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 {} diff --git a/core/src/main/java/org/acegisecurity/acl/AclManager.java b/core/src/main/java/org/acegisecurity/acl/AclManager.java new file mode 100644 index 0000000000..fe880ae31e --- /dev/null +++ b/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 AclEntry 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 null) + * + * @return the ACLs that apply, or null 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 + * Authentication object + * + * @param domainInstance the instance for which ACL information is required + * (never null) + * @param authentication the prncipal for which ACL information should be + * filtered (never null) + * + * @return only those ACLs applying to the domain instance that have been + * granted to the principal (or null) if no such ACLs + * are found + */ + public AclEntry[] getAcls(Object domainInstance, + Authentication authentication); +} diff --git a/core/src/main/java/org/acegisecurity/acl/AclProvider.java b/core/src/main/java/org/acegisecurity/acl/AclProvider.java new file mode 100644 index 0000000000..0b7f08ba13 --- /dev/null +++ b/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. + * + *

+ * Implementations are typically called from the {@link AclProviderManager}. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public interface AclProvider { + //~ Methods ================================================================ + + /** + * Obtains the ACLs that apply to the specified domain instance. + * + *

+ * Will never be called unless the {@link #supports(Object)} method + * returned true. + *

+ * + * @param domainInstance the instance for which ACL information is required + * (never null) + * + * @return the ACLs that apply, or null 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 Authentication object. + * + *

+ * Will never be called unless the {@link #supports(Object)} method + * returned true. + *

+ * + * @param domainInstance the instance for which ACL information is required + * (never null) + * @param authentication the prncipal for which ACL information should be + * filtered (never null) + * + * @return only those ACLs applying to the domain instance that have been + * granted to the principal (or null) if no such ACLs + * are found + */ + public AclEntry[] getAcls(Object domainInstance, + Authentication authentication); + + /** + * Indicates whether this AclProvider can authoritatively + * return ACL information for the specified domain object instance. + * + * @param domainInstance the instance for which ACL information is required + * (never null) + * + * @return true if this provider is authoritative for the + * specified domain object instance, false otherwise + */ + public boolean supports(Object domainInstance); +} diff --git a/core/src/main/java/org/acegisecurity/acl/AclProviderManager.java b/core/src/main/java/org/acegisecurity/acl/AclProviderManager.java new file mode 100644 index 0000000000..97c9de8290 --- /dev/null +++ b/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. + * + *

+ * If no compatible provider is found, it is assumed that no ACLs apply for the + * specified domain object instance and null is returned. + *

+ * + * @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"); + } + } +} diff --git a/core/src/main/java/org/acegisecurity/acl/basic/AbstractBasicAclEntry.java b/core/src/main/java/org/acegisecurity/acl/basic/AbstractBasicAclEntry.java new file mode 100644 index 0000000000..98f425de18 --- /dev/null +++ b/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}. + * + *

+ * Provides core bit mask handling methods. + *

+ * + * @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 public static final int. It is further recommended that + * valid combinations of permissions are also exposed as public + * static final ints. + * + *

+ * This method returns all permission integers that are allowed to be used + * together. This must include any combinations of valid + * permissions. 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. + *

+ * + *

+ * Whilst it may seem unduly time onerous to return every valid permission + * combination, 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. + *

+ * + * @return every 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 + * AbstractBasicAclEntry'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', '.'); + } +} diff --git a/core/src/main/java/org/acegisecurity/acl/basic/AclObjectIdentity.java b/core/src/main/java/org/acegisecurity/acl/basic/AclObjectIdentity.java new file mode 100644 index 0000000000..8b7385fb4c --- /dev/null +++ b/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. + * + *

+ * It should be noted that AclObjectIdentity instances are created + * in various locations throughout the package. As + * AclObjectIdentitys 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 AclObjectIdentitys equal if + * identity1.equals(identity2), rather than reference-equality of + * identity1==identity2. + *

+ * + *

+ * In practical terms this means you must implement the standard + * java.lang.Object methods shown below. Depending on your + * cache's internal structure, you may also need to implement special + * interfaces such as java.util.Comparator or + * java.lang.Comparable. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public interface AclObjectIdentity extends Serializable { + //~ Methods ================================================================ + + /** + * Refer to the java.lang.Object documentation for the + * interface contract. + * + * @param obj to be compared + * + * @return true if the objects are equal, false + * otherwise + */ + public boolean equals(Object obj); + + /** + * Refer to the java.lang.Object documentation for the + * interface contract. + * + * @return a hash code representation of this object + */ + public int hashCode(); +} diff --git a/core/src/main/java/org/acegisecurity/acl/basic/AclObjectIdentityAware.java b/core/src/main/java/org/acegisecurity/acl/basic/AclObjectIdentityAware.java new file mode 100644 index 0000000000..e25c5506d0 --- /dev/null +++ b/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. + * + *

+ * Domain objects must implement this interface if they wish to provide an + * AclObjectIdentity rather than it being determined by relying + * classes. Specifically, the {@link BasicAclProvider} detects and uses this + * interface. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public interface AclObjectIdentityAware { + //~ Methods ================================================================ + + /** + * Retrieves the AclObjectIdentity for this instance. + * + * @return the ACL object identity for this instance (can never be + * null) + */ + public AclObjectIdentity getAclObjectIdentity(); +} diff --git a/core/src/main/java/org/acegisecurity/acl/basic/BasicAclDao.java b/core/src/main/java/org/acegisecurity/acl/basic/BasicAclDao.java new file mode 100644 index 0000000000..e9f94b2ef6 --- /dev/null +++ b/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. + * + *

+ * BasicAclDao implementations are responsible for interpreting a + * given {@link AclObjectIdentity} and being able to lookup and return the + * corresponding {@link BasicAclEntry}[]s. + *

+ * + *

+ * BasicAclDaos many, but are not required to, allow the backend + * ACL repository to specify the class of BasicAclEntry + * implementations that should be returned. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public interface BasicAclDao { + //~ Methods ================================================================ + + /** + * Obtains the ACLs that apply to the specified domain instance. + * + *

+ * Does not 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. + *

+ * + * @param aclObjectIdentity the domain object instance that ACL information + * is being requested for (never null) + * + * @return the ACLs that apply (no nulls are permitted in the + * array), or null if no ACLs could be found for the + * specified ACL object identity + */ + public BasicAclEntry[] getAcls(AclObjectIdentity aclObjectIdentity); +} diff --git a/core/src/main/java/org/acegisecurity/acl/basic/BasicAclEntry.java b/core/src/main/java/org/acegisecurity/acl/basic/BasicAclEntry.java new file mode 100644 index 0000000000..14e8edce3a --- /dev/null +++ b/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 only 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 + * BasicAclEntry. 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 + * null) + */ + public AclObjectIdentity getAclObjectIdentity(); + + /** + * This setter should only be used by DAO implementations. + * + * @param aclObjectParentIdentity an object which represents the parent of + * the domain object instance subject of this ACL entry, or + * null 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 + * BasicAclProvider to walk the inheritance hierarchy. An + * domain object instance need not have a parent. + * + * @return the ACL object identity that is the parent of this ACL entry + * (may be null if no parent should be consulted) + */ + public AclObjectIdentity getAclObjectParentIdentity(); + + /** + * This setter should only 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 only be used by DAO implementations. + * + * @param recipient a representation of the recipient of this ACL entry + * that makes sense to an EffectiveAclsResolver + * implementation + */ + public void setRecipient(Object recipient); + + /** + * A domain object instance will usually have multiple + * BasicAclEntrys. Each separate BasicAclEntry + * applies to a particular "recipient". Typical examples of recipients + * include (but do not necessarily have to include) usernames, role names, + * complex granted authorities etc. + * + *

+ * It is essential that only one BasicAclEntry exists for a + * given recipient. Otherwise conflicts as to the mask that should + * apply to a given recipient will occur. + *

+ * + *

+ * This method indicates which recipient this BasicAclEntry + * applies to. The returned object type will vary depending on the type of + * recipient. For instance, it might be a String containing a + * username, or a GrantedAuthorityImpl 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 Authentication object. + *

+ * + * @return the recipient of this access control list entry (never + * null) + */ + public Object getRecipient(); +} diff --git a/core/src/main/java/org/acegisecurity/acl/basic/BasicAclEntryCache.java b/core/src/main/java/org/acegisecurity/acl/basic/BasicAclEntryCache.java new file mode 100644 index 0000000000..d143f2bdc4 --- /dev/null +++ b/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. + * + *

+ * 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 + * BasicAclEntryCache interface contract because they vary + * depending on the type of caching system used (eg in-memory vs disk vs + * cluster vs hybrid). + *

+ * + * @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 BasicAclEntrys (no + * nulls are permitted in the returned array) or + * null 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. + * + *

+ * No nulls are allowed in the passed array. If any + * null is passed, the implementation may throw an exception. + *

+ * + * @param basicAclEntry the ACL entries to cache (the key will be extracted + * from the {@link BasicAclEntry#getAclObjectIdentity()} method + */ + public void putEntriesInCache(BasicAclEntry[] basicAclEntry); +} diff --git a/core/src/main/java/org/acegisecurity/acl/basic/BasicAclProvider.java b/core/src/main/java/org/acegisecurity/acl/basic/BasicAclProvider.java new file mode 100644 index 0000000000..da8a310908 --- /dev/null +++ b/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; + + +/** + *

+ * Retrieves access control lists (ACL) entries for domain object instances + * from a data access object (DAO). + *

+ * + *

+ * 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 BasicAclProvider responding to particular + * domain object instances. + *

+ * + *

+ * BasicAclProvider will walk an inheritance hierarchy if a + * BasicAclEntry returned by the DAO indicates it has a parent. + * NB: inheritance occurs at a domain instance object level. It does + * not occur at an ACL recipient level. This means + * allBasicAclEntrys for a given domain instance object + * must have the same parent identity, or + * allBasicAclEntrys must have null as their + * parent identity. + *

+ * + *

+ * A cache should be used. This is provided by the {@link BasicAclEntryCache}. + * BasicAclProvider by default is setup to use the {@link + * NullAclEntryCache}, which performs no caching. + *

+ * + *

+ * To implement the {@link #getAcls(Object, Authentication)} method, + * BasicAclProvider requires a {@link EffectiveAclsResolver} to + * be configured against it. By default the {@link + * GrantedAuthorityEffectiveAclsResolver} is used. + *

+ * + * @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 + * null. + */ + 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 AclObjectIdentity class that an + * attempt should be made to construct if the passed object does not + * implement AclObjectIdentityAware. + * + *

+ * NB: Any defaultAclObjectIdentityClassmust provide a + * public constructor that accepts an Object. Otherwise it is + * not possible for the BasicAclProvider to try to create the + * AclObjectIdentity instance at runtime. + *

+ * + * @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 + * AclObjectIdentity is returned by {@link + * #obtainIdentity(Object)}. + * + * @param domainInstance the instance to check + * + * @return true if this provider supports the passed object, + * false otherwise + */ + public boolean supports(Object domainInstance) { + if (obtainIdentity(domainInstance) == null) { + return false; + } else { + return true; + } + } + + /** + * This method looks up the AclObjectIdentity of a passed + * domain object instance. + * + *

+ * This implementation attempts to obtain the + * AclObjectIdentity 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. + *

+ * + * @param domainInstance the domain object instance (never + * null) + * + * @return an ACL object identity, or null 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; + } +} diff --git a/core/src/main/java/org/acegisecurity/acl/basic/EffectiveAclsResolver.java b/core/src/main/java/org/acegisecurity/acl/basic/EffectiveAclsResolver.java new file mode 100644 index 0000000000..4d8e17efdd --- /dev/null +++ b/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 + * Authentication object. + * + *

+ * 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 + * Authentication-presented principals and granted authorities. + *

+ * + *

+ * 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 Authentication + * object), all three permissions would be returned as distinct + * BasicAclEntry 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. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public interface EffectiveAclsResolver { + //~ Methods ================================================================ + + /** + * Determines the ACLs that apply to the presented + * Authentication object. + * + * @param allAcls every ACL assigned to a domain object instance + * @param filteredBy the principal (populated with + * GrantedAuthoritys 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 + * null if there are none after filtering + */ + public AclEntry[] resolveEffectiveAcls(AclEntry[] allAcls, + Authentication filteredBy); +} diff --git a/core/src/main/java/org/acegisecurity/acl/basic/GrantedAuthorityEffectiveAclsResolver.java b/core/src/main/java/org/acegisecurity/acl/basic/GrantedAuthorityEffectiveAclsResolver.java new file mode 100644 index 0000000000..d98e9760f9 --- /dev/null +++ b/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}. + * + *

+ * This implementation does not need to understand the "recipient" types + * presented in a BasicAclEntry because it merely delegates to + * the detected {@link Authentication#getPrincipal()} or {@link + * Authentication#getAuthorities()}. The principal object or granted + * authorities object has its Object.equals(recipient) method + * called to make the decision as to whether the recipient in the + * BasicAclEntry is the same as the principal or granted + * authority. + *

+ * + *

+ * This class should prove an adequate ACLs resolver if you're using standard + * Acegi Security classes. This is because the typical + * Authentication token is + * UsernamePasswordAuthenticationToken, which for its + * principal is usually a String. The + * GrantedAuthorityImpl is typically used for granted + * authorities, which tests for equality based on a String. This + * means BasicAclDaos simply need to return a String + * to represent the recipient. If you use non-String objects, you + * will probably require an alternative EffectiveAclsResolver. + *

+ * + * @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; + } + } +} diff --git a/core/src/main/java/org/acegisecurity/acl/basic/NamedEntityObjectIdentity.java b/core/src/main/java/org/acegisecurity/acl/basic/NamedEntityObjectIdentity.java new file mode 100644 index 0000000000..872c8ce4d9 --- /dev/null +++ b/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}. + * + *

+ * Uses Strings to store the identity of the domain object + * instance. Also offers a constructor that uses reflection to build the + * identity information. + *

+ * + * @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 NamedEntityObjectIdentity based on the passed + * object instance. The passed object must provide a getId() + * 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 null) + */ + public String getClassname() { + return classname; + } + + /** + * Indicates the instance identity portion of the object identity. + * + * @return the instance identity (never null) + */ + public String getId() { + return id; + } + + /** + * Important so caching operates properly. + * + *

+ * Considers an object of the same class equal if it has the same + * classname and id properties. + *

+ * + * @param arg0 object to compare + * + * @return true 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(); + } +} diff --git a/core/src/main/java/org/acegisecurity/acl/basic/SimpleAclEntry.java b/core/src/main/java/org/acegisecurity/acl/basic/SimpleAclEntry.java new file mode 100644 index 0000000000..f6d4b94dc3 --- /dev/null +++ b/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 newInstance(). + * + *

+ * Normal classes should not use this default constructor. + *

+ */ + 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(); + } +} diff --git a/core/src/main/java/org/acegisecurity/acl/basic/cache/BasicAclEntryHolder.java b/core/src/main/java/org/acegisecurity/acl/basic/cache/BasicAclEntryHolder.java new file mode 100644 index 0000000000..f7038d8f32 --- /dev/null +++ b/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 + * BasicAclEntrys in the cache. + * + *

+ * This is necessary because caches store a single object per key, not an + * array. + *

+ * + *

+ * This class uses value object semantics. ie: construction-based + * initialisation without any setters for the properties. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public class BasicAclEntryHolder implements Serializable { + //~ Instance fields ======================================================== + + private BasicAclEntry[] basicAclEntries; + + //~ Constructors =========================================================== + + /** + * Constructs the BasicAclEntryHolder. + * + * @param aclEntries to cache (any nulls will cause an + * exception, which should not be a problem as the contract for + * BasicAclEntryCache allows exceptions if + * nulls are presented) + * + * @throws IllegalArgumentException if a null exists anywhere + * in the aclEntries or if a null 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; + } +} diff --git a/core/src/main/java/org/acegisecurity/acl/basic/cache/EhCacheBasedAclEntryCache.java b/core/src/main/java/org/acegisecurity/acl/basic/cache/EhCacheBasedAclEntryCache.java new file mode 100644 index 0000000000..0d6e1b276d --- /dev/null +++ b/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 BasicAclEntrys using EHCACHE. + * + * @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. + * + *

+ * Defaults to 5 minutes. + *

+ * + * @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); + } +} diff --git a/core/src/main/java/org/acegisecurity/acl/basic/cache/NullAclEntryCache.java b/core/src/main/java/org/acegisecurity/acl/basic/cache/NullAclEntryCache.java new file mode 100644 index 0000000000..d7d94f8464 --- /dev/null +++ b/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. + * + *

+ * Do not use in production settings, as ACL queries are likely to be + * extensive. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public class NullAclEntryCache implements BasicAclEntryCache { + //~ Methods ================================================================ + + /** + * As nothing ever stored in the cache, will always return + * null. + * + * @param aclObjectIdentity ignored + * + * @return always null + */ + 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) {} +} diff --git a/core/src/main/java/org/acegisecurity/acl/basic/cache/package.html b/core/src/main/java/org/acegisecurity/acl/basic/cache/package.html new file mode 100644 index 0000000000..aff4f2f651 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/acl/basic/cache/package.html @@ -0,0 +1,5 @@ + + +Caches ACL information for the BasicAclProvider. + + diff --git a/core/src/main/java/org/acegisecurity/acl/basic/jdbc/JdbcDaoImpl.java b/core/src/main/java/org/acegisecurity/acl/basic/jdbc/JdbcDaoImpl.java new file mode 100644 index 0000000000..4c068cbf66 --- /dev/null +++ b/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; + + +/** + *

+ * Retrieves ACL details from a JDBC location. + *

+ * + *

+ * 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. + *

+ * + * @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 + * AclObjectIdentity. + * + *

+ * The {@link BasicAclEntry}s returned by this method will have + * String-based recipients. This will not be a problem if you + * are using the GrantedAuthorityEffectiveAclsResolver, which + * is the default configured against BasicAclProvider. + *

+ * + *

+ * This method will only return ACLs for requests where the + * AclObjectIdentity is of type {@link + * NamedEntityObjectIdentity}. Of course, you can subclass or replace this + * class and support your own custom AclObjectIdentity types. + *

+ * + * @param aclObjectIdentity for which ACL information is required (cannot + * be null and must be an instance of + * NamedEntityObjectIdentity) + * + * @return the ACLs that apply (without any nulls inside the + * array), or null if not found or if an incompatible + * AclObjectIdentity 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. + * + *

+ * 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 + *

+ */ + 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); + } + } +} diff --git a/core/src/main/java/org/acegisecurity/acl/basic/jdbc/package.html b/core/src/main/java/org/acegisecurity/acl/basic/jdbc/package.html new file mode 100644 index 0000000000..e98cd09475 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/acl/basic/jdbc/package.html @@ -0,0 +1,5 @@ + + +JDBC-based data access object for ACL information. + + diff --git a/core/src/main/java/org/acegisecurity/acl/basic/package.html b/core/src/main/java/org/acegisecurity/acl/basic/package.html new file mode 100644 index 0000000000..239e2530e2 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/acl/basic/package.html @@ -0,0 +1,5 @@ + + +Access control list implementation based on integer bit masks. + + diff --git a/core/src/main/java/org/acegisecurity/acl/package.html b/core/src/main/java/org/acegisecurity/acl/package.html new file mode 100644 index 0000000000..782b8df14f --- /dev/null +++ b/core/src/main/java/org/acegisecurity/acl/package.html @@ -0,0 +1,15 @@ + + +Enables retrieval of access control lists (ACLs) for domain object instances. + +

The goal of this package is to locate the AclEntrys +that apply to a given domain object instance. +

+ +

+An AclManager has ultimate resposibility for obtaining the +AclEntrys instances, with a provider-based implementation +available via the AclProviderManager class (and +its AclProvider interface.

+ + diff --git a/core/src/test/java/org/acegisecurity/acl/AclProviderManagerTests.java b/core/src/test/java/org/acegisecurity/acl/AclProviderManagerTests.java new file mode 100644 index 0000000000..a8da196711 --- /dev/null +++ b/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 Objects of type String + * + * @param domainInstance DOCUMENT ME! + * + * @return DOCUMENT ME! + */ + public boolean supports(Object domainInstance) { + return (domainInstance instanceof String); + } + } +} diff --git a/core/src/test/java/org/acegisecurity/acl/basic/BasicAclProviderTests.java b/core/src/test/java/org/acegisecurity/acl/basic/BasicAclProviderTests.java new file mode 100644 index 0000000000..6683399345 --- /dev/null +++ b/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()); + } + } +} diff --git a/core/src/test/java/org/acegisecurity/acl/basic/GrantedAuthorityEffectiveAclsResolverTests.java b/core/src/test/java/org/acegisecurity/acl/basic/GrantedAuthorityEffectiveAclsResolverTests.java new file mode 100644 index 0000000000..1d2a9631df --- /dev/null +++ b/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 + } +} diff --git a/core/src/test/java/org/acegisecurity/acl/basic/MockAclObjectIdentity.java b/core/src/test/java/org/acegisecurity/acl/basic/MockAclObjectIdentity.java new file mode 100644 index 0000000000..3e57c705a6 --- /dev/null +++ b/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 AclObjectIdentity but is incompatible with + * BasicAclProvider 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! +} diff --git a/core/src/test/java/org/acegisecurity/acl/basic/NamedEntityObjectIdentityTests.java b/core/src/test/java/org/acegisecurity/acl/basic/NamedEntityObjectIdentityTests.java new file mode 100644 index 0000000000..0d99e4c8e0 --- /dev/null +++ b/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()); + } +} diff --git a/core/src/test/java/org/acegisecurity/acl/basic/SimpleAclEntryTests.java b/core/src/test/java/org/acegisecurity/acl/basic/SimpleAclEntryTests.java new file mode 100644 index 0000000000..3e9b01e8e6 --- /dev/null +++ b/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)]")); + } +} diff --git a/core/src/test/java/org/acegisecurity/acl/basic/SomeDomain.java b/core/src/test/java/org/acegisecurity/acl/basic/SomeDomain.java new file mode 100644 index 0000000000..36c56f1888 --- /dev/null +++ b/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 NamedEntityObjectIdentity. + * + * @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; + } +} diff --git a/core/src/test/java/org/acegisecurity/acl/basic/cache/BasicAclEntryHolderTests.java b/core/src/test/java/org/acegisecurity/acl/basic/cache/BasicAclEntryHolderTests.java new file mode 100644 index 0000000000..e75a562a62 --- /dev/null +++ b/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); + } + } +} diff --git a/core/src/test/java/org/acegisecurity/acl/basic/cache/EhCacheBasedAclEntryCacheTests.java b/core/src/test/java/org/acegisecurity/acl/basic/cache/EhCacheBasedAclEntryCacheTests.java new file mode 100644 index 0000000000..b0834549af --- /dev/null +++ b/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()); + } +} diff --git a/core/src/test/java/org/acegisecurity/acl/basic/cache/NullAclEntryCacheTests.java b/core/src/test/java/org/acegisecurity/acl/basic/cache/NullAclEntryCacheTests.java new file mode 100644 index 0000000000..8713d90e52 --- /dev/null +++ b/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")); + } +} diff --git a/core/src/test/java/org/acegisecurity/acl/basic/jdbc/JdbcDaoImplTests.java b/core/src/test/java/org/acegisecurity/acl/basic/jdbc/JdbcDaoImplTests.java new file mode 100644 index 0000000000..f9fee5ed20 --- /dev/null +++ b/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; + } + } +} diff --git a/docs/reference/src/index.xml b/docs/reference/src/index.xml index 80c2e9577d..d41591cb74 100644 --- a/docs/reference/src/index.xml +++ b/docs/reference/src/index.xml @@ -69,7 +69,7 @@ Key Components - The Acegi Security System for Spring essentially comprises six + The Acegi Security System for Spring essentially comprises seven key functional parts: @@ -109,6 +109,11 @@ authentication, authorization, run-as replacement and execution of a given operation. + + + An acess control list (ACL) management package, which can be + used to obtain ACLs for domain object instances. + Secure objects refer to any type of object that can have @@ -134,8 +139,8 @@ FilterInterceptor) with complete transparency. - Each of the six key parts is discussed in detail throughout this - document. + Each of the seven key parts is discussed in detail throughout + this document. @@ -2985,6 +2990,370 @@ $CATALINA_HOME/bin/startup.sh + + Instance-Based Access Control List (ACL) Security + + + Overview + + THIS FEATURE WAS ADDED IN VERSION 0.6. WE WELCOME YOUR COMMENTS + AND IMPROVEMENTS. + + 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 + (Authentication), where + (MethodInvocation) and what + (SomeDomainObject). In other words, authorization + decisions also need to consider the actual domain object instance + subject of a method invocation. + + 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: + + Write your business methods to enforce the security. You + could consult a collection within the + Customer domain object instance to determine + which users have access. By using the + ContextHolder.getContext() and casting it to + SecureContext, you'll be able to access the + Authentication object. + + + + Write an AccessDecisionVoter to enforce + the security from the GrantedAuthority[]s + stored in the Authentication object. This + would mean your AuthenticationManager would + need to populate the Authentication with + custom GrantedAuthority[]s representing each + of the Customer domain object instances the + principal has access to. + + + + Write an AccessDecisionVoter to enforce + the security and open the target Customer + domain object directly. This would mean your voter needs access + to a DAO that allows it to retrieve the + Customer object. It would then access the + Customer object's collection of approved + users and make the appropriate decision. + + + + 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 + Customer authorization logic elsewhere. Obtaining + the GrantedAuthority[]s from the + Authentication object is also fine, but will not + scale to large numbers of Customers. If a user + might be able to access 5,000 Customers (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 Authentication object would be undesirable. The + final method, opening the Customer 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 + AccessDecisionVoter and the eventual business + method itself will perform a call to the DAO responsible for + retrieving the Customer 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. + + Fortunately, there is another alternative, which we'll talk + about below. + + + + The net.sf.acegisecurity.acl Package + + The net.sf.acegisecurity.acl 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 AclManager, which is + defined by two methods: + + public AclEntry[] getAcls(java.lang.Object domainInstance); +public AclEntry[] getAcls(java.lang.Object domainInstance, Authentication authentication); + + AclManager is intended to be used as a + collaborator against your business objects, or, more desirably, + AccessDecisionVoters. This means you use Spring's + normal ApplicationContext features to wire up your + AccessDecisionVoter (or business method) with an + AclManager. Consideration was given to placing the + ACL information in the ContextHolder, 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. + + The first method of the AclManager 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 Authentication object. + + The AclEntry interface returned by + AclManager is merely a marker interface. You will + need to provide an implementation that reflects that ACL permissions + for your application. + + Rounding out the net.sf.acegisecurity.acl + package is an AclProviderManager class, with a + corresponding AclProvider interface. + AclProviderManager is a concrete implementation of + AclManager, which iterates through registered + AclProviders. The first + AclProvider that indicates it can authoritatively + provide ACL information for the presented domain object instance will + be used. This is very similar to the + AuthenticationProvider interface used for + authentication. + + With this background, let's now look at a usable ACL + implementation. + + + + Integer Masked ACLs + + 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 + chmod command will know all about this type of + permission masking (eg chmod 777). You'll find the + classes and interfaces for the integer masking ACL package under + net.sf.acegisecurity.acl.basic. + + Extending the AclEntry interface is a + BasicAclEntry interface, with the main methods + shown below: + + public AclObjectIdentity getAclObjectIdentity(); +public AclObjectIdentity getAclObjectParentIdentity(); +public int getMask(); +public java.lang.Object getRecipient(); + + As shown, each BasicAclEntry has four main + properties. The mask is the integer that represents + the permissions granted to the recipient. The + aclObjectIdentity is able to identify the domain + object instance for which the ACL applies, and the + aclObjectParentIdentity optionally specifies the + parent of the domain object instance. Multiple + BasicAclEntrys 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). + + BasicAclEntry implementations typically + provide convenience methods, such as + isReadAllowed(), to avoid application classes + needing to perform bit masking themselves. The + SimpleAclEntry and + AbstractBasicAclEntry demonstrate and provide much + of this bit masking logic. + + The AclObjectIdentity itself is merely a + marker interface, so you need to provide implementations for your + domain objects. However, the package does include a + NamedEntityObjectIdentity implementation which will + suit many needs. The NamedEntityObjectIdentity + identifies a given domain object instance by the classname of the + instance and the identity of the instance. A + NamedEntityObjectIdentity can be constructed + manually (by calling the constructor and providing the classname and + identity Strings), or by passing in any domain + object that contains a getId() method. + + The actual AclProvider implementation is + named BasicAclProvider. It has adopted a similar + design to that used by the authentication-related + DaoAuthenticationProvder. Specifically, you define + a BasicAclDao against the provider, so different + ACL repository types can be accessed in a pluggable manner. The + BasicAclProvider also supports pluggable cache + providers (with Acegi Security System for Spring including an + implementation that fronts EH-CACHE). + + The BasicAclDao interface is very simple to + implement: + + public BasicAclEntry[] getAcls(AclObjectIdentity aclObjectIdentity); + + A BasicAclDao implementation needs to + understand the presented AclObjectIdentity and how + it maps to a storage repository, find the relevant records, and create + appropriate BasicAclEntry objects and return + them. + + Acegi Security includes a single BasicAclDao + implementation called JdbcDaoImpl. 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: + + 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'); + + The JdbcDaoImpl will only respond to requests + for NamedEntityObjectIdentitys. It converts such + identities into a single String, comprising + the NamedEntityObjectIdentity.getClassname() + + ":" + + NamedEntityObjectIdentity.getId(). This yields the + type of object_identity values shown above. As + indicated by the sample data, each database row corresponds to a + single BasicAclEntry. As stated earlier and + demonstrated by corp.DomainObject:2 in the above + sample data, each domain object instance will often have multiple + BasicAclEntry[]s. + + As JdbcDaoImpl is required to return concrete + BasicAclEntry classes, it needs to know which + BasicAclEntry implementation it is to create and + populate. This is the role of the acl_class column. + JdbcDaoImpl will create the indicated class and set + its mask, recipient, + aclObjectIdentity and + aclObjectParentIdentity properties. + + As you can probably tell from the sample data, the + parent_object_identity value can either be null or + in the same format as the object_identity. If + non-null, JdbcDaoImpl will create a + NamedEntityObjectIdentity to place inside the + returned BasicAclEntry class. + + Returning to the BasicAclProvider, before it + can poll the BasicAclDao implementation it needs to + convert the domain object instance it was passed into an + AclObjectIdentity. + BasicAclProvider has a protected + AclObjectIdentity obtainIdentity(Object domainInstance) + 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 + AclObjectIdentityAware interface, which is merely a + getter for an AclObjectIdentity. 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 AclObjectIdentity by + passing the domain object instance to the constructor of a class + defined by the + BasicAclProvider.getDefaultAclObjectIdentity() + method. By default the defined class is + NamedEntityObjectIdentity, which was described in + more detail above. Therefore, you will need to either (i) provide a + getId() method on your domain objects, (ii) + implement AclObjectIdentityAware on your domain + objects, (iii) provide an alternative + AclObjectIdentity implementation that will accept + your domain object in its constructor, or (iv) override the + obtainIdentity(Object) method. + + Once the AclObjectIdentity of the domain + object instance is determined, the BasicAclProvider + will poll the DAO to obtain its BasicAclEntry[]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 + recipient 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: + + --- 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) + + So the above explains how a domain object instance has its + AclObjectIdentity discovered, and the + BasicAclDao will be polled successively until an + array of inherited permissions is constructed for the domain object + instance. The final step is to determine the + BasicAclEntry[]s that are actually applicable to a + given Authentication object. + + As you would recall, the AclManager (and all + delegates, up to and including BasicAclProvider) + provides a method which returns only those + BasicAclEntry[]s applying to a passed + Authentication object. + BasicAclProvider delivers this functionality by + delegating the filtering operation to an + EffectiveAclsResolver implementation. The default + implementation, + GrantedAuthorityEffectiveAclsResolver, will iterate + through the BasicAclEntry[]s and include only those + where the recipient is equal to either the + Authentication's principal or + any of the Authentication's + GrantedAuthority[]s. Please refer to the JavaDocs + for more information. + + + + Conclusion + + 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. + + + Filters @@ -3081,6 +3450,11 @@ $CATALINA_HOME/bin/startup.sh All of the above filters use FilterToBeanProxy, which is discussed in the previous section. + + If you're using SiteMesh, ensure the Acegi Security filters + execute before the SiteMesh filters are called. This enables the + ContextHolder to be populated in time for use by + SiteMesh decorators.