/*
* Copyright 2002-2016 the original author or authors.
*
* 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.springframework.security.access.hierarchicalroles;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
/**
* <p>
* This class defines a role hierarchy for use with the UserDetailsServiceWrapper.
*
* <p>
* Here is an example configuration of a role hierarchy (hint: read the ">" sign as
* "includes"):
*
* <pre>
* <property name="hierarchy">
* <value>
* ROLE_A > ROLE_B
* ROLE_B > ROLE_AUTHENTICATED
* ROLE_AUTHENTICATED > ROLE_UNAUTHENTICATED
* </value>
* </property>
* </pre>
*
* <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>
* Hierarchical Roles will dramatically shorten your access rules (and also make the
* access rules much more elegant).
*
* <p>
* Consider this access rule for Spring Security'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.
*
* @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<GrantedAuthority, Set<GrantedAuthority>> 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<GrantedAuthority, Set<GrantedAuthority>> rolesReachableInOneOrMoreStepsMap = null;
/**
* Set the role hierarchy and pre-calculate for every role the set of all reachable
* roles, i.e. all roles lower in the hierarchy of every given role. Pre-calculation
* is done for performance reasons (reachable roles can then be calculated in O(1)
* time). During pre-calculation, 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 Collection<GrantedAuthority> getReachableGrantedAuthorities(
Collection<? extends GrantedAuthority> authorities) {
if (authorities == null || authorities.isEmpty()) {
return AuthorityUtils.NO_AUTHORITIES;
}
Set<GrantedAuthority> reachableRoles = new HashSet<GrantedAuthority>();
for (GrantedAuthority authority : authorities) {
addReachableRoles(reachableRoles, authority);
Set<GrantedAuthority> additionalReachableRoles = getRolesReachableInOneOrMoreSteps(
authority);
if (additionalReachableRoles != null) {
reachableRoles.addAll(additionalReachableRoles);
}
}
if (logger.isDebugEnabled()) {
logger.debug("getReachableGrantedAuthorities() - From the roles "
+ authorities + " one can reach " + reachableRoles
+ " in zero or more steps.");
}
List<GrantedAuthority> reachableRoleList = new ArrayList<GrantedAuthority>(
reachableRoles.size());
reachableRoleList.addAll(reachableRoles);
return reachableRoleList;
}
// SEC-863
private void addReachableRoles(Set<GrantedAuthority> reachableRoles,
GrantedAuthority authority) {
for (GrantedAuthority testAuthority : reachableRoles) {
String testKey = testAuthority.getAuthority();
if ((testKey != null) && (testKey.equals(authority.getAuthority()))) {
return;
}
}
reachableRoles.add(authority);
}
// SEC-863
private Set<GrantedAuthority> getRolesReachableInOneOrMoreSteps(
GrantedAuthority authority) {
if (authority.getAuthority() == null) {
return null;
}
for (GrantedAuthority testAuthority : this.rolesReachableInOneOrMoreStepsMap
.keySet()) {
String testKey = testAuthority.getAuthority();
if ((testKey != null) && (testKey.equals(authority.getAuthority()))) {
return this.rolesReachableInOneOrMoreStepsMap.get(testAuthority);
}
}
return null;
}
/**
* 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() {
Pattern pattern = Pattern.compile("(\\s*([^\\s>]+)\\s*>\\s*([^\\s>]+))");
Matcher roleHierarchyMatcher = pattern
.matcher(this.roleHierarchyStringRepresentation);
this.rolesReachableInOneStepMap = new HashMap<GrantedAuthority, Set<GrantedAuthority>>();
while (roleHierarchyMatcher.find()) {
GrantedAuthority higherRole = new SimpleGrantedAuthority(
roleHierarchyMatcher.group(2));
GrantedAuthority lowerRole = new SimpleGrantedAuthority(
roleHierarchyMatcher.group(3));
Set<GrantedAuthority> rolesReachableInOneStepSet;
if (!this.rolesReachableInOneStepMap.containsKey(higherRole)) {
rolesReachableInOneStepSet = new HashSet<GrantedAuthority>();
this.rolesReachableInOneStepMap.put(higherRole,
rolesReachableInOneStepSet);
}
else {
rolesReachableInOneStepSet = this.rolesReachableInOneStepMap
.get(higherRole);
}
addReachableRoles(rolesReachableInOneStepSet, 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() {
this.rolesReachableInOneOrMoreStepsMap = new HashMap<GrantedAuthority, Set<GrantedAuthority>>();
// iterate over all higher roles from rolesReachableInOneStepMap
for (GrantedAuthority role : this.rolesReachableInOneStepMap.keySet()) {
Set<GrantedAuthority> rolesToVisitSet = new HashSet<GrantedAuthority>();
if (this.rolesReachableInOneStepMap.containsKey(role)) {
rolesToVisitSet.addAll(this.rolesReachableInOneStepMap.get(role));
}
Set<GrantedAuthority> visitedRolesSet = new HashSet<GrantedAuthority>();
while (!rolesToVisitSet.isEmpty()) {
// take a role from the rolesToVisit set
GrantedAuthority aRole = rolesToVisitSet.iterator().next();
rolesToVisitSet.remove(aRole);
addReachableRoles(visitedRolesSet, aRole);
if (this.rolesReachableInOneStepMap.containsKey(aRole)) {
Set<GrantedAuthority> newReachableRoles = this.rolesReachableInOneStepMap
.get(aRole);
// definition of a cycle: you can reach the role you are starting from
if (rolesToVisitSet.contains(role)
|| visitedRolesSet.contains(role)) {
throw new CycleInRoleHierarchyException();
}
else {
// no cycle
rolesToVisitSet.addAll(newReachableRoles);
}
}
}
this.rolesReachableInOneOrMoreStepsMap.put(role, visitedRolesSet);
logger.debug("buildRolesReachableInOneOrMoreStepsMap() - From role " + role
+ " one can reach " + visitedRolesSet + " in one or more steps.");
}
}
}