Explorar el Código

SEC-232: Add role hierarchy contribution.

Luke Taylor hace 18 años
padre
commit
2f03000b68

+ 30 - 0
core/src/main/java/org/acegisecurity/userdetails/hierarchicalroles/CycleInRoleHierarchyException.java

@@ -0,0 +1,30 @@
+/*
+ * 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.userdetails.hierarchicalroles;
+
+/**
+ * Exception that is thrown because of a cycle in the role hierarchy definition
+ * 
+ * @author Michael Mayr
+ */
+public class CycleInRoleHierarchyException extends RuntimeException {
+
+    private static final long serialVersionUID = -4970510612118296707L;
+
+    public CycleInRoleHierarchyException() {
+        super("Exception thrown because of a cycle in the role hierarchy definition!");
+    }
+
+}

+ 42 - 0
core/src/main/java/org/acegisecurity/userdetails/hierarchicalroles/RoleHierarchy.java

@@ -0,0 +1,42 @@
+/*
+ * 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.userdetails.hierarchicalroles;
+
+import org.acegisecurity.GrantedAuthority;
+
+/**
+ * The simple interface of a role hierarchy.
+ *
+ * @author Michael Mayr
+ *
+ */
+public interface RoleHierarchy {
+    
+    /**
+     * This method returns an array of all reachable authorities.<br>
+     * Reachable authorities are the directly assigned authorities plus all 
+     * authorities that are (transitively) reachable from them in the role 
+     * hierarchy.<br>
+     * Example:<br>
+     * Role hierarchy: ROLE_A > ROLE_B and ROLE_B > ROLE_C.<br>
+     * Directly assigned authority: ROLE_A.<br>
+     * Reachable authorities: ROLE_A, ROLE_B, ROLE_C.
+     *   
+     * @param authorities - Array of the directly assigned authorities.
+     * @return Array of all reachable authorities given the assigned authorities.
+     */
+    public GrantedAuthority[] getReachableGrantedAuthorities(GrantedAuthority[] authorities);
+
+}

+ 203 - 0
core/src/main/java/org/acegisecurity/userdetails/hierarchicalroles/RoleHierarchyImpl.java

@@ -0,0 +1,203 @@
+/*
+ * 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.userdetails.hierarchicalroles;
+
+import org.acegisecurity.GrantedAuthority;
+import org.acegisecurity.GrantedAuthorityImpl;
+
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang.ArrayUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+
+/**
+ * <p>
+ * This class defines a role hierarchy for use with the UserDetailsServiceWrapper.
+ * </p>
+ * <p>
+ * Here is an example configuration of a role hierarchy (hint: read the "&gt;" sign as "includes"):
+<pre>
+        &lt;property name="hierarchy"&gt;
+            &lt;value&gt;
+                ROLE_A &gt; ROLE_B
+                ROLE_B &gt; ROLE_AUTHENTICATED
+                ROLE_AUTHENTICATED &gt; ROLE_UNAUTHENTICATED
+            &lt;/value&gt;
+        &lt;/property&gt;
+</pre>
+</p>
+ * <p>
+ * Explanation of the above:<br>
+ * In effect every user with ROLE_A also has ROLE_B, ROLE_AUTHENTICATED and ROLE_UNAUTHENTICATED;<br>
+ * every user with ROLE_B also has ROLE_AUTHENTICATED and ROLE_UNAUTHENTICATED;<br>
+ * every user with ROLE_AUTHENTICATED also has ROLE_UNAUTHENTICATED.
+ * </p>
+ * <p>
+ * Hierarchical Roles will dramatically shorten your access rules (and also make the access rules much more elegant).
+ * </p>
+ * <p>
+ * Consider this access rule for Acegi's RoleVoter (background: every user that is authenticated should be
+ * able to log out):<br>
+ * /logout.html=ROLE_A,ROLE_B,ROLE_AUTHENTICATED<br>
+ * With hierarchical roles this can now be shortened to:<br>
+ * /logout.html=ROLE_AUTHENTICATED<br>
+ * In addition to shorter rules this will also make your access rules more readable and your intentions clearer.
+ * </p>
+ *
+ * @author Michael Mayr
+ *
+ */
+public class RoleHierarchyImpl implements RoleHierarchy {
+
+    private static final Log logger = LogFactory.getLog(RoleHierarchyImpl.class);
+
+    private String roleHierarchyStringRepresentation = null;
+
+    /**
+     * rolesReachableInOneStepMap is a Map that under the key of a specific role name contains a set of all roles
+     * reachable from this role in 1 step
+     */
+    private Map rolesReachableInOneStepMap = null;
+
+    /**
+     * rolesReachableInOneOrMoreStepsMap is a Map that under the key of a specific role name contains a set of all
+     * roles reachable from this role in 1 or more steps
+     */
+    private Map rolesReachableInOneOrMoreStepsMap = null;
+
+    /**
+     * Set the role hierarchy and precalculate for every role the set of all reachable roles, i. e. all roles lower in
+     * the hierarchy of every given role. Precalculation is done for performance reasons (reachable roles can then be
+     * calculated in O(1) time).
+     * During precalculation cycles in role hierarchy are detected and will cause a
+     * <tt>CycleInRoleHierarchyException</tt> to be thrown.
+     *
+     * @param roleHierarchyStringRepresentation - String definition of the role hierarchy.
+     */
+    public void setHierarchy(String roleHierarchyStringRepresentation) {
+        this.roleHierarchyStringRepresentation = roleHierarchyStringRepresentation;
+
+        logger.debug("setHierarchy() - The following role hierarchy was set: " + roleHierarchyStringRepresentation);
+
+        buildRolesReachableInOneStepMap();
+        buildRolesReachableInOneOrMoreStepsMap();
+    }
+
+    public GrantedAuthority[] getReachableGrantedAuthorities(GrantedAuthority[] authorities) {
+        if (authorities == null || authorities.length == 0) {
+            return null;
+        }
+
+        Set reachableRoles = new HashSet();
+
+        for (int i = 0; i < authorities.length; i++) {
+            reachableRoles.add(authorities[i]);
+            Set additionalReachableRoles = (Set) rolesReachableInOneOrMoreStepsMap.get(authorities[i]);
+            if (additionalReachableRoles != null) {
+                reachableRoles.addAll(additionalReachableRoles);
+            }
+        }
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("getReachableGrantedAuthorities() - From the roles " + ArrayUtils.toString(authorities)
+                    + " one can reach " + reachableRoles + " in zero or more steps.");
+        }
+
+        return (GrantedAuthority[]) reachableRoles.toArray(new GrantedAuthority[reachableRoles.size()]);
+    }
+
+    /**
+     * Parse input and build the map for the roles reachable in one step: the higher role will become a key that
+     * references a set of the reachable lower roles.
+     */
+    private void buildRolesReachableInOneStepMap() {
+        String parsingRegex = "(\\s*(\\w+)\\s*\\>\\s*(\\w+))";
+        Pattern pattern = Pattern.compile(parsingRegex);
+
+        Matcher roleHierarchyMatcher = pattern.matcher(roleHierarchyStringRepresentation);
+        rolesReachableInOneStepMap = new HashMap();
+
+        while (roleHierarchyMatcher.find()) {
+            GrantedAuthority higherRole = new GrantedAuthorityImpl(roleHierarchyMatcher.group(2));
+            GrantedAuthority lowerRole = new GrantedAuthorityImpl(roleHierarchyMatcher.group(3));
+            Set rolesReachableInOneStepSet = null;
+
+            if (!rolesReachableInOneStepMap.containsKey(higherRole)) {
+                rolesReachableInOneStepSet = new HashSet();
+                rolesReachableInOneStepMap.put(higherRole, rolesReachableInOneStepSet);
+            } else {
+                rolesReachableInOneStepSet = (Set) rolesReachableInOneStepMap.get(higherRole);
+            }
+            rolesReachableInOneStepSet.add(lowerRole);
+
+            logger.debug("buildRolesReachableInOneStepMap() - From role "
+                    + higherRole + " one can reach role " + lowerRole + " in one step.");
+        }
+    }
+
+    /**
+     * For every higher role from rolesReachableInOneStepMap store all roles that are reachable from it in the map of
+     * roles reachable in one or more steps. (Or throw a CycleInRoleHierarchyException if a cycle in the role
+     * hierarchy definition is detected)
+     */
+    private void buildRolesReachableInOneOrMoreStepsMap() {
+        rolesReachableInOneOrMoreStepsMap = new HashMap();
+        // iterate over all higher roles from rolesReachableInOneStepMap
+        Iterator roleIterator = rolesReachableInOneStepMap.keySet().iterator();
+
+        while (roleIterator.hasNext()) {
+            GrantedAuthority role = (GrantedAuthority) roleIterator.next();
+            Set rolesToVisitSet = new HashSet();
+
+            if (rolesReachableInOneStepMap.containsKey(role)) {
+                rolesToVisitSet.addAll((Set) rolesReachableInOneStepMap.get(role));
+            }
+
+            Set visitedRolesSet = new HashSet();
+
+            while (!rolesToVisitSet.isEmpty()) {
+                // take a role from the rolesToVisit set
+                GrantedAuthority aRole = (GrantedAuthority) rolesToVisitSet.iterator().next();
+                rolesToVisitSet.remove(aRole);
+                visitedRolesSet.add(aRole);
+                if (rolesReachableInOneStepMap.containsKey(aRole)) {
+                    Set newReachableRoles = (Set) rolesReachableInOneStepMap.get(aRole);
+
+                    if (CollectionUtils.containsAny(rolesToVisitSet, newReachableRoles)
+                            || CollectionUtils.containsAny(visitedRolesSet, newReachableRoles)) {
+                        throw new CycleInRoleHierarchyException();
+                    } else {
+                         // no cycle
+                        rolesToVisitSet.addAll(newReachableRoles);
+                    }
+                }
+            }
+            rolesReachableInOneOrMoreStepsMap.put(role, visitedRolesSet);
+
+            logger.debug("buildRolesReachableInOneOrMoreStepsMap() - From role "
+                    + role + " one can reach " + visitedRolesSet + " in one or more steps.");
+        }
+
+    }
+
+}

+ 53 - 0
core/src/main/java/org/acegisecurity/userdetails/hierarchicalroles/UserDetailsServiceWrapper.java

@@ -0,0 +1,53 @@
+/*
+ * 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.userdetails.hierarchicalroles;
+
+import org.acegisecurity.userdetails.UserDetails;
+import org.acegisecurity.userdetails.UserDetailsService;
+import org.acegisecurity.userdetails.UsernameNotFoundException;
+import org.springframework.dao.DataAccessException;
+
+/**
+ * This class wraps Acegi's UserDetailsService in a way that its loadUserByUsername() 
+ * method returns wrapped UserDetails that return all hierachically reachable authorities 
+ * instead of only the directly assigned authorities.
+ * 
+ * @author Michael Mayr
+ */
+public class UserDetailsServiceWrapper implements UserDetailsService {
+    
+    private UserDetailsService userDetailsService = null;
+    
+    private RoleHierarchy roleHierarchy = null;
+
+    public void setRoleHierarchy(RoleHierarchy roleHierarchy) {
+        this.roleHierarchy = roleHierarchy;
+    }
+    
+    public void setUserDetailsService(UserDetailsService userDetailsService) {
+        this.userDetailsService = userDetailsService;
+    }
+
+    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
+        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
+        // wrapped UserDetailsService might throw UsernameNotFoundException or DataAccessException which will then bubble up
+        return new UserDetailsWrapper(userDetails, roleHierarchy);
+    }
+    
+    public UserDetailsService getWrappedUserDetailsService() {
+        return userDetailsService;
+    }
+
+}

+ 72 - 0
core/src/main/java/org/acegisecurity/userdetails/hierarchicalroles/UserDetailsWrapper.java

@@ -0,0 +1,72 @@
+/*
+ * 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.userdetails.hierarchicalroles;
+
+import org.acegisecurity.GrantedAuthority;
+import org.acegisecurity.userdetails.UserDetails;
+
+/**
+ * This class wraps Acegi's UserDetails in a way that its getAuthorities()-Method is 
+ * delegated to RoleHierarchy.getReachableGrantedAuthorities. All other methods are
+ * delegated to the UserDetails implementation.
+ * 
+ * @author Michael Mayr
+ */
+public class UserDetailsWrapper implements UserDetails {
+
+    private static final long serialVersionUID = 1532428778390085311L;
+    
+    private UserDetails userDetails = null;
+    
+    private RoleHierarchy roleHierarchy = null;
+    
+    public UserDetailsWrapper(UserDetails userDetails, RoleHierarchy roleHierarchy) {
+        this.userDetails = userDetails;
+        this.roleHierarchy = roleHierarchy;
+    }
+
+    public boolean isAccountNonExpired() {
+        return userDetails.isAccountNonExpired();
+    }
+
+    public boolean isAccountNonLocked() {
+        return userDetails.isAccountNonLocked();
+    }
+
+    public GrantedAuthority[] getAuthorities() {
+        return roleHierarchy.getReachableGrantedAuthorities(userDetails.getAuthorities());
+    }
+
+    public boolean isCredentialsNonExpired() {
+        return userDetails.isCredentialsNonExpired();
+    }
+
+    public boolean isEnabled() {
+        return userDetails.isEnabled();
+    }
+
+    public String getPassword() {
+        return userDetails.getPassword();
+    }
+
+    public String getUsername() {
+        return userDetails.getUsername();
+    }
+    
+    public UserDetails getUnwrappedUserDetails() {
+        return userDetails;
+    }
+    
+}

+ 43 - 0
core/src/test/java/org/acegisecurity/userdetails/hierarchicalroles/HierarchicalRolesTestHelper.java

@@ -0,0 +1,43 @@
+/*
+ * 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.userdetails.hierarchicalroles;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.acegisecurity.GrantedAuthority;
+import org.apache.commons.collections.CollectionUtils;
+
+/**
+ * Test helper class for the hierarchical roles tests.
+ * 
+ * @author Michael Mayr
+ */
+public abstract class HierarchicalRolesTestHelper {
+
+    public static boolean containTheSameGrantedAuthorities(GrantedAuthority[] authorities1, GrantedAuthority[] authorities2) {
+        if (authorities1 == null && authorities2 == null) {
+            return true;
+        } else if (authorities1 == null || authorities2 == null) {
+            return false;
+        }
+        List authoritiesList1 = new ArrayList();
+        CollectionUtils.addAll(authoritiesList1, authorities1);
+        List authoritiesList2 = new ArrayList();
+        CollectionUtils.addAll(authoritiesList2, authorities2);
+        return CollectionUtils.isEqualCollection(authoritiesList1, authoritiesList2);
+    }
+    
+}

+ 88 - 0
core/src/test/java/org/acegisecurity/userdetails/hierarchicalroles/RoleHierarchyImplTests.java

@@ -0,0 +1,88 @@
+/*
+* 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.userdetails.hierarchicalroles;
+
+import junit.framework.TestCase;
+import junit.textui.TestRunner;
+
+import org.acegisecurity.GrantedAuthority;
+import org.acegisecurity.GrantedAuthorityImpl;
+
+/**
+ * Tests for {@link RoleHierarchyImpl}.
+ *
+ * @author Michael Mayr
+ */
+public class RoleHierarchyImplTests extends TestCase {
+
+    public RoleHierarchyImplTests() {
+    }
+
+    public RoleHierarchyImplTests(String testCaseName) {
+        super(testCaseName);
+    }
+
+    public static void main(String[] args) {
+        TestRunner.run(RoleHierarchyImplTests.class);
+    }
+
+    public void testSimpleRoleHierarchy() {
+        GrantedAuthority[] authorities0 = new GrantedAuthority[] { new GrantedAuthorityImpl("ROLE_0") };
+        GrantedAuthority[] authorities1 = new GrantedAuthority[] { new GrantedAuthorityImpl("ROLE_A") };
+        GrantedAuthority[] authorities2 = new GrantedAuthority[] { new GrantedAuthorityImpl("ROLE_A"), new GrantedAuthorityImpl("ROLE_B") };
+
+        RoleHierarchyImpl roleHierarchyImpl = new RoleHierarchyImpl();
+        roleHierarchyImpl.setHierarchy("ROLE_A > ROLE_B");
+
+        assertTrue(HierarchicalRolesTestHelper.containTheSameGrantedAuthorities(roleHierarchyImpl.getReachableGrantedAuthorities(authorities0), authorities0));
+        assertTrue(HierarchicalRolesTestHelper.containTheSameGrantedAuthorities(roleHierarchyImpl.getReachableGrantedAuthorities(authorities1), authorities2));
+        assertTrue(HierarchicalRolesTestHelper.containTheSameGrantedAuthorities(roleHierarchyImpl.getReachableGrantedAuthorities(authorities2), authorities2));
+    }
+
+    public void testTransitiveRoleHierarchies() {
+        GrantedAuthority[] authorities1 = new GrantedAuthority[] { new GrantedAuthorityImpl("ROLE_A") };
+        GrantedAuthority[] authorities2 = new GrantedAuthority[] { new GrantedAuthorityImpl("ROLE_A"), new GrantedAuthorityImpl("ROLE_B"), new GrantedAuthorityImpl("ROLE_C") };
+        GrantedAuthority[] authorities3 = new GrantedAuthority[] { new GrantedAuthorityImpl("ROLE_A"), new GrantedAuthorityImpl("ROLE_B"), new GrantedAuthorityImpl("ROLE_C"),
+                                                                   new GrantedAuthorityImpl("ROLE_D") };
+
+        RoleHierarchyImpl roleHierarchyImpl = new RoleHierarchyImpl();
+
+        roleHierarchyImpl.setHierarchy("ROLE_A > ROLE_B\nROLE_B > ROLE_C");
+        assertTrue(HierarchicalRolesTestHelper.containTheSameGrantedAuthorities(roleHierarchyImpl.getReachableGrantedAuthorities(authorities1), authorities2));
+
+        roleHierarchyImpl.setHierarchy("ROLE_A > ROLE_B\nROLE_B > ROLE_C\nROLE_C > ROLE_D");
+        assertTrue(HierarchicalRolesTestHelper.containTheSameGrantedAuthorities(roleHierarchyImpl.getReachableGrantedAuthorities(authorities1), authorities3));
+    }
+
+    public void testCyclesInRoleHierarchy() {
+        RoleHierarchyImpl roleHierarchyImpl = new RoleHierarchyImpl();
+
+        try {
+            roleHierarchyImpl.setHierarchy("ROLE_A > ROLE_A");
+            fail("Cycle in role hierarchy was not detected!");
+        } catch (CycleInRoleHierarchyException e) {}
+
+        try {
+            roleHierarchyImpl.setHierarchy("ROLE_A > ROLE_B\nROLE_B > ROLE_A");
+            fail("Cycle in role hierarchy was not detected!");
+        } catch (CycleInRoleHierarchyException e) {}
+
+        try {
+            roleHierarchyImpl.setHierarchy("ROLE_A > ROLE_B\nROLE_B > ROLE_C\nROLE_C > ROLE_A");
+            fail("Cycle in role hierarchy was not detected!");
+        } catch (CycleInRoleHierarchyException e) {}
+    }
+
+}

+ 58 - 0
core/src/test/java/org/acegisecurity/userdetails/hierarchicalroles/TestHelperTests.java

@@ -0,0 +1,58 @@
+/*
+ * 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.userdetails.hierarchicalroles;
+
+import junit.framework.TestCase;
+import junit.textui.TestRunner;
+
+import org.acegisecurity.GrantedAuthority;
+import org.acegisecurity.GrantedAuthorityImpl;
+
+/**
+ * Tests for {@link HierarchicalRolesTestHelper}.
+ *
+ * @author Michael Mayr
+ */
+public class TestHelperTests extends TestCase {
+
+    public TestHelperTests() {
+    }
+
+    public TestHelperTests(String testCaseName) {
+        super(testCaseName);
+    }
+
+    public void testContainTheSameGrantedAuthorities() {
+        GrantedAuthority[] authorities1 = new GrantedAuthority[] { new GrantedAuthorityImpl("ROLE_A"), new GrantedAuthorityImpl("ROLE_B") };
+        GrantedAuthority[] authorities2 = new GrantedAuthority[] { new GrantedAuthorityImpl("ROLE_B"), new GrantedAuthorityImpl("ROLE_A") };
+        GrantedAuthority[] authorities3 = new GrantedAuthority[] { new GrantedAuthorityImpl("ROLE_A"), new GrantedAuthorityImpl("ROLE_C") };
+        GrantedAuthority[] authorities4 = new GrantedAuthority[] { new GrantedAuthorityImpl("ROLE_A") };
+        GrantedAuthority[] authorities5 = new GrantedAuthority[] { new GrantedAuthorityImpl("ROLE_A"), new GrantedAuthorityImpl("ROLE_A") };
+
+        assertTrue(HierarchicalRolesTestHelper.containTheSameGrantedAuthorities(null, null));
+        assertTrue(HierarchicalRolesTestHelper.containTheSameGrantedAuthorities(authorities1, authorities1));
+        assertTrue(HierarchicalRolesTestHelper.containTheSameGrantedAuthorities(authorities1, authorities2));
+        assertTrue(HierarchicalRolesTestHelper.containTheSameGrantedAuthorities(authorities2, authorities1));
+
+        assertFalse(HierarchicalRolesTestHelper.containTheSameGrantedAuthorities(null, authorities1));
+        assertFalse(HierarchicalRolesTestHelper.containTheSameGrantedAuthorities(authorities1, null));
+        assertFalse(HierarchicalRolesTestHelper.containTheSameGrantedAuthorities(authorities1, authorities3));
+        assertFalse(HierarchicalRolesTestHelper.containTheSameGrantedAuthorities(authorities3, authorities1));
+        assertFalse(HierarchicalRolesTestHelper.containTheSameGrantedAuthorities(authorities1, authorities4));
+        assertFalse(HierarchicalRolesTestHelper.containTheSameGrantedAuthorities(authorities4, authorities1));
+        assertFalse(HierarchicalRolesTestHelper.containTheSameGrantedAuthorities(authorities4, authorities5));
+    }
+
+}

+ 75 - 0
core/src/test/java/org/acegisecurity/userdetails/hierarchicalroles/UserDetailsServiceWrapperTests.java

@@ -0,0 +1,75 @@
+package org.acegisecurity.userdetails.hierarchicalroles;
+
+import junit.textui.TestRunner;
+
+import org.acegisecurity.GrantedAuthority;
+import org.acegisecurity.GrantedAuthorityImpl;
+import org.acegisecurity.userdetails.User;
+import org.acegisecurity.userdetails.UserDetails;
+import org.acegisecurity.userdetails.UserDetailsService;
+import org.acegisecurity.userdetails.UsernameNotFoundException;
+import org.jmock.Mock;
+import org.jmock.MockObjectTestCase;
+import org.springframework.dao.DataAccessException;
+import org.springframework.dao.EmptyResultDataAccessException;
+
+public class UserDetailsServiceWrapperTests extends MockObjectTestCase {
+
+    private UserDetailsService wrappedUserDetailsService = null;
+    private UserDetailsServiceWrapper userDetailsServiceWrapper = null;
+    
+    public UserDetailsServiceWrapperTests() {
+        super();
+    }
+
+    public UserDetailsServiceWrapperTests(String testCaseName) {
+        super(testCaseName);
+    }
+
+    public static void main(String[] args) {
+        TestRunner.run(UserDetailsServiceWrapperTests.class);
+    }
+
+    protected void setUp() throws Exception {
+        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
+        roleHierarchy.setHierarchy("ROLE_A > ROLE_B");
+        GrantedAuthority[] authorities = new GrantedAuthority[] { new GrantedAuthorityImpl("ROLE_A") };
+        UserDetails user = new User("EXISTING_USER", "PASSWORD", true, true, true, true, authorities);
+        Mock wrappedUserDetailsServiceMock = mock(UserDetailsService.class);
+        wrappedUserDetailsServiceMock.stubs().method("loadUserByUsername").with(eq("EXISTING_USER")).will(returnValue(user));
+        wrappedUserDetailsServiceMock.stubs().method("loadUserByUsername").with(eq("USERNAME_NOT_FOUND_EXCEPTION")).will(throwException(new UsernameNotFoundException("USERNAME_NOT_FOUND_EXCEPTION")));
+        wrappedUserDetailsServiceMock.stubs().method("loadUserByUsername").with(eq("DATA_ACCESS_EXCEPTION")).will(throwException(new EmptyResultDataAccessException(1234)));
+        wrappedUserDetailsService = (UserDetailsService) wrappedUserDetailsServiceMock.proxy();
+        userDetailsServiceWrapper = new UserDetailsServiceWrapper();
+        userDetailsServiceWrapper.setRoleHierarchy(roleHierarchy);
+        userDetailsServiceWrapper.setUserDetailsService(wrappedUserDetailsService);
+    }
+
+    public void testLoadUserByUsername() {
+        GrantedAuthority[] authorities = new GrantedAuthority[] { new GrantedAuthorityImpl("ROLE_A"), new GrantedAuthorityImpl("ROLE_B") };
+        UserDetails expectedUserDetails = new User("EXISTING_USER", "PASSWORD", true, true, true, true, authorities);
+        UserDetails userDetails = userDetailsServiceWrapper.loadUserByUsername("EXISTING_USER");
+        assertEquals(expectedUserDetails.getPassword(), userDetails.getPassword()); 
+        assertEquals(expectedUserDetails.getUsername(), userDetails.getUsername());
+        assertEquals(expectedUserDetails.isAccountNonExpired(), userDetails.isAccountNonExpired());
+        assertEquals(expectedUserDetails.isAccountNonLocked(), userDetails.isAccountNonLocked());
+        assertEquals(expectedUserDetails.isCredentialsNonExpired(), expectedUserDetails.isCredentialsNonExpired());
+        assertEquals(expectedUserDetails.isEnabled(), userDetails.isEnabled());
+        assertTrue(HierarchicalRolesTestHelper.containTheSameGrantedAuthorities(expectedUserDetails.getAuthorities(), userDetails.getAuthorities()));
+        
+        try {
+            userDetails = userDetailsServiceWrapper.loadUserByUsername("USERNAME_NOT_FOUND_EXCEPTION");
+            fail("testLoadUserByUsername() - UsernameNotFoundException did not bubble up!");
+        } catch (UsernameNotFoundException e) {}
+        
+        try {
+            userDetails = userDetailsServiceWrapper.loadUserByUsername("DATA_ACCESS_EXCEPTION");
+            fail("testLoadUserByUsername() - DataAccessException did not bubble up!");
+        } catch (DataAccessException e) {}
+    }
+    
+    public void testGetWrappedUserDetailsService() {
+        assertTrue(userDetailsServiceWrapper.getWrappedUserDetailsService() == wrappedUserDetailsService);
+    }
+   
+}

+ 82 - 0
core/src/test/java/org/acegisecurity/userdetails/hierarchicalroles/UserDetailsWrapperTests.java

@@ -0,0 +1,82 @@
+package org.acegisecurity.userdetails.hierarchicalroles;
+
+import junit.framework.TestCase;
+import junit.textui.TestRunner;
+
+import org.acegisecurity.GrantedAuthority;
+import org.acegisecurity.GrantedAuthorityImpl;
+import org.acegisecurity.userdetails.User;
+import org.acegisecurity.userdetails.UserDetails;
+
+/**
+ * Tests for {@link UserDetailsWrapper}.
+ *
+ * @author Michael Mayr
+ */
+public class UserDetailsWrapperTests extends TestCase {
+
+    private GrantedAuthority[] authorities = null;
+    private UserDetails userDetails1 = null;
+    private UserDetails userDetails2 = null;
+    private UserDetailsWrapper userDetailsWrapper1 = null;
+    private UserDetailsWrapper userDetailsWrapper2 = null;
+
+    public UserDetailsWrapperTests() {
+    }
+
+    public UserDetailsWrapperTests(String testCaseName) {
+        super(testCaseName);
+    }
+
+    protected void setUp() throws Exception {
+        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
+        roleHierarchy.setHierarchy("ROLE_A > ROLE_B");
+        authorities = new GrantedAuthority[] { new GrantedAuthorityImpl("ROLE_A") };
+        userDetails1 = new User("TestUser1", "TestPassword1", true, true, true, true, authorities);
+        userDetails2 = new User("TestUser2", "TestPassword2", false, false, false, false, authorities);
+        userDetailsWrapper1 = new UserDetailsWrapper(userDetails1, roleHierarchy);
+        userDetailsWrapper2 = new UserDetailsWrapper(userDetails2, roleHierarchy);
+    }
+
+    public void testIsAccountNonExpired() {
+        assertEquals(userDetails1.isAccountNonExpired(), userDetailsWrapper1.isAccountNonExpired());
+        assertEquals(userDetails2.isAccountNonExpired(), userDetailsWrapper2.isAccountNonExpired());
+    }
+
+    public void testIsAccountNonLocked() {
+        assertEquals(userDetails1.isAccountNonLocked(), userDetailsWrapper1.isAccountNonLocked());
+        assertEquals(userDetails2.isAccountNonLocked(), userDetailsWrapper2.isAccountNonLocked());
+    }
+
+    public void testGetAuthorities() {
+        GrantedAuthority[] expectedAuthorities = new GrantedAuthority[] { new GrantedAuthorityImpl("ROLE_A"), new GrantedAuthorityImpl("ROLE_B") };
+        assertTrue(HierarchicalRolesTestHelper.containTheSameGrantedAuthorities(userDetailsWrapper1.getAuthorities(), expectedAuthorities));
+        assertTrue(HierarchicalRolesTestHelper.containTheSameGrantedAuthorities(userDetailsWrapper2.getAuthorities(), expectedAuthorities));
+    }
+
+    public void testIsCredentialsNonExpired() {
+        assertEquals(userDetails1.isCredentialsNonExpired(), userDetailsWrapper1.isCredentialsNonExpired());
+        assertEquals(userDetails2.isCredentialsNonExpired(), userDetailsWrapper2.isCredentialsNonExpired());
+    }
+
+    public void testIsEnabled() {
+        assertEquals(userDetails1.isEnabled(), userDetailsWrapper1.isEnabled());
+        assertEquals(userDetails2.isEnabled(), userDetailsWrapper2.isEnabled());
+    }
+
+    public void testGetPassword() {
+        assertEquals(userDetails1.getPassword(), userDetailsWrapper1.getPassword());
+        assertEquals(userDetails2.getPassword(), userDetailsWrapper2.getPassword());
+    }
+
+    public void testGetUsername() {
+        assertEquals(userDetails1.getUsername(), userDetailsWrapper1.getUsername());
+        assertEquals(userDetails2.getUsername(), userDetailsWrapper2.getUsername());
+    }
+
+    public void testGetUnwrappedUserDetails() {
+        assertTrue(userDetailsWrapper1.getUnwrappedUserDetails() == userDetails1);
+        assertTrue(userDetailsWrapper2.getUnwrappedUserDetails() == userDetails2);
+    }
+
+}