Browse Source

Fixes to new ACL implementation. Thanks to Nathan Sarr.

Ben Alex 19 years ago
parent
commit
9769394c92

+ 500 - 500
sandbox/src/main/java/org/acegisecurity/acls/domain/AclImpl.java

@@ -1,500 +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 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;
-
-import java.io.Serializable;
-
-import java.util.Iterator;
-import java.util.List;
-import java.util.Vector;
-
-
-/**
- * Base implementation of <code>Acl</code>.
- *
- * @author Ben Alex
- * @version $Id
- */
-public class AclImpl implements Acl, MutableAcl, AuditableAcl, OwnershipAcl {
-    //~ Static fields/initializers =====================================================================================
-
-    private static final int CHANGE_OWNERSHIP = 0;
-    private static final int CHANGE_AUDITING = 1;
-    private static final int CHANGE_GENERAL = 2;
-
-    //~ Instance fields ================================================================================================
-
-    private Acl parentAcl;
-    private AuditLogger auditLogger = new ConsoleAuditLogger(); // AuditableAcl
-    private GrantedAuthority gaGeneralChanges;
-    private GrantedAuthority gaModifyAuditing;
-    private GrantedAuthority gaTakeOwnership;
-    private List aces = new Vector();
-    private List deletedAces = new Vector();
-    private Long id;
-    private ObjectIdentity objectIdentity;
-    private Sid owner; // OwnershipAcl
-    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 entriesInheriting = false;
-    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 <code>GrantedAuthority</code>s 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);
-    }
-
-/**
-     * 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 <code>GrantedAuthority</code>s 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 <code>null</code>)
-     * @param loadedSids the loaded SIDs if only a subset were loaded (may be
-     *        <code>null</code>)
-     * @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 ========================================================================================================
-
-    /**
-     * Clears the dirty flags on the <code>Acl</code>, but not any associated ACEs.
-     */
-    public void clearDirtyFlags() {
-        this.aclDirty = false;
-        this.addedAces = false;
-        this.updatedAces = false;
-    }
-
-    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 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 isAclDirty() {
-        return aclDirty;
-    }
-
-    public boolean isEntriesInheriting() {
-        return entriesInheriting;
-    }
-
-    /**
-     * Determines authorization.  The order of the <code>permission</code> and <code>sid</code> arguments is
-     * <em>extremely important</em>! The method will iterate through each of the <code>permission</code>s in the order
-     * specified. For each iteration, all of the <code>sid</code>s will be considered, again in the order they are
-     * presented. The iteration of each <code>permission:sid</code> combination will then inspect the ACEs in the
-     * order they appear in the ACL. When the <em>first full match</em> 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 <code>true</code>. If the ACE
-     * specifies to deny access, the loop will stop and the next <code>permission</code> 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 <code>true</code>. The parent ACL will
-     * also scan its parent and so on. If ultimately no matching ACE is found, a <code>NotFoundException</code> 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 <code>UnloadedSidException</code> 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 <code>true</code> denotes the query is for administrative purposes and no auditing
-     *        will be undertaken
-     *
-     * @return <code>true</code> if one of the permissions has been granted, <code>false</code> 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 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;
-    }
-
-    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");
-    }
-
-    /**
-     * Change the special adminstrative permissions honoured by this object.<p>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).</p>
-     *
-     * @param auths an array of <code>GrantedAuthority</code>s 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];
-    }
-
-    public void setEntriesInheriting(boolean entriesInheriting) {
-        securityCheck(CHANGE_GENERAL);
-        this.entriesInheriting = entriesInheriting;
-        this.aclDirty = true;
-    }
-
-    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 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();
-    }
-
-    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;
-    }
-}
+/* 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.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;
+
+import java.io.Serializable;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Vector;
+
+
+/**
+ * Base implementation of <code>Acl</code>.
+ *
+ * @author Ben Alex
+ * @version $Id
+ */
+public class AclImpl implements Acl, MutableAcl, AuditableAcl, OwnershipAcl {
+    //~ Static fields/initializers =====================================================================================
+
+    private static final int CHANGE_OWNERSHIP = 0;
+    private static final int CHANGE_AUDITING = 1;
+    private static final int CHANGE_GENERAL = 2;
+
+    //~ Instance fields ================================================================================================
+
+    private Acl parentAcl;
+    private AuditLogger auditLogger = new ConsoleAuditLogger(); // AuditableAcl
+    private GrantedAuthority gaGeneralChanges;
+    private GrantedAuthority gaModifyAuditing;
+    private GrantedAuthority gaTakeOwnership;
+    private List aces = new Vector();
+    private List deletedAces = new Vector();
+    private Long id;
+    private ObjectIdentity objectIdentity;
+    private Sid owner; // OwnershipAcl
+    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 entriesInheriting = false;
+    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 <code>GrantedAuthority</code>s 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);
+    }
+
+/**
+     * 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 <code>GrantedAuthority</code>s 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 <code>null</code>)
+     * @param loadedSids the loaded SIDs if only a subset were loaded (may be
+     *        <code>null</code>)
+     * @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 ========================================================================================================
+
+    /**
+     * Clears the dirty flags on the <code>Acl</code>, but not any associated ACEs.
+     */
+    public void clearDirtyFlags() {
+        this.aclDirty = false;
+        this.addedAces = false;
+        this.updatedAces = false;
+    }
+
+    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 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 isAclDirty() {
+        return aclDirty;
+    }
+
+    public boolean isEntriesInheriting() {
+        return entriesInheriting;
+    }
+
+    /**
+     * Determines authorization.  The order of the <code>permission</code> and <code>sid</code> arguments is
+     * <em>extremely important</em>! The method will iterate through each of the <code>permission</code>s in the order
+     * specified. For each iteration, all of the <code>sid</code>s will be considered, again in the order they are
+     * presented. The iteration of each <code>permission:sid</code> combination will then inspect the ACEs in the
+     * order they appear in the ACL. When the <em>first full match</em> 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 <code>true</code>. If the ACE
+     * specifies to deny access, the loop will stop and the next <code>permission</code> 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 <code>true</code>. The parent ACL will
+     * also scan its parent and so on. If ultimately no matching ACE is found, a <code>NotFoundException</code> 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 <code>UnloadedSidException</code> 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 <code>true</code> denotes the query is for administrative purposes and no auditing
+     *        will be undertaken
+     *
+     * @return <code>true</code> if one of the permissions has been granted, <code>false</code> 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 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;
+    }
+
+    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");
+    }
+
+    /**
+     * Change the special adminstrative permissions honoured by this object.<p>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).</p>
+     *
+     * @param auths an array of <code>GrantedAuthority</code>s 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];
+    }
+
+    public void setEntriesInheriting(boolean entriesInheriting) {
+        securityCheck(CHANGE_GENERAL);
+        this.entriesInheriting = entriesInheriting;
+        this.aclDirty = true;
+    }
+
+    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 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();
+    }
+
+    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;
+    }
+}

+ 106 - 102
sandbox/src/main/java/org/acegisecurity/acls/domain/BasePermission.java

@@ -1,102 +1,106 @@
-/* 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;
-
-
-/**
- * DOCUMENT ME!
- *
- * @author $author$
- * @version $Revision$
-  */
-public class BasePermission implements Permission {
-    //~ Static fields/initializers =====================================================================================
-
-    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
-
-    //~ Instance fields ================================================================================================
-
-    private char code;
-    private int mask;
-
-    //~ Constructors ===================================================================================================
-
-    private BasePermission(int mask, char code) {
-        this.mask = mask;
-        this.code = code;
-    }
-
-    //~ Methods ========================================================================================================
-
-    /**
-     * Dynamically creates a <code>CumulativePermission</code> representing the active bits in the passed mask.
-     * NB: Only uses <code>BasePermission</code>!
-     *
-     * @param mask to review
-     *
-     * @return DOCUMENT ME!
-     */
-    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 boolean equals(Object arg0) {
-        if (!(arg0 instanceof BasePermission)) {
-            return false;
-        }
-
-        BasePermission rhs = (BasePermission) arg0;
-
-        return (this.mask == rhs.getMask());
-    }
-
-    public int getMask() {
-        return mask;
-    }
-
-    public String getPattern() {
-        return AclFormattingUtils.printBinary(mask, code);
-    }
-
-    public String toString() {
-        return "BasePermission[" + getPattern() + "=" + mask + "]";
-    }
-}
+/* 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;
+
+/**
+ * DOCUMENT ME!
+ *
+ * @author $author$
+ * @version $Revision$
+  */
+public class BasePermission implements Permission {
+    //~ Static fields/initializers =====================================================================================
+
+    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 DELETE = new BasePermission(1 << 3, 'D'); // 8
+    public static final Permission ADMINISTRATION = new BasePermission(1 << 4, 'A'); // 16
+
+    //~ Instance fields ================================================================================================
+
+    private char code;
+    private int mask;
+
+    //~ Constructors ===================================================================================================
+
+    private BasePermission(int mask, char code) {
+        this.mask = mask;
+        this.code = code;
+    }
+
+    //~ Methods ========================================================================================================
+
+    /**
+     * Dynamically creates a <code>CumulativePermission</code> representing the active bits in the passed mask.
+     * NB: Only uses <code>BasePermission</code>!
+     *
+     * @param mask to review
+     *
+     * @return DOCUMENT ME!
+     */
+    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(DELETE);
+        }
+        
+        if (mask == 16) {
+            permission.set(ADMINISTRATION);
+        }
+
+        return permission;
+    }
+
+    public boolean equals(Object arg0) {
+        if (!(arg0 instanceof BasePermission)) {
+            return false;
+        }
+
+        BasePermission rhs = (BasePermission) arg0;
+
+        return (this.mask == rhs.getMask());
+    }
+
+    public int getMask() {
+        return mask;
+    }
+
+    public String getPattern() {
+        return AclFormattingUtils.printBinary(mask, code);
+    }
+
+    public String toString() {
+        return "BasePermission[" + getPattern() + "=" + mask + "]";
+    }
+}

+ 70 - 70
sandbox/src/test/java/org/acegisecurity/acls/domain/PermissionTests.java

@@ -1,70 +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.domain;
-
-import junit.framework.TestCase;
-
-
-/**
- * Tests BasePermission and CumulativePermission.
- *
- * @author Ben Alex
- * @version $Id${date}
- */
-public class PermissionTests extends TestCase {
-    //~ Methods ========================================================================================================
-
-    public void testExpectedIntegerValues() {
-        assertEquals(1, BasePermission.READ.getMask());
-        assertEquals(8, BasePermission.ADMINISTRATION.getMask());
-        assertEquals(9, new CumulativePermission().set(BasePermission.READ).set(BasePermission.ADMINISTRATION).getMask());
-    }
-
-    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());
-    }
-}
+/* 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 junit.framework.TestCase;
+
+
+/**
+ * Tests BasePermission and CumulativePermission.
+ *
+ * @author Ben Alex
+ * @version $Id${date}
+ */
+public class PermissionTests extends TestCase {
+    //~ Methods ========================================================================================================
+
+    public void testExpectedIntegerValues() {
+        assertEquals(1, BasePermission.READ.getMask());
+        assertEquals(16, BasePermission.ADMINISTRATION.getMask());
+        assertEquals(17, new CumulativePermission().set(BasePermission.READ).set(BasePermission.ADMINISTRATION).getMask());
+    }
+
+    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....=16]", 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....=16]",
+            new CumulativePermission().set(BasePermission.ADMINISTRATION).toString());
+
+        System.out.println("RA = "
+            + new CumulativePermission().set(BasePermission.ADMINISTRATION).set(BasePermission.READ).toString());
+        assertEquals("CumulativePermission[...........................A...R=17]",
+            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());
+    }
+}