From 5ba40705e80bb7582cb727a3bfcd10996cace77f Mon Sep 17 00:00:00 2001 From: Ben Alex Date: Tue, 23 May 2006 08:48:19 +0000 Subject: [PATCH] SEC-239: Initial commit. Work-in-progress only. --- .../acls/AccessControlEntry.java | 30 + .../main/java/org/acegisecurity/acls/Acl.java | 206 +++++++ .../acls/AclFormattingUtils.java | 118 ++++ .../org/acegisecurity/acls/AclService.java | 75 +++ .../acls/AlreadyExistsException.java | 49 ++ .../acls/AuditableAccessControlEntry.java | 13 + .../org/acegisecurity/acls/AuditableAcl.java | 12 + .../acls/ChildrenExistException.java | 51 ++ .../acls/IdentityUnavailableException.java | 49 ++ .../org/acegisecurity/acls/MutableAcl.java | 61 +++ .../acegisecurity/acls/MutableAclService.java | 77 +++ .../acegisecurity/acls/NotFoundException.java | 49 ++ .../org/acegisecurity/acls/OwnershipAcl.java | 37 ++ .../org/acegisecurity/acls/Permission.java | 70 +++ .../acls/UnloadedSidException.java | 51 ++ .../acls/domain/AccessControlEntryImpl.java | 124 +++++ .../acegisecurity/acls/domain/AclImpl.java | 500 +++++++++++++++++ .../acls/domain/AuditLogger.java | 14 + .../acls/domain/BasePermission.java | 66 +++ .../acls/domain/ConsoleAuditLogger.java | 19 + .../acls/domain/CumulativePermission.java | 84 +++ .../org/acegisecurity/acls/jdbc/AclCache.java | 18 + .../acls/jdbc/BasicLookupStrategy.java | 517 ++++++++++++++++++ .../acls/jdbc/EhCacheBasedAclCache.java | 58 ++ .../acls/jdbc/JdbcAclService.java | 51 ++ .../acls/jdbc/LookupStrategy.java | 47 ++ .../acls/objectidentity/ObjectIdentity.java | 78 +++ .../objectidentity/ObjectIdentityImpl.java | 157 ++++++ .../java/org/acegisecurity/acls/package.html | 5 + .../acls/sid/GrantedAuthoritySid.java | 74 +++ .../acegisecurity/acls/sid/PrincipalSid.java | 81 +++ .../java/org/acegisecurity/acls/sid/Sid.java | 54 ++ .../acls/domain/PermissionTests.java | 41 ++ .../acls/jdbc/DatabaseSeeder.java | 23 + .../acls/jdbc/JdbcAclServiceTests.java | 40 ++ .../acls/jdbc/applicationContext-test.xml | 71 +++ .../org/acegisecurity/acls/jdbc/select.sql | 23 + .../org/acegisecurity/acls/jdbc/testData.sql | 70 +++ 38 files changed, 3163 insertions(+) create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/AccessControlEntry.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/Acl.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/AclFormattingUtils.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/AclService.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/AlreadyExistsException.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/AuditableAccessControlEntry.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/AuditableAcl.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/ChildrenExistException.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/IdentityUnavailableException.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/MutableAcl.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/MutableAclService.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/NotFoundException.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/OwnershipAcl.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/Permission.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/UnloadedSidException.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/domain/AccessControlEntryImpl.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/domain/AclImpl.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/domain/AuditLogger.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/domain/BasePermission.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/domain/ConsoleAuditLogger.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/domain/CumulativePermission.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/jdbc/AclCache.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/jdbc/BasicLookupStrategy.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/jdbc/EhCacheBasedAclCache.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/jdbc/JdbcAclService.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/jdbc/LookupStrategy.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/objectidentity/ObjectIdentity.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/objectidentity/ObjectIdentityImpl.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/package.html create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/sid/GrantedAuthoritySid.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/sid/PrincipalSid.java create mode 100644 sandbox/src/main/java/org/acegisecurity/acls/sid/Sid.java create mode 100644 sandbox/src/test/java/org/acegisecurity/acls/domain/PermissionTests.java create mode 100644 sandbox/src/test/java/org/acegisecurity/acls/jdbc/DatabaseSeeder.java create mode 100644 sandbox/src/test/java/org/acegisecurity/acls/jdbc/JdbcAclServiceTests.java create mode 100644 sandbox/src/test/java/org/acegisecurity/acls/jdbc/applicationContext-test.xml create mode 100644 sandbox/src/test/java/org/acegisecurity/acls/jdbc/select.sql create mode 100644 sandbox/src/test/java/org/acegisecurity/acls/jdbc/testData.sql diff --git a/sandbox/src/main/java/org/acegisecurity/acls/AccessControlEntry.java b/sandbox/src/main/java/org/acegisecurity/acls/AccessControlEntry.java new file mode 100644 index 0000000000..12cc1184d5 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/AccessControlEntry.java @@ -0,0 +1,30 @@ +package org.acegisecurity.acls; + +import java.io.Serializable; + +import org.acegisecurity.acls.sid.Sid; + +/** + * Represents an individual permission assignment within an {@link Acl}. + * + *

+ * Instances MUST be immutable, as they are returned by Acl + * and should not allow client modification. + * + * @author Ben Alex + * @version $Id$ + * + */ +public interface AccessControlEntry { + /** + * Obtains an identifier that represents this ACE. + * + * @return the identifier, or null if unsaved + */ + public Serializable getId(); + + public Acl getAcl(); + public Sid getSid(); + public Permission getPermission(); + public boolean isGranting(); +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/Acl.java b/sandbox/src/main/java/org/acegisecurity/acls/Acl.java new file mode 100644 index 0000000000..30c4b1ef84 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/Acl.java @@ -0,0 +1,206 @@ +/* Copyright 2004, 2005, 2006 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 org.acegisecurity.acls; + +import java.io.Serializable; + +import org.acegisecurity.acls.objectidentity.ObjectIdentity; +import org.acegisecurity.acls.sid.Sid; + + +/** + * Represents an access control list (ACL) for a domain object. + * + *

+ * An Acl represents all ACL entries for a given domain object. In + * order to avoid needing references to the domain object itself, this + * interface handles indirection between a domain object and an ACL object + * identity via the {@link + * org.acegisecurity.acls.objectidentity.ObjectIdentity} interface. + *

+ * + *

+ * An implementation represents the {@link org.acegisecurity.acls.Permission} + * list applicable for some or all {@link org.acegisecurity.acls.sid.Sid} + * instances. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public interface Acl extends Serializable { + //~ Methods ================================================================ + + + /** + * Returns all of the entries represented by the present Acl + * (not parents). + * + *

+ * This method is typically used for administrative purposes. + *

+ * + *

+ * The order that entries appear in the array is unspecified. However, if + * implementations use particular ordering logic in authorization + * decisions, the entries returned by this method MUST be ordered + * in that manner. + *

+ * + *

+ * Do NOT use this method for making authorization decisions. + * Instead use {@link #isGranted(Permission[], Sid[])}. + *

+ * + *

+ * This method must operate correctly even if the Acl only + * represents a subset of Sids. The caller is responsible for + * correctly handling the result if only a subset of Sids is + * represented. + *

+ * + * @return the list of entries represented by the Acl + */ + public AccessControlEntry[] getEntries(); + + /** + * Obtains the domain object this Acl provides entries for. + * This is immutable once an Acl is created. + * + * @return the object identity + */ + public ObjectIdentity getObjectIdentity(); + + /** + * A domain object may have a parent for the purpose of ACL inheritance. If + * there is a parent, its ACL can be accessed via this method. In turn, + * the parent's parent (grandparent) can be accessed and so on. + * + *

+ * This method solely represents the presence of a navigation hierarchy + * between the parent Acl and this Acl. For + * actual inheritance to take place, the {@link #isEntriesInheriting()} + * must also be true. + *

+ * + *

+ * This method must operate correctly even if the Acl only + * represents a subset of Sids. The caller is responsible for + * correctly handling the result if only a subset of Sids is + * represented. + *

+ * + * @return the parent Acl + */ + public Acl getParentAcl(); + + /** + * Indicates whether the ACL entries from the {@link #getParentAcl()} + * should flow down into the current Acl. + * + *

+ * The mere link between an Acl and a parent Acl + * on its own is insufficient to cause ACL entries to inherit down. This + * is because a domain object may wish to have entirely independent + * entries, but maintain the link with the parent for navigation purposes. + * Thus, this method denotes whether or not the navigation relationship + * also extends to the actual inheritence of entries. + *

+ * + * @return true if parent ACL entries inherit into the current + * Acl + */ + public boolean isEntriesInheriting(); + + /** + * This is the actual authorization logic method, and must be used whenever + * ACL authorization decisions are required. + * + *

+ * An array of Sids are presented, representing security + * identifies of the current principal. In addition, an array of + * Permissions is presented which will have one or more bits + * set in order to indicate the permissions needed for an affirmative + * authorization decision. An array is presented because holding + * any of the Permissions inside the array will be + * sufficient for an affirmative authorization. + *

+ * + *

+ * The actual approach used to make authorization decisions is left to the + * implementation and is not specified by this interface. For example, an + * implementation MAY search the current ACL in the order the ACL + * entries have been stored. If a single entry is found that has the same + * active bits as are shown in a passed Permission, that + * entry's grant or deny state may determine the authorization decision. + * If the case of a deny state, the deny decision will only be relevant if + * all other Permissions passed in the array have also been + * unsuccessfully searched. If no entry is found that match the bits in + * the current ACL, provided that {@link #isEntriesInheriting()} is + * true, the authorization decision may be passed to the + * parent ACL. If there is no matching entry, the implementation MAY throw + * an exception, or make a predefined authorization decision. + *

+ * + *

+ * This method must operate correctly even if the Acl only + * represents a subset of Sids. The caller is responsible for + * correctly handling the result if only a subset of Sids is + * represented. + *

+ * + * @param permission the permission or permissions required + * @param sids the security identities held by the principal + * @param administrativeMode if true denotes the query is for + * administrative purposes and no logger or auditing (if supported + * by the implementation) should be undertaken + * + * @return true is authorization is granted + * + * @throws NotFoundException MAY be thrown if an implementation cannot make + * an authoritative authorization decision + * @throws UnloadedSidException thrown if the Acl does not + * have details for one or more of the Sids passed as + * arguments + */ + public boolean isGranted(Permission[] permission, Sid[] sids, + boolean administrativeMode) + throws NotFoundException, UnloadedSidException; + + /** + * For efficiency reasons an Acl may be loaded and + * not contain entries for every Sid in the system. + * If an Acl has been loaded and does not represent every + * Sid, all methods of the Sid can only be used + * within the limited scope of the Sid instances it actually + * represents. + * + *

+ * It is normal to load an Acl for only particular + * Sids if read-only authorization decisions are being made. + * However, if user interface reporting or modification of + * Acls are desired, an Acl should be loaded + * with all Sids. This method denotes whether or not the + * specified Sids have been loaded or not. + *

+ * + * @param sids DOCUMENT ME! + * + * @return true if every passed Sid is + * represented by this Acl instance + */ + public boolean isSidLoaded(Sid[] sids); +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/AclFormattingUtils.java b/sandbox/src/main/java/org/acegisecurity/acls/AclFormattingUtils.java new file mode 100644 index 0000000000..a3aaf09816 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/AclFormattingUtils.java @@ -0,0 +1,118 @@ +/* Copyright 2004, 2005, 2006 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 org.acegisecurity.acls; + +import org.springframework.util.Assert; + + +/** + * Utility methods for displaying ACL information. + * + * @author Ben Alex + * @version $Id$ + */ +public class AclFormattingUtils { + //~ Methods ================================================================ + + public static String demergePatterns(String original, String removeBits) { + Assert.notNull(original, "Original string required"); + Assert.notNull(removeBits, "Bits To Remove string required"); + Assert.isTrue(original.length() == removeBits.length(), + "Original and Bits To Remove strings must be identical length"); + + char[] replacement = new char[original.length()]; + + for (int i = 0; i < original.length(); i++) { + if (removeBits.charAt(i) == Permission.RESERVED_OFF) { + replacement[i] = original.charAt(i); + } else { + replacement[i] = Permission.RESERVED_OFF; + } + } + + return new String(replacement); + } + + public static String mergePatterns(String original, String extraBits) { + Assert.notNull(original, "Original string required"); + Assert.notNull(extraBits, "Extra Bits string required"); + Assert.isTrue(original.length() == extraBits.length(), + "Original and Extra Bits strings must be identical length"); + + char[] replacement = new char[extraBits.length()]; + + for (int i = 0; i < extraBits.length(); i++) { + if (extraBits.charAt(i) == Permission.RESERVED_OFF) { + replacement[i] = original.charAt(i); + } else { + replacement[i] = extraBits.charAt(i); + } + } + + return new String(replacement); + } + + private static String printBinary(int i, char on, char off) { + String s = Integer.toString(i, 2); + String pattern = Permission.THIRTY_TWO_RESERVED_OFF; + String temp2 = pattern.substring(0, pattern.length() - s.length()) + s; + + return temp2.replace('0', off).replace('1', on); + } + + /** + * Returns a representation of the active bits in the presented mask, with + * each active bit being denoted by character "". + * + *

+ * Inactive bits will be denoted by character {@link + * Permission#RESERVED_OFF}. + *

+ * + * @param i the integer bit mask to print the active bits for + * + * @return a 32-character representation of the bit mask + */ + public static String printBinary(int i) { + return AclFormattingUtils.printBinary(i, '*', Permission.RESERVED_OFF); + } + + /** + * Returns a representation of the active bits in the presented mask, with + * each active bit being denoted by the passed character. + * + *

+ * Inactive bits will be denoted by character {@link + * Permission#RESERVED_OFF}. + *

+ * + * @param mask the integer bit mask to print the active bits for + * @param code the character to print when an active bit is detected + * + * @return a 32-character representation of the bit mask + */ + public static String printBinary(int mask, char code) { + Assert.doesNotContain(new Character(code).toString(), + new Character(Permission.RESERVED_ON).toString(), + Permission.RESERVED_ON + " is a reserved character code"); + Assert.doesNotContain(new Character(code).toString(), + new Character(Permission.RESERVED_OFF).toString(), + Permission.RESERVED_OFF + " is a reserved character code"); + + return AclFormattingUtils.printBinary(mask, Permission.RESERVED_ON, + Permission.RESERVED_OFF).replace(Permission.RESERVED_ON, code); + } +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/AclService.java b/sandbox/src/main/java/org/acegisecurity/acls/AclService.java new file mode 100644 index 0000000000..1761f443a9 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/AclService.java @@ -0,0 +1,75 @@ +/* Copyright 2004, 2005, 2006 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 org.acegisecurity.acls; + +import org.acegisecurity.acls.objectidentity.ObjectIdentity; +import org.acegisecurity.acls.sid.Sid; + +import java.util.Map; + + +/** + * Provides retrieval of {@link Acl} instances. + * + * @author Ben Alex + * @version $Id$ + */ +public interface AclService { + //~ Methods ================================================================ + + /** + * Obtains all the Acls that apply for the passed + * Objects. + * + *

+ * The returned map is keyed on the passed objects, with the values being + * the Acl instances. Any unknown objects will not have a map + * key. + *

+ * + * @param objects the objects to find ACL information for + * + * @return a map with zero or more elements (never null) + */ + public Map readAclsById(ObjectIdentity[] objects) throws NotFoundException; + + /** + * Obtains all the Acls that apply for the passed + * Objects, but only for the security identifies passed. + * + *

+ * Implementations MAY provide a subset of the ACLs via this + * method although this is NOT a requirement. This is intended to allow + * performance optimisations within implementations. Callers should + * therefore use this method in preference to the alternative overloaded + * version which does not have performance optimisation opportunities. + *

+ * + *

+ * The returned map is keyed on the passed objects, with the values being + * the Acl instances. Any unknown objects (or objects for + * which the interested Sids do not have entries) will not + * have a map key. + *

+ * + * @param objects the objects to find ACL information for + * @param sids the security identities for which ACL information is + * required (may be null to denote all entries) + * + * @return a map with zero or more elements (never null) + */ + public Map readAclsById(ObjectIdentity[] objects, Sid[] sids) throws NotFoundException; +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/AlreadyExistsException.java b/sandbox/src/main/java/org/acegisecurity/acls/AlreadyExistsException.java new file mode 100644 index 0000000000..dcfde5d7a5 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/AlreadyExistsException.java @@ -0,0 +1,49 @@ +/* Copyright 2004, 2005, 2006 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 org.acegisecurity.acls; + +import org.acegisecurity.AcegiSecurityException; + + +/** + * Thrown if an Acl entry already exists for the object. + * + * @author Ben Alex + * @version $Id$ + */ +public class AlreadyExistsException extends AcegiSecurityException { + //~ Constructors =========================================================== + + /** + * Constructs an AlreadyExistsException with the specified message. + * + * @param msg the detail message + */ + public AlreadyExistsException(String msg) { + super(msg); + } + + /** + * Constructs an AlreadyExistsException with the specified message + * and root cause. + * + * @param msg the detail message + * @param t root cause + */ + public AlreadyExistsException(String msg, Throwable t) { + super(msg, t); + } +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/AuditableAccessControlEntry.java b/sandbox/src/main/java/org/acegisecurity/acls/AuditableAccessControlEntry.java new file mode 100644 index 0000000000..a37f1463a5 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/AuditableAccessControlEntry.java @@ -0,0 +1,13 @@ +package org.acegisecurity.acls; + +/** + * Represents an ACE that provides auditing information. + * + * @author Ben Alex + * @version $Id$ + * + */ +public interface AuditableAccessControlEntry extends AccessControlEntry { + public boolean isAuditSuccess(); + public boolean isAuditFailure(); +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/AuditableAcl.java b/sandbox/src/main/java/org/acegisecurity/acls/AuditableAcl.java new file mode 100644 index 0000000000..78dd668434 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/AuditableAcl.java @@ -0,0 +1,12 @@ +package org.acegisecurity.acls; + +/** + * A mutable ACL that provides audit capabilities. + * + * @author Ben Alex + * @version $Id$ + * + */ +public interface AuditableAcl extends MutableAcl { + public void updateAuditing(Long aceId, boolean auditSuccess, boolean auditFailure); +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/ChildrenExistException.java b/sandbox/src/main/java/org/acegisecurity/acls/ChildrenExistException.java new file mode 100644 index 0000000000..e64366ce55 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/ChildrenExistException.java @@ -0,0 +1,51 @@ +/* Copyright 2004, 2005, 2006 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 org.acegisecurity.acls; + +import org.acegisecurity.AcegiSecurityException; + + +/** + * Thrown if an {@link Acl} cannot be deleted because children + * Acls exist. + * + * @author Ben Alex + * @version $Id$ + */ +public class ChildrenExistException extends AcegiSecurityException { + //~ Constructors =========================================================== + + /** + * Constructs an ChildrenExistException with the specified + * message. + * + * @param msg the detail message + */ + public ChildrenExistException(String msg) { + super(msg); + } + + /** + * Constructs an ChildrenExistException with the specified + * message and root cause. + * + * @param msg the detail message + * @param t root cause + */ + public ChildrenExistException(String msg, Throwable t) { + super(msg, t); + } +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/IdentityUnavailableException.java b/sandbox/src/main/java/org/acegisecurity/acls/IdentityUnavailableException.java new file mode 100644 index 0000000000..3256ccd68e --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/IdentityUnavailableException.java @@ -0,0 +1,49 @@ +/* Copyright 2004, 2005, 2006 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 org.acegisecurity.acls; + +import org.acegisecurity.AcegiSecurityException; + + +/** + * Thrown if an ACL identity could not be extracted from an object. + * + * @author Ben Alex + * @version $Id$ + */ +public class IdentityUnavailableException extends AcegiSecurityException { + //~ Constructors =========================================================== + + /** + * Constructs an IdentityUnavailableException with the specified message. + * + * @param msg the detail message + */ + public IdentityUnavailableException(String msg) { + super(msg); + } + + /** + * Constructs an IdentityUnavailableException with the specified message + * and root cause. + * + * @param msg the detail message + * @param t root cause + */ + public IdentityUnavailableException(String msg, Throwable t) { + super(msg, t); + } +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/MutableAcl.java b/sandbox/src/main/java/org/acegisecurity/acls/MutableAcl.java new file mode 100644 index 0000000000..773ecbc232 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/MutableAcl.java @@ -0,0 +1,61 @@ +/* Copyright 2004, 2005, 2006 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 org.acegisecurity.acls; + +import java.io.Serializable; + +import org.acegisecurity.acls.sid.Sid; + + +/** + * A mutable Acl. + * + *

+ * A mutable ACL must ensure that appropriate security checks are performed + * before allowing access to its methods. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public interface MutableAcl extends Acl { + //~ Methods ================================================================ + + + /** + * Obtains an identifier that represents this MutableAcl. + * + * @return the identifier, or null if unsaved + */ + public Serializable getId(); + + + public void deleteAce(Long aceId) throws NotFoundException ; + + public void insertAce(Long afterAceId, Permission permission, Sid sid, + boolean granting) throws NotFoundException; + + /** + * Changes the parent of this ACL. + * + * @param newParent the new parent + */ + public void setParent(MutableAcl newParent); + + public void updateAce(Long aceId, Permission permission) throws NotFoundException; + + public void setEntriesInheriting(boolean entriesInheriting); +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/MutableAclService.java b/sandbox/src/main/java/org/acegisecurity/acls/MutableAclService.java new file mode 100644 index 0000000000..64676cbc2f --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/MutableAclService.java @@ -0,0 +1,77 @@ +/* Copyright 2004, 2005, 2006 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 org.acegisecurity.acls; + +import org.acegisecurity.acls.objectidentity.ObjectIdentity; + +/** + * Provides support for creating and storing Acl instances. + * + * @author Ben Alex + * @version $Id$ + */ +public interface MutableAclService extends AclService { + //~ Methods ================================================================ + + /** + * Creates an empty Acl object in the database. It will have + * no entries. The returned object will then be used to add entries. + * + * @param object the object identity to create + * + * @return an ACL object with its ID set + * + * @throws AlreadyExistsException if the passed object identity already has + * a record + */ + public MutableAcl createAcl(ObjectIdentity object) + throws AlreadyExistsException; + + /** + * Removes the specified entry from the database. + * + * @param object the object identity to remove + * @param deleteChildren whether to cascade the delete to children + * + * @throws ChildrenExistException if the deleteChildren argument was + * false but children exist + */ + public void deleteAcl(ObjectIdentity object, boolean deleteChildren) + throws ChildrenExistException; + + /** + * Locates all object identities that use the specified parent. This is + * useful for administration tools, and before issuing a {@link + * #deleteAcl(ObjectIdentity, boolean)}. + * + * @param parentIdentity to locate children of + * + * @return the children (or null if none were found) + */ + public ObjectIdentity[] findChildren(ObjectIdentity parentIdentity); + + /** + * Changes an existing Acl in the database. + * + * @param acl to modify + * + * @throws NotFoundException if the relevant record could not be found (did + * you remember to use {@link #createAcl(ObjectIdentity)} to + * create the object, rather than creating it with the + * new keyword?) + */ + public void updateAcl(MutableAcl acl) throws NotFoundException; +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/NotFoundException.java b/sandbox/src/main/java/org/acegisecurity/acls/NotFoundException.java new file mode 100644 index 0000000000..6309d284cf --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/NotFoundException.java @@ -0,0 +1,49 @@ +/* Copyright 2004, 2005, 2006 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 org.acegisecurity.acls; + +import org.acegisecurity.AcegiSecurityException; + + +/** + * Thrown if an ACL-related object cannot be found. + * + * @author Ben Alex + * @version $Id$ + */ +public class NotFoundException extends AcegiSecurityException { + //~ Constructors =========================================================== + + /** + * Constructs an NotFoundException with the specified message. + * + * @param msg the detail message + */ + public NotFoundException(String msg) { + super(msg); + } + + /** + * Constructs an NotFoundException with the specified message + * and root cause. + * + * @param msg the detail message + * @param t root cause + */ + public NotFoundException(String msg, Throwable t) { + super(msg, t); + } +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/OwnershipAcl.java b/sandbox/src/main/java/org/acegisecurity/acls/OwnershipAcl.java new file mode 100644 index 0000000000..239d02f1d0 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/OwnershipAcl.java @@ -0,0 +1,37 @@ +/* Copyright 2004, 2005, 2006 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 org.acegisecurity.acls; + +import org.acegisecurity.acls.sid.Sid; + +/** + * A mutable ACL that provides ownership capabilities. + * + *

+ * Generally the owner of an ACL is able to call any ACL mutator method, as + * well as assign a new owner. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public interface OwnershipAcl extends MutableAcl { + //~ Methods ================================================================ + + public Sid getOwner(); + + public void setOwner(Sid newOwner); +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/Permission.java b/sandbox/src/main/java/org/acegisecurity/acls/Permission.java new file mode 100644 index 0000000000..f4d84511f0 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/Permission.java @@ -0,0 +1,70 @@ +/* Copyright 2004, 2005, 2006 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 org.acegisecurity.acls; + +import org.acegisecurity.acls.sid.Sid; + + +/** + * Represents a permission granted to a {@link Sid} for a given domain object. + * + * @author Ben Alex + * @version $Id$ + */ +public interface Permission { + //~ Static fields/initializers ============================================= + + public static final char RESERVED_ON = '~'; + public static final char RESERVED_OFF = '.'; + public static final String THIRTY_TWO_RESERVED_OFF = "................................"; + + //~ Methods ================================================================ + + /** + * Returns the bits that represents the permission. + * + * @return the bits that represent the permission + */ + public int getMask(); + + /** + * Returns a 32-character long bit pattern String representing + * this permission. + * + *

+ * Implementations are free to format the pattern as they see fit, although + * under no circumstances may {@link #RESERVED_OFF} or {@link + * #RESERVED_ON} be used within the pattern. An exemption is in the case + * of {@link #RESERVED_OFF} which is used to denote a bit that is off + * (clear). Implementations may also elect to use {@link #RESERVED_ON} + * internally for computation purposes, although this method may not + * return any String containing {@link #RESERVED_ON}. + *

+ * + *

+ * The returned String must be 32 characters in length. + *

+ * + *

+ * This method is only used for user interface and logging purposes. It is + * not used in any permission calculations. Therefore, duplication of + * characters within the output is permitted. + *

+ * + * @return a 32-character bit pattern + */ + public String getPattern(); +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/UnloadedSidException.java b/sandbox/src/main/java/org/acegisecurity/acls/UnloadedSidException.java new file mode 100644 index 0000000000..1228cb98f6 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/UnloadedSidException.java @@ -0,0 +1,51 @@ +/* Copyright 2004, 2005, 2006 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 org.acegisecurity.acls; + +import org.acegisecurity.AcegiSecurityException; + + +/** + * Thrown if an {@link Acl} cannot perform an operation because it only + * loaded a subset of Sids and the caller has requested details + * for an unloaded Sid. + * + * @author Ben Alex + * @version $Id$ + */ +public class UnloadedSidException extends AcegiSecurityException { + //~ Constructors =========================================================== + + /** + * Constructs an NotFoundException with the specified message. + * + * @param msg the detail message + */ + public UnloadedSidException(String msg) { + super(msg); + } + + /** + * Constructs an NotFoundException with the specified message + * and root cause. + * + * @param msg the detail message + * @param t root cause + */ + public UnloadedSidException(String msg, Throwable t) { + super(msg, t); + } +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/domain/AccessControlEntryImpl.java b/sandbox/src/main/java/org/acegisecurity/acls/domain/AccessControlEntryImpl.java new file mode 100644 index 0000000000..4f1c89f0ad --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/domain/AccessControlEntryImpl.java @@ -0,0 +1,124 @@ +package org.acegisecurity.acls.domain; + +import java.io.Serializable; + +import org.acegisecurity.acls.AccessControlEntry; +import org.acegisecurity.acls.Acl; +import org.acegisecurity.acls.AuditableAccessControlEntry; +import org.acegisecurity.acls.Permission; +import org.acegisecurity.acls.sid.Sid; +import org.springframework.util.Assert; + +/** + * An immutable default implementation of AccessControlEntry. + * + * @author Ben Alex + * @version $Id$ + */ +public class AccessControlEntryImpl implements AccessControlEntry, AuditableAccessControlEntry { + private Serializable id; + private Acl acl; + private Sid sid; + private Permission permission; + private boolean granting; + private boolean auditSuccess = false; + private boolean auditFailure = false; + private boolean aceDirty = false; + + public void clearDirtyFlags() { + this.aceDirty = false; + } + + public boolean equals(Object arg0) { + if (!(arg0 instanceof AccessControlEntryImpl)) { + return false; + } + AccessControlEntryImpl rhs = (AccessControlEntryImpl) arg0; + if (this.aceDirty != rhs.isAceDirty() || + this.auditFailure != rhs.isAuditFailure() || + this.auditSuccess != rhs.isAuditSuccess() || + this.granting != rhs.isGranting() || + !this.acl.equals(rhs.getAcl()) || + !this.id.equals(rhs.getId()) || + !this.permission.equals(rhs.getPermission()) || + !this.sid.equals(rhs.getSid()) ) { + return false; + } + return true; + } + + + + public AccessControlEntryImpl(Serializable id, Acl acl, Sid sid, Permission permission, boolean granting, boolean auditSuccess, boolean auditFailure) { + Assert.notNull(acl, "Acl required"); + Assert.notNull(sid, "Sid required"); + Assert.notNull(permission, "Permission required"); + this.id = id; + this.acl = acl; // can be null + this.sid = sid; + this.permission = permission; + this.granting = granting; + this.auditSuccess = auditSuccess; + this.auditFailure = auditFailure; + } + + public Acl getAcl() { + return acl; + } + public boolean isGranting() { + return granting; + } + public Serializable getId() { + return id; + } + public Permission getPermission() { + return permission; + } + public Sid getSid() { + return sid; + } + + void setPermission(Permission permission) { + Assert.notNull(permission, "Permission required"); + this.permission = permission; + this.aceDirty = true; + } + + void setId(Serializable id) { + this.id = id; + } + + public boolean isAuditFailure() { + return auditFailure; + } + + void setAuditFailure(boolean auditFailure) { + this.auditFailure = auditFailure; + this.aceDirty = true; + } + + public boolean isAuditSuccess() { + return auditSuccess; + } + + void setAuditSuccess(boolean auditSuccess) { + this.auditSuccess = auditSuccess; + this.aceDirty = true; + } + + public boolean isAceDirty() { + return aceDirty; + } + + + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("AccessControlEntryImpl["); + sb.append("id: ").append(this.id).append("; "); + sb.append("granting: ").append(this.granting).append("; "); + sb.append("sid: ").append(this.sid).append("; "); + sb.append("permission: ").append(this.permission); + sb.append("]"); + return sb.toString(); + } +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/domain/AclImpl.java b/sandbox/src/main/java/org/acegisecurity/acls/domain/AclImpl.java new file mode 100644 index 0000000000..bde9b465d0 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/domain/AclImpl.java @@ -0,0 +1,500 @@ +/* Copyright 2004, 2005, 2006 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 org.acegisecurity.acls.domain; + +import java.io.Serializable; +import java.util.Iterator; +import java.util.List; +import java.util.Vector; + +import org.acegisecurity.AccessDeniedException; +import org.acegisecurity.Authentication; +import org.acegisecurity.GrantedAuthority; +import org.acegisecurity.acls.AccessControlEntry; +import org.acegisecurity.acls.Acl; +import org.acegisecurity.acls.AuditableAcl; +import org.acegisecurity.acls.MutableAcl; +import org.acegisecurity.acls.NotFoundException; +import org.acegisecurity.acls.OwnershipAcl; +import org.acegisecurity.acls.Permission; +import org.acegisecurity.acls.UnloadedSidException; +import org.acegisecurity.acls.objectidentity.ObjectIdentity; +import org.acegisecurity.acls.sid.PrincipalSid; +import org.acegisecurity.acls.sid.Sid; +import org.acegisecurity.context.SecurityContextHolder; +import org.springframework.util.Assert; + + +/** + * Base implementation of Acl. + * + * @author Ben Alex + * @version $Id + */ +public class AclImpl implements Acl, MutableAcl, AuditableAcl, OwnershipAcl { + //~ Instance fields ======================================================== + private static final int CHANGE_OWNERSHIP = 0; + private static final int CHANGE_AUDITING = 1; + private static final int CHANGE_GENERAL = 2; + + private GrantedAuthority gaTakeOwnership; + private GrantedAuthority gaModifyAuditing; + private GrantedAuthority gaGeneralChanges; + + private Acl parentAcl; + private AuditLogger auditLogger = new ConsoleAuditLogger(); // AuditableAcl + private List aces = new Vector(); + private List deletedAces = new Vector(); + private Long id; + private ObjectIdentity objectIdentity; + private Sid owner; // OwnershipAcl + private boolean entriesInheriting = false; + private Sid[] loadedSids = null; // includes all SIDs the WHERE clause covered, even if there was no ACE for a SID + private boolean aclDirty = false; // for snapshot detection + private boolean addedAces = false; // for snapshot detection + private boolean updatedAces = false; // for snapshot detection + + //~ Constructors =========================================================== + + /** + * Minimal constructor, which should be used {@link + * org.acegisecurity.acls.MutableAclService#createAcl(ObjectIdentity)}. + * + * @param objectIdentity the object identity this ACL relates to (required) + * @param id the primary key assigned to this ACL (required) + * @param auths an array of GrantedAuthoritys that have + * special permissions (index 0 is the authority needed to change + * ownership, index 1 is the authority needed to modify auditing details, + * index 2 is the authority needed to change other ACL and ACE details) (required) + */ + public AclImpl(ObjectIdentity objectIdentity, Long id, GrantedAuthority[] auths) { + Assert.notNull(objectIdentity, "Object Identity required"); + Assert.notNull(id, "Id required"); + this.objectIdentity = objectIdentity; + this.id = id; + this.setAuthorities(auths); + } + + /** + * Change the special adminstrative permissions honoured by this + * object. + * + *

+ * Normally a principal must be the owner of the ACL in order to + * make most changes. The authorities passed to this method provide + * a way for non-owners to modify the ACL (and indeed modify audit + * parameters, which are not available to ACL owners). + * + * @param auths an array of GrantedAuthoritys that have + * administrative permissions (index 0 is the authority needed to change + * ownership, index 1 is the authority needed to modify auditing details, + * index 2 is the authority needed to change other ACL and ACE details) + */ + private void setAuthorities(GrantedAuthority[] auths) { + Assert.notEmpty(auths, "GrantedAuthority[] with three elements required"); + Assert.isTrue(auths.length == 3, "GrantedAuthority[] with three elements required"); + this.gaTakeOwnership = auths[0]; + this.gaModifyAuditing = auths[1]; + this.gaGeneralChanges = auths[2]; + } + + /** + * Full constructor, which should be used by persistence tools that do not + * provide field-level access features. + * + * @param objectIdentity the object identity this ACL relates to (required) + * @param id the primary key assigned to this ACL (required) + * @param auths an array of GrantedAuthoritys that have + * special permissions (index 0 is the authority needed to change + * ownership, index 1 is the authority needed to modify auditing details, + * index 2 is the authority needed to change other ACL and ACE details) (required) + * @param parentAcl the parent (may be null) + * @param loadedSids the loaded SIDs if only a subset were loaded (may be + * null) + * @param entriesInheriting if ACEs from the parent should inherit into + * this ACL + * @param owner the owner (required) + */ + public AclImpl(ObjectIdentity objectIdentity, Long id, Acl parentAcl, GrantedAuthority[] auths, + Sid[] loadedSids, boolean entriesInheriting, Sid owner) { + Assert.notNull(objectIdentity, "Object Identity required"); + Assert.notNull(id, "Id required"); + Assert.notNull(owner, "Owner required"); + this.objectIdentity = objectIdentity; + this.id = id; + setAuthorities(auths); + this.parentAcl = parentAcl; // may be null + this.loadedSids = loadedSids; // may be null + this.entriesInheriting = entriesInheriting; + this.owner = owner; + } + + /** + * Private no-argument constructor for use by reflection-based persistence + * tools along with field-level access. + */ + private AclImpl() {} + + //~ Methods ================================================================ + + protected void securityCheck(int changeType) { + if (SecurityContextHolder.getContext() == null || SecurityContextHolder.getContext().getAuthentication() == null || !SecurityContextHolder.getContext().getAuthentication().isAuthenticated()) { + throw new AccessDeniedException("Authenticated principal required to operate with ACLs"); + } + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + // Check if authorized by virtue of ACL ownership + Sid currentUser = new PrincipalSid(authentication); + if (currentUser.equals(this.owner) && (changeType == CHANGE_GENERAL || changeType == CHANGE_OWNERSHIP)) { + return; + } + + // Not authorized by ACL ownership; try via adminstrative permissions + GrantedAuthority requiredAuthority = null; + if (changeType == CHANGE_AUDITING) { + requiredAuthority = this.gaModifyAuditing; + } else if (changeType == CHANGE_GENERAL) { + requiredAuthority = this.gaGeneralChanges; + } else if (changeType == CHANGE_OWNERSHIP) { + requiredAuthority = this.gaTakeOwnership; + } else { + throw new IllegalArgumentException("Unknown change type"); + } + + // Iterate this principal's authorities to determine right + GrantedAuthority[] auths = authentication.getAuthorities(); + for (int i = 0; i < auths.length; i++) { + if (requiredAuthority.equals(auths[i])) { + return; + } + } + + throw new AccessDeniedException("Principal does not have required ACL permissions to perform requested operation"); + } + + public void deleteAce(Long aceId) throws NotFoundException { + securityCheck(CHANGE_GENERAL); + + synchronized (aces) { + int offset = findAceOffset(aceId); + + if (offset == 1) { + throw new NotFoundException("Requested ACE ID not found"); + } + + aces.remove(offset); + deletedAces.add(aceId); + } + } + + private int findAceOffset(Long aceId) { + Assert.notNull(aceId, "ACE ID is required"); + + synchronized (aces) { + for (int i = 0; i < aces.size(); i++) { + AccessControlEntry ace = (AccessControlEntry) aces.get(i); + + if (ace.getId().equals(aceId)) { + return i; + } + } + } + + return -1; + } + + public AccessControlEntry[] getEntries() { + // Can safely return AccessControlEntry directly, as they're immutable + return (AccessControlEntry[]) aces.toArray(new AccessControlEntry[] {}); + } + + public void setEntriesInheriting(boolean entriesInheriting) { + securityCheck(CHANGE_GENERAL); + this.entriesInheriting = entriesInheriting; + this.aclDirty = true; + } + + public Serializable getId() { + return this.id; + } + + public ObjectIdentity getObjectIdentity() { + return objectIdentity; + } + + public Sid getOwner() { + return this.owner; + } + + public Acl getParentAcl() { + return parentAcl; + } + + public void insertAce(Long afterAceId, Permission permission, Sid sid, + boolean granting) throws NotFoundException { + securityCheck(CHANGE_GENERAL); + Assert.notNull(permission, "Permission required"); + Assert.notNull(sid, "Sid required"); + + AccessControlEntryImpl ace = new AccessControlEntryImpl(null, this, + sid, permission, granting, false, false); + + synchronized (aces) { + if (afterAceId != null) { + int offset = findAceOffset(afterAceId); + + if (offset == -1) { + throw new NotFoundException("Requested ACE ID not found"); + } + + aces.add(offset + 1, ace); + } else { + aces.add(ace); + } + + } + + this.addedAces = true; + } + + public boolean isSidLoaded(Sid[] sids) { + // If loadedSides is null, this indicates all SIDs were loaded + // Also return true if the caller didn't specify a SID to find + if (this.loadedSids == null || sids == null || sids.length == 0) { + return true; + } + + // This ACL applies to a SID subset. Iterate to check it applies + for (int i = 0; i < sids.length; i++) { + boolean found = false; + for (int y = 0; y < this.loadedSids.length; y++) { + if (sids[i].equals(this.loadedSids[y])) { + // this SID is OK + found = true; + break; // out of loadedSids for loop + } + } + if (!found) { + return false; + } + } + return true; + } + + public boolean isEntriesInheriting() { + return entriesInheriting; + } + + /** + * Determines authorization. The order of the permission and + * sid arguments is extremely important! The method + * will iterate through each of the permissions in the order + * specified. For each iteration, all of the sids will be + * considered, again in the order they are presented. The iteration of + * each permission:sid combination will then inspect the ACEs + * in the order they appear in the ACL. When the first full match + * is found (ie an ACE that has the SID currently being searched for and + * the exact permission bit mask being search for), the grant or deny flag + * for that ACE will prevail. If the ACE specifies to grant access, the + * method will return true. If the ACE specifies to deny + * access, the loop will stop and the next permission + * iteration will be performed. If each permission indicates to deny + * access, the first deny ACE found will be considered the reason for the + * failure (as it was the first match found, and is therefore the one most + * logically requiring changes - although not always). If absolutely no + * matching ACE was found at all for any permission, the parent ACL will + * be tried (provided that there is a parent and {@link + * #isEntriesInheriting()} is true. The parent ACL will also + * scan its parent and so on. If ultimately no matching ACE is found, a + * NotFoundException will be thrown and the caller will need + * to decide how to handle the permission check. Similarly, if any of the + * passed SIDs were not loaded by the ACL, the + * UnloadedSidException will be thrown. + * + * @param permission the exact permissions to scan for (order is important) + * @param sids the exact SIDs to scan for (order is important) + * @param administrativeMode if true denotes the query is for + * administrative purposes and no auditing will be undertaken + * + * @return true if one of the permissions has been granted, + * false if one of the permissions has been + * specifically revoked + * + * @throws NotFoundException if an exact ACE for one of the permission bit + * masks and SID combination could not be found + * @throws UnloadedSidException if the passed SIDs are unknown to this ACL + * because the ACL was only loaded for a subset of SIDs + */ + public boolean isGranted(Permission[] permission, Sid[] sids, boolean administrativeMode) + throws NotFoundException, UnloadedSidException { + Assert.notEmpty(permission, "Permissions required"); + Assert.notEmpty(sids, "SIDs required"); + + if (!this.isSidLoaded(sids)) { + throw new UnloadedSidException("ACL was not loaded for one or more SID"); + } + + AccessControlEntry firstRejection = null; + + for (int i = 0; i < permission.length; i++) { + for (int x = 0; x < sids.length; x++) { + // Attempt to find exact match for this permission mask and SID + Iterator acesIterator = aces.iterator(); + boolean scanNextSid = true; + + while (acesIterator.hasNext()) { + AccessControlEntry ace = (AccessControlEntry) acesIterator + .next(); + + if ((ace.getPermission().getMask() == permission[i].getMask()) + && ace.getSid().equals(sids[x])) { + // Found a matching ACE, so its authorization decision will prevail + if (ace.isGranting()) { + // Success + if (!administrativeMode) { + auditLogger.logIfNeeded(true, ace); + } + + return true; + } else { + // Failure for this permission, so stop search + // We will see if they have a different permission + // (this permission is 100% rejected for this SID) + if (firstRejection == null) { + // Store first rejection for auditing reasons + firstRejection = ace; + } + + scanNextSid = false; // helps break the loop + + break; // exit "aceIterator" while loop + } + } + } + + if (!scanNextSid) { + break; // exit SID for loop (now try next permission) + } + } + } + + if (firstRejection != null) { + // We found an ACE to reject the request at this point, as no + // other ACEs were found that granted a different permission + + if (!administrativeMode) { + auditLogger.logIfNeeded(false, firstRejection); + } + + return false; + } + + // No matches have been found so far + if (isEntriesInheriting() && (parentAcl != null)) { + // We have a parent, so let them try to find a matching ACE + return parentAcl.isGranted(permission, sids, false); + } else { + // We either have no parent, or we're the uppermost parent + throw new NotFoundException( + "Unable to locate a matching ACE for passed permissions and SIDs"); + } + } + + public void setOwner(Sid newOwner) { + securityCheck(CHANGE_OWNERSHIP); + Assert.notNull(newOwner, "Owner required"); + this.owner = newOwner; + this.aclDirty = true; + } + + public void setParent(MutableAcl newParent) { + securityCheck(CHANGE_GENERAL); + Assert.notNull(newParent, "New Parent required"); + this.parentAcl = newParent; + this.aclDirty = true; + } + + public void updateAce(Long aceId, Permission permission) + throws NotFoundException { + securityCheck(CHANGE_GENERAL); + synchronized (aces) { + int offset = findAceOffset(aceId); + + if (offset == 1) { + throw new NotFoundException("Requested ACE ID not found"); + } + + AccessControlEntryImpl ace = (AccessControlEntryImpl) aces.get(offset); + ace.setPermission(permission); + } + + this.updatedAces = true; + } + + public void updateAuditing(Long aceId, boolean auditSuccess, + boolean auditFailure) { + securityCheck(CHANGE_AUDITING); + + synchronized (aces) { + int offset = findAceOffset(aceId); + + if (offset == 1) { + throw new NotFoundException("Requested ACE ID not found"); + } + + AccessControlEntryImpl ace = (AccessControlEntryImpl) aces.get(offset); + ace.setAuditSuccess(auditSuccess); + ace.setAuditFailure(auditFailure); + } + this.updatedAces = true; + } + + /** + * Clears the dirty flags on the Acl, but not any + * associated ACEs. + */ + public void clearDirtyFlags() { + this.aclDirty = false; + this.addedAces = false; + this.updatedAces = false; + } + + public boolean isAclDirty() { + return aclDirty; + } + + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("AclImpl["); + sb.append("id: ").append(this.id).append("; "); + sb.append("objectIdentity: ").append(this.objectIdentity).append("; "); + sb.append("owner: ").append(this.owner).append("; "); + Iterator iterator = this.aces.iterator(); + int count = 0; + while (iterator.hasNext()) { + count++; + if (count == 1) { + sb.append("\r\n"); + } + sb.append(iterator.next().toString()).append("\r\n"); + } + sb.append("inheriting: ").append(this.entriesInheriting).append("; "); + sb.append("parent: ").append(this.parentAcl == null ? "Null" : this.parentAcl.getObjectIdentity()); + sb.append("]"); + return sb.toString(); + } + +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/domain/AuditLogger.java b/sandbox/src/main/java/org/acegisecurity/acls/domain/AuditLogger.java new file mode 100644 index 0000000000..538816fc5c --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/domain/AuditLogger.java @@ -0,0 +1,14 @@ +package org.acegisecurity.acls.domain; + +import org.acegisecurity.acls.AccessControlEntry; + +/** + * Used by AclImpl to log audit events. + * + * @author Ben Alex + * @version $Id$ + * + */ +public interface AuditLogger { + public void logIfNeeded(boolean granted, AccessControlEntry ace); +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/domain/BasePermission.java b/sandbox/src/main/java/org/acegisecurity/acls/domain/BasePermission.java new file mode 100644 index 0000000000..143a513e98 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/domain/BasePermission.java @@ -0,0 +1,66 @@ +package org.acegisecurity.acls.domain; + +import org.acegisecurity.acls.AclFormattingUtils; +import org.acegisecurity.acls.Permission; + +public class BasePermission implements Permission { + public static final Permission READ = new BasePermission(1<<0, 'R'); // 1 + public static final Permission WRITE = new BasePermission(1<<1, 'W'); // 2 + public static final Permission CREATE = new BasePermission(1<<2, 'C'); // 4 + public static final Permission ADMINISTRATION = new BasePermission(1<<3, 'A'); // 8 + + private int mask; + private char code; + + private BasePermission(int mask, char code) { + this.mask = mask; + this.code = code; + } + + public boolean equals(Object arg0) { + if (!(arg0 instanceof BasePermission)) { + return false; + } + BasePermission rhs = (BasePermission) arg0; + return (this.mask == rhs.getMask()); + } + + /** + * Dynamically creates a CumulativePermission + * representing the active bits in the passed mask. + * NB: Only uses BasePermission! + * + * @param mask to review + */ + public static Permission buildFromMask(int mask) { + CumulativePermission permission = new CumulativePermission(); + + // TODO: Write the rest of it to iterate through the 32 bits and instantiate BasePermissions + if (mask == 1) { + permission.set(READ); + } + if (mask == 2) { + permission.set(WRITE); + } + if (mask == 4) { + permission.set(CREATE); + } + if (mask == 8) { + permission.set(ADMINISTRATION); + } + return permission; + } + + public int getMask() { + return mask; + } + + public String toString() { + return "BasePermission[" + getPattern() + "=" + mask + "]"; + } + + public String getPattern() { + return AclFormattingUtils.printBinary(mask, code); + } + +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/domain/ConsoleAuditLogger.java b/sandbox/src/main/java/org/acegisecurity/acls/domain/ConsoleAuditLogger.java new file mode 100644 index 0000000000..fe44081fc6 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/domain/ConsoleAuditLogger.java @@ -0,0 +1,19 @@ +package org.acegisecurity.acls.domain; + +import org.acegisecurity.acls.AccessControlEntry; +import org.acegisecurity.acls.AuditableAccessControlEntry; +import org.springframework.util.Assert; + +public class ConsoleAuditLogger implements AuditLogger { + public void logIfNeeded(boolean granted, AccessControlEntry ace) { + Assert.notNull(ace, "AccessControlEntry required"); + if (ace instanceof AuditableAccessControlEntry) { + AuditableAccessControlEntry auditableAce = (AuditableAccessControlEntry) ace; + if (granted && auditableAce.isAuditSuccess()) { + System.out.println("GRANTED due to ACE: " + ace); + } else if (!granted && auditableAce.isAuditFailure()) { + System.out.println("DENIED due to ACE: " + ace); + } + } + } +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/domain/CumulativePermission.java b/sandbox/src/main/java/org/acegisecurity/acls/domain/CumulativePermission.java new file mode 100644 index 0000000000..bcdb0f7b96 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/domain/CumulativePermission.java @@ -0,0 +1,84 @@ +/* Copyright 2004, 2005, 2006 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 org.acegisecurity.acls.domain; + +import org.acegisecurity.acls.AclFormattingUtils; +import org.acegisecurity.acls.Permission; + + +/** + * Represents a Permission that is constructed at runtime from + * other permissions. + * + *

+ * Methods return this, in order to facilitate method chaining. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public class CumulativePermission implements Permission { + //~ Instance fields ======================================================== + + private String pattern = THIRTY_TWO_RESERVED_OFF; + private int mask = 0; + + //~ Methods ================================================================ + + public CumulativePermission clear(Permission permission) { + this.mask &= ~permission.getMask(); + this.pattern = AclFormattingUtils.demergePatterns(this.pattern, + permission.getPattern()); + + return this; + } + + public boolean equals(Object arg0) { + if (!(arg0 instanceof CumulativePermission)) { + return false; + } + CumulativePermission rhs = (CumulativePermission) arg0; + return (this.mask == rhs.getMask()); + } + + + public CumulativePermission clear() { + this.mask = 0; + this.pattern = THIRTY_TWO_RESERVED_OFF; + + return this; + } + + public int getMask() { + return this.mask; + } + + public String getPattern() { + return this.pattern; + } + + public CumulativePermission set(Permission permission) { + this.mask |= permission.getMask(); + this.pattern = AclFormattingUtils.mergePatterns(this.pattern, + permission.getPattern()); + + return this; + } + + public String toString() { + return "CumulativePermission[" + pattern + "=" + this.mask + "]"; + } +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/jdbc/AclCache.java b/sandbox/src/main/java/org/acegisecurity/acls/jdbc/AclCache.java new file mode 100644 index 0000000000..63fb694a5a --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/jdbc/AclCache.java @@ -0,0 +1,18 @@ +package org.acegisecurity.acls.jdbc; + +import org.acegisecurity.acls.domain.AclImpl; +import org.acegisecurity.acls.objectidentity.ObjectIdentity; + +/** + * A caching layer for {@link JdbcAclService}. + * + * @author Ben Alex + * @version $Id$ + * + */ +public interface AclCache { + public AclImpl getFromCache(ObjectIdentity objectIdentity); + public AclImpl getFromCache(Long pk); + public void putInCache(AclImpl acl); // should walk tree as well! + public void evictFromCache(Long pk); +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/jdbc/BasicLookupStrategy.java b/sandbox/src/main/java/org/acegisecurity/acls/jdbc/BasicLookupStrategy.java new file mode 100644 index 0000000000..175c2f4be2 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/jdbc/BasicLookupStrategy.java @@ -0,0 +1,517 @@ +/* Copyright 2004, 2005, 2006 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 org.acegisecurity.acls.jdbc; + +import java.lang.reflect.Field; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.sql.DataSource; + +import org.acegisecurity.GrantedAuthority; +import org.acegisecurity.acls.AccessControlEntry; +import org.acegisecurity.acls.Acl; +import org.acegisecurity.acls.NotFoundException; +import org.acegisecurity.acls.Permission; +import org.acegisecurity.acls.UnloadedSidException; +import org.acegisecurity.acls.domain.AccessControlEntryImpl; +import org.acegisecurity.acls.domain.AclImpl; +import org.acegisecurity.acls.domain.BasePermission; +import org.acegisecurity.acls.objectidentity.ObjectIdentity; +import org.acegisecurity.acls.objectidentity.ObjectIdentityImpl; +import org.acegisecurity.acls.sid.GrantedAuthoritySid; +import org.acegisecurity.acls.sid.PrincipalSid; +import org.acegisecurity.acls.sid.Sid; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.PreparedStatementSetter; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.util.Assert; + + +/** + * Performs lookups in a manner that is compatible with ANSI SQL. + * + *

+ * NB: This implementation does attempt to provide reasonably optimised lookups + * - within the constraints of a normalised database and standard ANSI SQL + * features. If you are willing to sacrifice either of these constraints (eg + * use a particular database feature such as hierarchical queries or + * materalized views, or reduce normalisation) you are likely to achieve better + * performance. In such situations you will need to provide your own custom + * LookupStrategy. This class does not support subclassing, as + * it is likely to change in future releases and therefore subclassing is + * unsupported. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public final class BasicLookupStrategy implements LookupStrategy { + + private int batchSize = 50; + private AclCache aclCache; + private JdbcTemplate jdbcTemplate; + private GrantedAuthority[] auths; + + //~ Constructors =========================================================== + + /** + * Constructor accepting mandatory arguments + * + * @param dataSource to access the database + * @param aclCache the cache where fully-loaded elements can be stored + * @param auths as per the format defined by {@link + * AclImpl#setAuthorities(GrantedAuthority[])} for instances + * created by this implementation + */ + public BasicLookupStrategy(DataSource dataSource, AclCache aclCache, + GrantedAuthority[] auths) { + Assert.notNull(dataSource, "DataSource required"); + Assert.notNull(aclCache, "AclCache required"); + Assert.notEmpty(auths, "GrantedAuthority[] with three elements required"); + Assert.isTrue(auths.length == 3, + "GrantedAuthority[] with three elements required"); + this.jdbcTemplate = new JdbcTemplate(dataSource); + this.aclCache = aclCache; + this.auths = auths; + } + + //~ Methods ================================================================ + + public void setBatchSize(int batchSize) { + this.batchSize = batchSize; + } + + /** + * The main method. + * + *

+ * WARNING: This implementation completely disregards the "sids" argument! + * Every item in the cache is expected to contain all SIDs. + * + *

+ * The implementation works in batch sizes specfied by {@link #batchSize}. + * + */ + public Map readAclsById(ObjectIdentity[] objects, Sid[] sids) + throws NotFoundException { + Assert.isTrue(batchSize >= 1, "BatchSize must be >= 1"); + Assert.notEmpty(objects, "Objects to lookup required"); + + Map result = new HashMap(); // contains FULLY loaded Acl objects + + Set currentBatchToLoad = new HashSet(); // contains ObjectIdentitys + + for (int i = 0; i < objects.length; i++) { + // Check we don't already have this ACL in the results + if (result.containsKey(objects[i])) { + continue; // already in results, so move to next element + } + + // Check cache for the present ACL entry + Acl acl = aclCache.getFromCache(objects[i]); + + // Ensure any cached element supports all the requested SIDs + // (they should always, as our base impl doesn't filter on SID) + if (acl != null) { + if (acl.isSidLoaded(sids)) { + result.put(acl.getObjectIdentity(), acl); + continue; // now in results, so move to next element + } else { + throw new IllegalStateException( + "Error: SID-filtered element detected when implementation does not perform SID filtering - have you added something to the cache manually?"); + } + } + + // To get this far, we have no choice but to retrieve it via JDBC + // (although we don't do it until we get a batch of them to load) + currentBatchToLoad.add(objects[i]); + + // Is it time to load from JDBC the currentBatchToLoad? + if ((currentBatchToLoad.size() == this.batchSize) || (i+1 == objects.length)) { + Map loadedBatch = lookupObjectIdentities((ObjectIdentity[]) currentBatchToLoad + .toArray(new ObjectIdentity[] {})); + + // Add loaded batch (all elements 100% initialized) to results + result.putAll(loadedBatch); + + // Add the loaded batch to the cache + Iterator loadedAclIterator = loadedBatch.values().iterator(); + + while (loadedAclIterator.hasNext()) { + aclCache.putInCache((AclImpl) loadedAclIterator.next()); + } + + currentBatchToLoad.clear(); + } + } + + // TODO: Now we're done, check every requested object identity was found (throw NotFoundException if needed) + + return result; + } + + /** + * Looks up a batch of ObjectIdentitys directly from the + * database. + * + *

+ * The caller is responsible for optimization issues, such as selecting the + * identities to lookup, ensuring the cache doesn't contain them already, + * and adding the returned elements to the cache etc. + *

+ * + *

+ * This subclass is required to return fully valid Acls, + * including properly-configured parent ACLs. + *

+ */ + private Map lookupObjectIdentities(final ObjectIdentity[] objectIdentities) { + Assert.notEmpty(objectIdentities, "Must provide identities to lookup"); + + final Map acls = new HashMap(); // contains Acls with StubAclParents + + // Make the "acls" map contain all requested objectIdentities + // (including markers to each parent in the hierarchy) + String sql = computeRepeatingSql("(ACL_OBJECT_IDENTITY.OBJECT_ID_IDENTITY = ? and ACL_CLASS.CLASS = ?)", objectIdentities.length); + System.out.println("Executing lookupObjectIdentities; length: " + objectIdentities.length); + jdbcTemplate.query(sql, + new PreparedStatementSetter() { + public void setValues(PreparedStatement ps) + throws SQLException { + for (int i = 0; i < objectIdentities.length; i++) { + // Determine prepared statement values for this iteration + String javaType = objectIdentities[i].getJavaType().getName(); + Assert.isInstanceOf(Long.class, objectIdentities[i].getIdentifier(),"This class requires ObjectIdentity.getIdentifier() to be a Long"); + long id = ((Long) objectIdentities[i].getIdentifier()).longValue(); + + // Inject values + ps.setLong((2 * i) + 1, id); + ps.setString((2 * i) + 2, javaType); + } + } + }, new ProcessResultSet(acls)); + + // Finally, convert our "acls" containing StubAclParents into true Acls + Map resultMap = new HashMap(); + Iterator iter = acls.values().iterator(); + while (iter.hasNext()) { + Acl inputAcl = (Acl) iter.next(); + Assert.isInstanceOf(AclImpl.class, inputAcl, "Map should have contained an AclImpl"); + Assert.isInstanceOf(Long.class, ((AclImpl)inputAcl).getId(), "Acl.getId() must be Long"); + Acl result = convert(acls, (Long)((AclImpl)inputAcl).getId()); + resultMap.put(result.getObjectIdentity(), result); + } + + return resultMap; + } + + /** + * Locates the primary key IDs specified in "findNow", adding AclImpl + * instances with StubAclParents to the "acls" Map. + * + * @param acls the AclImpls (with StubAclParents) + * @param findNow Long-based primary keys to retrieve + */ + private void lookupPrimaryKeys(final Map acls, final Set findNow) { + Assert.notNull(acls, "ACLs are required"); + Assert.notEmpty(findNow, "Items to find now required"); + + String sql = computeRepeatingSql("(ACL_OBJECT_IDENTITY.ID = ?)",findNow.size()); + System.out.println("Executing lookupPrimaryKeys; length: " + findNow.size()); + + jdbcTemplate.query(sql, + new PreparedStatementSetter() { + public void setValues(PreparedStatement ps) throws SQLException { + Iterator iter = findNow.iterator(); + int i = 0; + while (iter.hasNext()) { + i++; + ps.setLong(i, ((Long)iter.next()).longValue()); + } + } + }, new ProcessResultSet(acls)); + } + + /** + * Accepts the current ResultSet row, and converts it into + * an AclImpl that contains a StubAclParent + * + * @param acls the Map we should add the converted Acl to + * @param rs the ResultSet focused on a current row + * @throws SQLException if something goes wrong converting values + */ + private void convertCurrentResultIntoObject(Map acls, ResultSet rs) throws SQLException { + Long id = new Long(rs.getLong("ACL_ID")); + + // If we already have an ACL for this ID, just create the ACE + AclImpl acl = (AclImpl) acls.get(id); + + if (acl == null) { + // Make an AclImpl and pop it into the Map + ObjectIdentity objectIdentity = new ObjectIdentityImpl(rs.getString( + "CLASS"), new Long(rs.getLong("OBJECT_ID_IDENTITY"))); + + Acl parentAcl = null; + long parentAclId = rs.getLong("PARENT_OBJECT"); + + if (parentAclId != 0) { + parentAcl = new StubAclParent(new Long(parentAclId)); + } + + boolean entriesInheriting = rs.getBoolean("ENTRIES_INHERITING"); + Sid owner; + + if (rs.getBoolean("ACL_PRINCIPAL")) { + owner = new PrincipalSid(rs.getString("ACL_SID")); + } else { + owner = new GrantedAuthoritySid(rs.getString("ACL_SID")); + } + + acl = new AclImpl(objectIdentity, id, parentAcl, auths, null, + entriesInheriting, owner); + acls.put(id, acl); + } + + // Add an extra ACE to the ACL (ORDER BY maintains the ACE list order) + Long aceId = new Long(rs.getLong("ACE_ID")); + Sid recipient; + + if (rs.getBoolean("ACE_PRINCIPAL")) { + recipient = new PrincipalSid(rs.getString("ACE_SID")); + } else { + recipient = new GrantedAuthoritySid(rs.getString("ACE_SID")); + } + + Permission permission = BasePermission.buildFromMask(rs.getInt("MASK")); + boolean granting = rs.getBoolean("GRANTING"); + boolean auditSuccess = rs.getBoolean("AUDIT_SUCCESS"); + boolean auditFailure = rs.getBoolean("AUDIT_FAILURE"); + + AccessControlEntryImpl ace = new AccessControlEntryImpl(aceId, acl, + recipient, permission, granting, auditSuccess, auditFailure); + + + Field acesField = getAccessibleField(AclImpl.class, "aces"); + List aces; + + try { + aces = (List) acesField.get(acl); + } catch (IllegalAccessException ex) { + throw new IllegalStateException( + "Could not obtain AclImpl.ace field", ex); + } + + // Add the ACE if it doesn't already exist in the ACL.aces field + if (!aces.contains(ace)) { + aces.add(ace); + } + } + + /** + * The final phase of converting the Map of AclImpl instances + * which contain StubAclParents into proper, valid AclImpls with + * correct ACL parents. + * + * @param inputMap the unconverted AclImpls + * @param inputAcl the current Acl that we wish to convert (this may be + * @return + */ + private AclImpl convert(Map inputMap, Long currentIdentity) { + Assert.notEmpty(inputMap, "InputMap required"); + Assert.notNull(currentIdentity, "CurrentIdentity required"); + + // Retrieve this Acl from the InputMap + Acl uncastAcl = (Acl) inputMap.get(currentIdentity); + Assert.isInstanceOf(AclImpl.class, uncastAcl, "The inputMap contained a non-AclImpl"); + AclImpl inputAcl = (AclImpl) uncastAcl; + + Acl parent = inputAcl.getParentAcl(); + if (parent != null && parent instanceof StubAclParent) { + // Lookup the parent + StubAclParent stubAclParent = (StubAclParent) parent; + parent = convert(inputMap, stubAclParent.getId()); + } + + // Now we have the parent (if there is one), create the true AclImpl + AclImpl result = new AclImpl(inputAcl.getObjectIdentity(), (Long)inputAcl.getId(), parent, auths, null, inputAcl.isEntriesInheriting(), inputAcl.getOwner()); + + // Copy the "aces" from the input to the destination + Field field = getAccessibleField(AclImpl.class, "aces"); + try { + field.set(result, field.get(inputAcl)); + } catch (IllegalAccessException ex) { + throw new IllegalStateException("Could not obtain or set AclImpl.ace field"); + } + + return result; + } + + private static String computeRepeatingSql(String repeatingSql, int requiredRepetitions) { + Assert.isTrue(requiredRepetitions >= 1, "Must be => 1"); + + String startSql = "select ACL_OBJECT_IDENTITY.OBJECT_ID_IDENTITY, ACL_ENTRY.ACE_ORDER, " + + "ACL_OBJECT_IDENTITY.ID as ACL_ID, " + + "ACL_OBJECT_IDENTITY.PARENT_OBJECT, " + + "ACL_OBJECT_IDENTITY,ENTRIES_INHERITING, " + + "ACL_ENTRY.ID as ACE_ID, ACL_ENTRY.MASK, ACL_ENTRY.GRANTING, ACL_ENTRY.AUDIT_SUCCESS, ACL_ENTRY.AUDIT_FAILURE, " + + "ACE_SID.PRINCIPAL as ACE_PRINCIPAL, ACE_SID.SID as ACE_SID, " + + "ACL_SID.PRINCIPAL as ACL_PRINCIPAL, ACL_SID.SID as ACL_SID, " + + "ACL_CLASS.CLASS " + + "from ACL_OBJECT_IDENTITY, ACL_ENTRY, ACL_SID ACE_SID, ACL_SID ACL_SID, ACL_CLASS " + + "where ACL_ENTRY.ACL_OBJECT_IDENTITY = ACL_OBJECT_IDENTITY.ID " + + "and ACE_SID.ID = ACL_ENTRY.SID " + + "and ACL_SID.ID = ACL_OBJECT_IDENTITY.OWNER_SID " + + "and ACL_CLASS.ID = ACL_OBJECT_IDENTITY.OBJECT_ID_CLASS " + + "and ( "; + + String endSql = ") order by ACL_ENTRY.ACL_OBJECT_IDENTITY asc, ACL_ENTRY.ACE_ORDER asc"; + + StringBuffer sqlStringBuffer = new StringBuffer(); + sqlStringBuffer.append(startSql); + + for (int i = 1; i <= requiredRepetitions; i++) { + sqlStringBuffer.append(repeatingSql); + + if (i != requiredRepetitions) { + sqlStringBuffer.append(" or "); + } + } + + sqlStringBuffer.append(endSql); + + return sqlStringBuffer.toString(); + } + + private static Field getAccessibleField(Class clazz, String protectedField) { + Field field = null; + + try { + field = clazz.getDeclaredField(protectedField); + } catch (NoSuchFieldException nsf) {} + + if (field == null) { + // Unable to locate, so try the superclass (if there is one) + if (clazz.getSuperclass() != null) { + getAccessibleField(clazz.getSuperclass(), protectedField); + } else { + throw new IllegalArgumentException("Couldn't find '" + + protectedField + "' field"); + } + } + + // We have a field, so process + field.setAccessible(true); + + return field; + } + + //~ Inner Classes ========================================================== + + private class StubAclParent implements Acl { + private Long id; + + public StubAclParent(Long id) { + this.id = id; + } + + public AccessControlEntry[] getEntries() { + throw new UnsupportedOperationException("Stub only"); + } + + public Long getId() { + return id; + } + + public ObjectIdentity getObjectIdentity() { + throw new UnsupportedOperationException("Stub only"); + } + + public Acl getParentAcl() { + throw new UnsupportedOperationException("Stub only"); + } + + public boolean isEntriesInheriting() { + throw new UnsupportedOperationException("Stub only"); + } + + public boolean isGranted(Permission[] permission, Sid[] sids, + boolean administrativeMode) + throws NotFoundException, UnloadedSidException { + throw new UnsupportedOperationException("Stub only"); + } + + public boolean isSidLoaded(Sid[] sids) { + throw new UnsupportedOperationException("Stub only"); + } + } + + private class ProcessResultSet implements ResultSetExtractor { + private Map acls; + + public ProcessResultSet(Map acls) { + Assert.notNull(acls, "ACLs cannot be null"); + this.acls = acls; + } + + public Object extractData(ResultSet rs) throws SQLException, DataAccessException { + Set parentIdsToLookup = new HashSet(); // Set of parent_id Longs + + while (rs.next()) { + // Convert current row into an Acl (albeit with a StubAclParent) + convertCurrentResultIntoObject(acls, rs); + + // Figure out if this row means we need to lookup another parent + long parentId = rs.getLong("PARENT_OBJECT"); + + if (parentId != 0) { + // See if its already in the "acls" + if (acls.containsKey(new Long(parentId))) { + continue; // skip this while element + } + + // Now try to find it in the cache + Acl cached = aclCache.getFromCache(new Long(parentId)); + + if (cached == null) { + parentIdsToLookup.add(new Long(parentId)); + } else { + // Pop into the acls map, so our convert method doesn't + // need to deal with an unsynchronized AclCache + Assert.isInstanceOf(AclImpl.class, cached, "Cached ACL must be an AclImpl"); + acls.put(((AclImpl)cached).getId(), cached); + } + } + } + + // Lookup parents, adding Acls (with StubAclParents) to "acl" map + if (parentIdsToLookup.size() > 0) { + lookupPrimaryKeys(acls, parentIdsToLookup); + } + + // Return null to meet ResultSetExtractor method contract + return null; + } + } +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/jdbc/EhCacheBasedAclCache.java b/sandbox/src/main/java/org/acegisecurity/acls/jdbc/EhCacheBasedAclCache.java new file mode 100644 index 0000000000..f50913b6e3 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/jdbc/EhCacheBasedAclCache.java @@ -0,0 +1,58 @@ +package org.acegisecurity.acls.jdbc; + +import net.sf.ehcache.Cache; +import net.sf.ehcache.CacheException; +import net.sf.ehcache.Element; + +import org.acegisecurity.acls.domain.AclImpl; +import org.acegisecurity.acls.objectidentity.ObjectIdentity; +import org.springframework.util.Assert; + +public class EhCacheBasedAclCache implements AclCache { + + private Cache cache; + + public EhCacheBasedAclCache(Cache cache) { + Assert.notNull(cache, "Cache required"); + this.cache = cache; + } + + public AclImpl getFromCache(ObjectIdentity objectIdentity) { + Element element = null; + try { + element = cache.get(objectIdentity); + } catch (CacheException ignored) {} + if (element == null) { + return null; + } + return (AclImpl) element.getValue(); + } + + public AclImpl getFromCache(Long pk) { + Element element = null; + try { + element = cache.get(pk); + } catch (CacheException ignored) {} + if (element == null) { + return null; + } + return (AclImpl) element.getValue(); + } + + public void putInCache(AclImpl acl) { + if (acl.getParentAcl() != null && acl.getParentAcl() instanceof AclImpl) { + putInCache((AclImpl)acl.getParentAcl()); + } + cache.put(new Element(acl.getObjectIdentity(), acl)); + cache.put(new Element(acl.getId(), acl)); + } + + public void evictFromCache(Long pk) { + AclImpl acl = getFromCache(pk); + if (acl != null) { + cache.remove(pk); + cache.remove(acl.getObjectIdentity()); + } + } + +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/jdbc/JdbcAclService.java b/sandbox/src/main/java/org/acegisecurity/acls/jdbc/JdbcAclService.java new file mode 100644 index 0000000000..e0b61ff2b5 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/jdbc/JdbcAclService.java @@ -0,0 +1,51 @@ +package org.acegisecurity.acls.jdbc; + +import java.util.Map; + +import javax.sql.DataSource; + +import org.acegisecurity.acls.AclService; +import org.acegisecurity.acls.NotFoundException; +import org.acegisecurity.acls.objectidentity.ObjectIdentity; +import org.acegisecurity.acls.sid.Sid; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.util.Assert; + +/** + * Simple JDBC-based implementation of AclService. + * + *

+ * Requires the "dirty" flags in {@link org.acegisecurity.acls.domain.AclImpl} and {@link org.acegisecurity.acls.domain.AccessControlEntryImpl} + * to be set, so that the implementation can detect changed parameters easily. + * + * @author Ben Alex + * @version $Id$ + */ +public class JdbcAclService implements AclService/*, MutableAclService */ { + + private AclCache aclCache; + private JdbcTemplate template; + private LookupStrategy lookupStrategy; + + public JdbcAclService(DataSource dataSource, AclCache aclCache, LookupStrategy lookupStrategy) { + Assert.notNull(dataSource, "DataSource required"); + Assert.notNull(aclCache, "AclCache required"); + Assert.notNull(lookupStrategy, "LookupStrategy required"); + this.template = new JdbcTemplate(dataSource); + this.aclCache = aclCache; + this.lookupStrategy = lookupStrategy; + } + + public Map readAclsById(ObjectIdentity[] objects) { + return readAclsById(objects, null); + } + + /** + * Method required by interface. + */ + public Map readAclsById(ObjectIdentity[] objects, Sid[] sids) throws NotFoundException { + return lookupStrategy.readAclsById(objects, sids); + } + + +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/jdbc/LookupStrategy.java b/sandbox/src/main/java/org/acegisecurity/acls/jdbc/LookupStrategy.java new file mode 100644 index 0000000000..eb5bec6e90 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/jdbc/LookupStrategy.java @@ -0,0 +1,47 @@ +/* Copyright 2004, 2005, 2006 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 org.acegisecurity.acls.jdbc; + +import org.acegisecurity.acls.objectidentity.ObjectIdentity; +import org.acegisecurity.acls.sid.Sid; + +import java.util.Map; + + +/** + * Performs optimised lookups for {@link JdbcAclService}. + * + * @author Ben Alex + * @version $Id$ + */ +public interface LookupStrategy { + //~ Methods ================================================================ + + /** + * Perform database-specific optimized lookup. + * + * @param objects the identities to lookup (required) + * @param sids the SIDs for which identities are required (may be + * null - implementations may elect not to provide SID + * optimisations) + * + * @return the Map pursuant to the interface contract for + * {@link + * org.acegisecurity.acls.AclService#readAclsById(ObjectIdentity[], + * Sid[])} + */ + public Map readAclsById(ObjectIdentity[] objects, Sid[] sids); +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/objectidentity/ObjectIdentity.java b/sandbox/src/main/java/org/acegisecurity/acls/objectidentity/ObjectIdentity.java new file mode 100644 index 0000000000..7721ebd40f --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/objectidentity/ObjectIdentity.java @@ -0,0 +1,78 @@ +/* Copyright 2004, 2005, 2006 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 org.acegisecurity.acls.objectidentity; + +import java.io.Serializable; + +/** + * Interface representing the identity of an individual domain object instance. + * + *

+ * As implementations are used as the key for caching and lookup, 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 ObjectIdentitys equal if + * identity1.equals(identity2), rather than reference-equality of + * identity1==identity2. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public interface ObjectIdentity 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); + + /** + * Obtains the actual identifier. This identifier must not be reused to + * represent other domain objects with the same javaType. + * + *

+ * Because ACLs are largely immutable, it is strongly recommended to use a + * synthetic identifier (such as a database sequence number for the + * primary key). Do not use an identifier with business meaning, as that + * business meaning may change. + *

+ * + * @return the identifier (unique within this javaType + */ + public Serializable getIdentifier(); + + /** + * Obtains the Java type represented by the domain object. + * + * @return the Java type of the domain object + */ + public Class getJavaType(); + + /** + * 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/sandbox/src/main/java/org/acegisecurity/acls/objectidentity/ObjectIdentityImpl.java b/sandbox/src/main/java/org/acegisecurity/acls/objectidentity/ObjectIdentityImpl.java new file mode 100644 index 0000000000..29425d5ad6 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/objectidentity/ObjectIdentityImpl.java @@ -0,0 +1,157 @@ +/* Copyright 2004, 2005, 2006 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 org.acegisecurity.acls.objectidentity; + +import org.acegisecurity.acl.basic.AclObjectIdentity; + +import org.acegisecurity.acls.IdentityUnavailableException; + +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +import java.io.Serializable; + +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. + *

+ */ +public class ObjectIdentityImpl implements ObjectIdentity { + //~ Instance fields ======================================================== + + private Class javaType; + private Serializable identifier; + + //~ Constructors =========================================================== + + public ObjectIdentityImpl(String javaType, Serializable identifier) { + Assert.hasText(javaType, "Java Type required"); + Assert.notNull(identifier, "identifier required"); + try { + this.javaType = Class.forName(javaType); + } catch (Exception ex) { + ReflectionUtils.handleReflectionException(ex); + } + this.identifier = identifier; + } + + public ObjectIdentityImpl(Class javaType, Serializable identifier) { + Assert.notNull(javaType, "Java Type required"); + Assert.notNull(identifier, "identifier required"); + this.javaType = javaType; + this.identifier = identifier; + } + + /** + * 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 IdentityUnavailableException if identity could not be extracted + */ + public ObjectIdentityImpl(Object object) + throws IdentityUnavailableException { + Assert.notNull(object, "object cannot be null"); + + this.javaType = object.getClass(); + + Object result; + + try { + Method method = this.javaType.getMethod("getId", new Class[] {}); + result = method.invoke(object, new Object[] {}); + } catch (Exception e) { + throw new IdentityUnavailableException( + "Could not extract identity from object " + object, e); + } + + Assert.isInstanceOf(Serializable.class, result, + "Getter must provide a return value of type Serializable"); + this.identifier = (Serializable) result; + } + + //~ Methods ================================================================ + + /** + * 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 ObjectIdentityImpl)) { + return false; + } + + ObjectIdentityImpl other = (ObjectIdentityImpl) arg0; + + if (this.getIdentifier().equals(other.getIdentifier()) + && this.getJavaType().equals(other.getJavaType())) { + return true; + } + + return false; + } + + public Serializable getIdentifier() { + return identifier; + } + + public Class getJavaType() { + return javaType; + } + + /** + * Important so caching operates properly. + * + * @return the hash + */ + public int hashCode() { + int code = 31; + code ^= this.javaType.hashCode(); + code ^= this.identifier.hashCode(); + + return code; + } + + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append(this.getClass().getName()).append("["); + sb.append("Java Type: ").append(this.javaType); + sb.append("; Identifier: ").append(this.identifier).append("]"); + + return sb.toString(); + } +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/package.html b/sandbox/src/main/java/org/acegisecurity/acls/package.html new file mode 100644 index 0000000000..692d065470 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/package.html @@ -0,0 +1,5 @@ + + +Enables retrieval of access control lists (ACLs) for domain object instances. + + diff --git a/sandbox/src/main/java/org/acegisecurity/acls/sid/GrantedAuthoritySid.java b/sandbox/src/main/java/org/acegisecurity/acls/sid/GrantedAuthoritySid.java new file mode 100644 index 0000000000..5794ebc953 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/sid/GrantedAuthoritySid.java @@ -0,0 +1,74 @@ +/* Copyright 2004, 2005, 2006 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 org.acegisecurity.acls.sid; + +import org.acegisecurity.GrantedAuthority; + +import org.springframework.util.Assert; + + +/** + * Represents a GrantedAuthority as a Sid. + * + *

+ * This is a basic implementation that simply uses the + * String-based principal for Sid comparison. More + * complex principal objects may wish to provide an alternative + * Sid implementation that uses some other identifier. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public class GrantedAuthoritySid implements Sid { + //~ Instance fields ======================================================== + + private String grantedAuthority; + + //~ Constructors =========================================================== + + public GrantedAuthoritySid(String grantedAuthority) { + Assert.hasText(grantedAuthority, "GrantedAuthority required"); + this.grantedAuthority = grantedAuthority; + } + + public GrantedAuthoritySid(GrantedAuthority grantedAuthority) { + Assert.notNull(grantedAuthority, "GrantedAuthority required"); + Assert.notNull(grantedAuthority.getAuthority(), + "This Sid is only compatible with GrantedAuthoritys that provide a non-null getAuthority()"); + this.grantedAuthority = grantedAuthority.getAuthority(); + } + + //~ Methods ================================================================ + + public boolean equals(Object object) { + if ((object == null) || !(object instanceof GrantedAuthoritySid)) { + return false; + } + + // Delegate to getGrantedAuthority() to perform actual comparison (both should be identical) + return ((GrantedAuthoritySid) object).getGrantedAuthority() + .equals(this.getGrantedAuthority()); + } + + public String getGrantedAuthority() { + return grantedAuthority; + } + + public String toString() { + return "GrantedAuthoritySid[" + this.grantedAuthority + "]"; + } +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/sid/PrincipalSid.java b/sandbox/src/main/java/org/acegisecurity/acls/sid/PrincipalSid.java new file mode 100644 index 0000000000..78d5edffbf --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/sid/PrincipalSid.java @@ -0,0 +1,81 @@ +/* Copyright 2004, 2005, 2006 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 org.acegisecurity.acls.sid; + +import org.acegisecurity.Authentication; + +import org.acegisecurity.userdetails.UserDetails; + +import org.springframework.util.Assert; + + +/** + * Represents an Authentication.getPrincipal() as a + * Sid. + * + *

+ * This is a basic implementation that simply uses the + * String-based principal for Sid comparison. More + * complex principal objects may wish to provide an alternative + * Sid implementation that uses some other identifier. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public class PrincipalSid implements Sid { + //~ Instance fields ======================================================== + + private String principal; + + //~ Constructors =========================================================== + + public PrincipalSid(String principal) { + Assert.hasText(principal, "Principal required"); + this.principal = principal; + } + + public PrincipalSid(Authentication authentication) { + Assert.notNull(authentication, "Authentication required"); + Assert.notNull(authentication.getPrincipal(), "Principal required"); + this.principal = authentication.getPrincipal().toString(); + + if (authentication.getPrincipal() instanceof UserDetails) { + this.principal = ((UserDetails) authentication.getPrincipal()) + .getUsername(); + } + } + + //~ Methods ================================================================ + + public boolean equals(Object object) { + if ((object == null) || !(object instanceof PrincipalSid)) { + return false; + } + + // Delegate to getPrincipal() to perform actual comparison (both should be identical) + return ((PrincipalSid) object).getPrincipal() + .equals(this.getPrincipal()); + } + + public String getPrincipal() { + return principal; + } + + public String toString() { + return "PrincipalSid[" + this.principal + "]"; + } +} diff --git a/sandbox/src/main/java/org/acegisecurity/acls/sid/Sid.java b/sandbox/src/main/java/org/acegisecurity/acls/sid/Sid.java new file mode 100644 index 0000000000..018d8e03d7 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/acls/sid/Sid.java @@ -0,0 +1,54 @@ +/* Copyright 2004, 2005, 2006 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 org.acegisecurity.acls.sid; + +/** + * A security identity recognised by the ACL system. + * + *

+ * This interface provides indirection between actual security objects (eg + * principals, roles, groups etc) and what is stored inside an + * Acl. This is because an Acl will not store an + * entire security object, but only an abstraction of it. This interface + * therefore provides a simple way to compare these abstracted security + * identities with other security identities and actual security objects. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public interface Sid { + //~ 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/sandbox/src/test/java/org/acegisecurity/acls/domain/PermissionTests.java b/sandbox/src/test/java/org/acegisecurity/acls/domain/PermissionTests.java new file mode 100644 index 0000000000..450f625f6b --- /dev/null +++ b/sandbox/src/test/java/org/acegisecurity/acls/domain/PermissionTests.java @@ -0,0 +1,41 @@ +package org.acegisecurity.acls.domain; + +import junit.framework.TestCase; + +/** + * Tests BasePermission and CumulativePermission. + * + * @author Ben Alex + * @version $Id${date} + * + */ +public class PermissionTests extends TestCase { + public void testStringConversion() { + System.out.println("R = " + BasePermission.READ.toString()); + assertEquals("BasePermission[...............................R=1]", BasePermission.READ.toString()); + + System.out.println("A = " + BasePermission.ADMINISTRATION.toString()); + assertEquals("BasePermission[............................A...=8]", BasePermission.ADMINISTRATION.toString()); + + System.out.println("R = " + new CumulativePermission().set(BasePermission.READ).toString()); + assertEquals("CumulativePermission[...............................R=1]", new CumulativePermission().set(BasePermission.READ).toString()); + + System.out.println("A = " + new CumulativePermission().set(BasePermission.ADMINISTRATION).toString()); + assertEquals("CumulativePermission[............................A...=8]", new CumulativePermission().set(BasePermission.ADMINISTRATION).toString()); + + System.out.println("RA = " + new CumulativePermission().set(BasePermission.ADMINISTRATION).set(BasePermission.READ).toString()); + assertEquals("CumulativePermission[............................A..R=9]", new CumulativePermission().set(BasePermission.ADMINISTRATION).set(BasePermission.READ).toString()); + + System.out.println("R = " + new CumulativePermission().set(BasePermission.ADMINISTRATION).set(BasePermission.READ).clear(BasePermission.ADMINISTRATION).toString()); + assertEquals("CumulativePermission[...............................R=1]", new CumulativePermission().set(BasePermission.ADMINISTRATION).set(BasePermission.READ).clear(BasePermission.ADMINISTRATION).toString()); + + System.out.println("0 = " + new CumulativePermission().set(BasePermission.ADMINISTRATION).set(BasePermission.READ).clear(BasePermission.ADMINISTRATION).clear(BasePermission.READ).toString()); + assertEquals("CumulativePermission[................................=0]", new CumulativePermission().set(BasePermission.ADMINISTRATION).set(BasePermission.READ).clear(BasePermission.ADMINISTRATION).clear(BasePermission.READ).toString()); + } + + public void testExpectedIntegerValues() { + assertEquals(1, BasePermission.READ.getMask()); + assertEquals(8, BasePermission.ADMINISTRATION.getMask()); + assertEquals(9, new CumulativePermission().set(BasePermission.READ).set(BasePermission.ADMINISTRATION).getMask()); + } +} diff --git a/sandbox/src/test/java/org/acegisecurity/acls/jdbc/DatabaseSeeder.java b/sandbox/src/test/java/org/acegisecurity/acls/jdbc/DatabaseSeeder.java new file mode 100644 index 0000000000..d173bc08a4 --- /dev/null +++ b/sandbox/src/test/java/org/acegisecurity/acls/jdbc/DatabaseSeeder.java @@ -0,0 +1,23 @@ +package org.acegisecurity.acls.jdbc; + +import java.io.IOException; + +import javax.sql.DataSource; + +import org.springframework.core.io.Resource; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; + +public class DatabaseSeeder { + + public DatabaseSeeder(DataSource dataSource, Resource resource) throws IOException { + Assert.notNull(dataSource, "dataSource required"); + Assert.notNull(resource, "resource required"); + + JdbcTemplate template = new JdbcTemplate(dataSource); + String sql = new String(FileCopyUtils.copyToByteArray(resource.getInputStream())); + template.execute(sql); + } + +} diff --git a/sandbox/src/test/java/org/acegisecurity/acls/jdbc/JdbcAclServiceTests.java b/sandbox/src/test/java/org/acegisecurity/acls/jdbc/JdbcAclServiceTests.java new file mode 100644 index 0000000000..f537192674 --- /dev/null +++ b/sandbox/src/test/java/org/acegisecurity/acls/jdbc/JdbcAclServiceTests.java @@ -0,0 +1,40 @@ +package org.acegisecurity.acls.jdbc; + +import java.util.Iterator; +import java.util.Map; + +import org.acegisecurity.acls.Acl; +import org.acegisecurity.acls.objectidentity.ObjectIdentity; +import org.acegisecurity.acls.objectidentity.ObjectIdentityImpl; +import org.springframework.test.AbstractDependencyInjectionSpringContextTests; + +public class JdbcAclServiceTests extends AbstractDependencyInjectionSpringContextTests { + + protected String[] getConfigLocations() { + return new String[] {"classpath:org/acegisecurity/acls/jdbc/applicationContext-test.xml"}; + } + + private JdbcAclService jdbcAclService; + + public void testStub() { + ObjectIdentity id1 = new ObjectIdentityImpl("sample.contact.Contact", new Long(1)); + ObjectIdentity id2 = new ObjectIdentityImpl("sample.contact.Contact", new Long(2)); + ObjectIdentity id3 = new ObjectIdentityImpl("sample.contact.Contact", new Long(3)); + ObjectIdentity id4 = new ObjectIdentityImpl("sample.contact.Contact", new Long(4)); + ObjectIdentity id5 = new ObjectIdentityImpl("sample.contact.Contact", new Long(5)); + ObjectIdentity id6 = new ObjectIdentityImpl("sample.contact.Contact", new Long(6)); + Map map = jdbcAclService.readAclsById(new ObjectIdentity[] {id1, id2, id3, id4, id5, id6}); + Iterator iterator = map.keySet().iterator(); + while (iterator.hasNext()) { + ObjectIdentity identity = (ObjectIdentity) iterator.next(); + assertEquals(identity, ((Acl)map.get(identity)).getObjectIdentity()); + System.out.println(map.get(identity)); + } + } + + public void setJdbcAclService(JdbcAclService jdbcAclService) { + this.jdbcAclService = jdbcAclService; + } + + +} diff --git a/sandbox/src/test/java/org/acegisecurity/acls/jdbc/applicationContext-test.xml b/sandbox/src/test/java/org/acegisecurity/acls/jdbc/applicationContext-test.xml new file mode 100644 index 0000000000..50436f7ae0 --- /dev/null +++ b/sandbox/src/test/java/org/acegisecurity/acls/jdbc/applicationContext-test.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + aclCache + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.hsqldb.jdbcDriver + + + jdbc:hsqldb:mem:test + + + sa + + + + + + + diff --git a/sandbox/src/test/java/org/acegisecurity/acls/jdbc/select.sql b/sandbox/src/test/java/org/acegisecurity/acls/jdbc/select.sql new file mode 100644 index 0000000000..06a1b8af16 --- /dev/null +++ b/sandbox/src/test/java/org/acegisecurity/acls/jdbc/select.sql @@ -0,0 +1,23 @@ +-- Not required. Just shows the sort of queries being sent to DB. + +select ACL_OBJECT_IDENTITY.OBJECT_ID_IDENTITY, ACL_ENTRY.ACE_ORDER, +ACL_OBJECT_IDENTITY.ID as ACL_ID, +ACL_OBJECT_IDENTITY.PARENT_OBJECT, +ACL_OBJECT_IDENTITY,ENTRIES_INHERITING, +ACL_ENTRY.ID as ACE_ID, ACL_ENTRY.MASK, ACL_ENTRY.GRANTING, ACL_ENTRY.AUDIT_SUCCESS, ACL_ENTRY.AUDIT_FAILURE, +ACE_SID.PRINCIPAL as ACE_PRINCIPAL, ACE_SID.SID as ACE_SID, +ACL_SID.PRINCIPAL as ACL_PRINCIPAL, ACL_SID.SID as ACL_SID, +ACL_CLASS.CLASS +from ACL_OBJECT_IDENTITY, ACL_ENTRY, ACL_SID ACE_SID, ACL_SID ACL_SID, ACL_CLASS +where ACL_ENTRY.ACL_OBJECT_IDENTITY = ACL_OBJECT_IDENTITY.ID + +and ACE_SID.ID = ACL_ENTRY.SID +and ACL_SID.ID = ACL_OBJECT_IDENTITY.OWNER_SID +and ACL_CLASS.ID = ACL_OBJECT_IDENTITY.OBJECT_ID_CLASS +and ( +(ACL_OBJECT_IDENTITY.OBJECT_ID_IDENTITY = 1 +and ACL_CLASS.CLASS = 'sample.contact.Contact') +or +(ACL_OBJECT_IDENTITY.OBJECT_ID_IDENTITY = 2 +and ACL_CLASS.CLASS = 'sample.contact.Contact') +) order by ACL_ENTRY.ACL_OBJECT_IDENTITY asc, ACL_ENTRY.ACE_ORDER asc diff --git a/sandbox/src/test/java/org/acegisecurity/acls/jdbc/testData.sql b/sandbox/src/test/java/org/acegisecurity/acls/jdbc/testData.sql new file mode 100644 index 0000000000..fe9d2d2ecc --- /dev/null +++ b/sandbox/src/test/java/org/acegisecurity/acls/jdbc/testData.sql @@ -0,0 +1,70 @@ +CREATE TABLE ACL_SID( +ID BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 100) NOT NULL PRIMARY KEY, +PRINCIPAL BOOLEAN NOT NULL, +SID VARCHAR_IGNORECASE(100) NOT NULL, +CONSTRAINT UNIQUE_UK_1 UNIQUE(SID,PRINCIPAL)); + +INSERT INTO ACL_SID VALUES (1, TRUE, 'MARISSA'); +INSERT INTO ACL_SID VALUES (2, TRUE, 'DIANNE'); +INSERT INTO ACL_SID VALUES (3, TRUE, 'SCOTT'); +INSERT INTO ACL_SID VALUES (4, TRUE, 'PETER'); + +CREATE TABLE ACL_CLASS( +ID BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 100) NOT NULL PRIMARY KEY, +CLASS VARCHAR_IGNORECASE(100) NOT NULL, +CONSTRAINT UNIQUE_UK_2 UNIQUE(CLASS)); + +INSERT INTO ACL_CLASS VALUES (1, 'sample.contact.Contact'); + +CREATE TABLE ACL_OBJECT_IDENTITY( +ID BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 100) NOT NULL PRIMARY KEY, +OBJECT_ID_CLASS BIGINT NOT NULL, +OBJECT_ID_IDENTITY BIGINT NOT NULL, +PARENT_OBJECT BIGINT, +OWNER_SID BIGINT, +ENTRIES_INHERITING BOOLEAN NOT NULL, +CONSTRAINT UNIQUE_UK_3 UNIQUE(OBJECT_ID_CLASS,OBJECT_ID_IDENTITY), +CONSTRAINT FOREIGN_FK_1 FOREIGN KEY(PARENT_OBJECT)REFERENCES ACL_OBJECT_IDENTITY(ID), +CONSTRAINT FOREIGN_FK_2 FOREIGN KEY(OBJECT_ID_CLASS)REFERENCES ACL_CLASS(ID), +CONSTRAINT FOREIGN_FK_3 FOREIGN KEY(OWNER_SID)REFERENCES ACL_SID(ID)); + +INSERT INTO ACL_OBJECT_IDENTITY VALUES (1, 1, 1, NULL, 1, TRUE); +INSERT INTO ACL_OBJECT_IDENTITY VALUES (2, 1, 2, 1, 2, TRUE); +INSERT INTO ACL_OBJECT_IDENTITY VALUES (3, 1, 3, 1, 1, FALSE); +INSERT INTO ACL_OBJECT_IDENTITY VALUES (4, 1, 4, 1, 2, TRUE); +INSERT INTO ACL_OBJECT_IDENTITY VALUES (5, 1, 5, 1, 2, FALSE); +INSERT INTO ACL_OBJECT_IDENTITY VALUES (6, 1, 6, NULL, 1, TRUE); + +CREATE TABLE ACL_ENTRY( +ID BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 100) NOT NULL PRIMARY KEY, +ACL_OBJECT_IDENTITY BIGINT NOT NULL, +ACE_ORDER INT NOT NULL, +SID BIGINT NOT NULL, +MASK INTEGER NOT NULL, +GRANTING BOOLEAN NOT NULL, +AUDIT_SUCCESS BOOLEAN NOT NULL, +AUDIT_FAILURE BOOLEAN NOT NULL, +CONSTRAINT UNIQUE_UK_4 UNIQUE(ACL_OBJECT_IDENTITY,ACE_ORDER), +CONSTRAINT FOREIGN_FK_4 FOREIGN KEY(ACL_OBJECT_IDENTITY) REFERENCES ACL_OBJECT_IDENTITY(ID), +CONSTRAINT FOREIGN_FK_5 FOREIGN KEY(SID) REFERENCES ACL_SID(ID)); + +INSERT INTO ACL_ENTRY VALUES (1, 1, 1, 2, 8, TRUE, FALSE, FALSE); +INSERT INTO ACL_ENTRY VALUES (2, 1, 2, 1, 2, TRUE, FALSE, FALSE); +INSERT INTO ACL_ENTRY VALUES (3, 1, 3, 3, 8, TRUE, FALSE, FALSE); +INSERT INTO ACL_ENTRY VALUES (4, 2, 1, 3, 4, TRUE, FALSE, FALSE); +INSERT INTO ACL_ENTRY VALUES (5, 2, 2, 4, 8, TRUE, FALSE, FALSE); +INSERT INTO ACL_ENTRY VALUES (6, 3, 1, 3, 8, TRUE, FALSE, FALSE); +INSERT INTO ACL_ENTRY VALUES (7, 3, 2, 4, 8, TRUE, FALSE, FALSE); +INSERT INTO ACL_ENTRY VALUES (8, 3, 3, 1, 8, FALSE, FALSE, FALSE); +INSERT INTO ACL_ENTRY VALUES (9, 4, 1, 4, 8, TRUE, FALSE, FALSE); +INSERT INTO ACL_ENTRY VALUES (10, 5, 1, 2, 8, TRUE, FALSE, FALSE); +INSERT INTO ACL_ENTRY VALUES (11, 5, 2, 3, 8, FALSE, TRUE, TRUE); +INSERT INTO ACL_ENTRY VALUES (12, 5, 3, 1, 8, TRUE, FALSE, FALSE); +INSERT INTO ACL_ENTRY VALUES (13, 5, 4, 4, 8, TRUE, FALSE, FALSE); +INSERT INTO ACL_ENTRY VALUES (14, 6, 1, 2, 1, TRUE, FALSE, FALSE); +INSERT INTO ACL_ENTRY VALUES (15, 6, 2, 1, 2, TRUE, FALSE, FALSE); +INSERT INTO ACL_ENTRY VALUES (16, 6, 3, 2, 4, TRUE, FALSE, FALSE); +INSERT INTO ACL_ENTRY VALUES (17, 6, 4, 3, 2, TRUE, FALSE, FALSE); +INSERT INTO ACL_ENTRY VALUES (18, 6, 5, 3, 1, TRUE, FALSE, FALSE); +INSERT INTO ACL_ENTRY VALUES (19, 6, 6, 4, 4, TRUE, FALSE, FALSE); +INSERT INTO ACL_ENTRY VALUES (20, 6, 7, 4, 2, TRUE, FALSE, FALSE);