/*
* 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 fr.openwide.core.jpa.security.hierarchy;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.acls.domain.PermissionFactory;
import org.springframework.security.acls.model.Permission;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.SetMultimap;
/**
* <p>
* This class defines a permission hierarchy for use with the UserDetailsServiceWrapper.
* </p>
* <p>
* Here is an example configuration of a permission 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>
* <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 Permissions will dramatically shorten your access rules (and also make the access rules much more elegant).
* </p>
* <p>
* Consider this access rule for Spring Security's PermissionVoter (background: every user that is authenticated should be
* able to log out):<br>
* /logout.html=ROLE_A,ROLE_B,ROLE_AUTHENTICATED<br>
* With hierarchical permissions 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 PermissionHierarchyImpl implements IPermissionHierarchy, Serializable {
private static final long serialVersionUID = -6242073226861679992L;
private static final Logger LOGGER = LoggerFactory.getLogger(PermissionHierarchyImpl.class);
private String permissionHierarchyStringRepresentation = null;
/**
* permissionsAcceptableInOneStepMap is a Map that under the key of a specific permission name contains a set of all permissions
* that can be accepted in place of this permission in 1 step
*/
private SetMultimap<Permission, Permission> permissionsReachableInOneStepMap = null;
/**
* permissionsAcceptableInOneStepMap is a Map that under the key of a specific permission name contains a set of all permissions
* reachable from this permission in 1 step
*/
private SetMultimap<Permission, Permission> permissionsAcceptableInOneStepMap = null;
/**
* permissionsAcceptableInOneOrMoreStepsMap is a Map that under the key of a specific permission name contains a set of all
* permissions that can be accepted in place of this permission in 1 or more steps
*/
private SetMultimap<Permission, Permission> permissionsAcceptableInOneOrMoreStepsMap = null;
/**
* permissionsAcceptableInOneOrMoreStepsMap is a Map that under the key of a specific permission name contains a set of all
* permissions reachable from this permission in 1 or more steps
*/
private SetMultimap<Permission, Permission> permissionsReachableInOneOrMoreStepsMap = null;
private PermissionFactory permissionFactory;
public PermissionHierarchyImpl(PermissionFactory permissionFactory) {
this.permissionFactory = permissionFactory;
}
/**
* Set the permission hierarchy and precalculate for every permission the set of all acceptable permissions, i. e. all permissions lower in
* the hierarchy of every given permission. Precalculation is done for performance reasons (acceptable permissions can then be
* calculated in O(1) time).
* During precalculation cycles in permission hierarchy are detected and will cause a
* <tt>CycleInPermissionHierarchyException</tt> to be thrown.
*
* @param permissionHierarchyStringRepresentation - String definition of the permission hierarchy.
*/
public void setHierarchy(String permissionHierarchyStringRepresentation) {
this.permissionHierarchyStringRepresentation = permissionHierarchyStringRepresentation;
LOGGER.debug("setHierarchy() - The following permission hierarchy was set: " + permissionHierarchyStringRepresentation);
buildOneStepRelationsMaps();
this.permissionsAcceptableInOneOrMoreStepsMap = buildClosures(permissionsAcceptableInOneStepMap);
this.permissionsReachableInOneOrMoreStepsMap = buildClosures(permissionsReachableInOneStepMap);
}
@Override
public List<Permission> getAcceptablePermissions(Permission permission) {
return getAcceptablePermissions(ImmutableSet.of(permission));
}
@Override
public List<Permission> getAcceptablePermissions(Collection<Permission> permissions) {
if (permissions == null || permissions.size() == 0) {
return new ArrayList<Permission>(0);
}
Set<Permission> acceptablePermissions = new HashSet<Permission>();
for (Permission permission : permissions) {
acceptablePermissions.add(permission);
Set<Permission> additionalAcceptablePermissions = permissionsAcceptableInOneOrMoreStepsMap.get(permission);
if (additionalAcceptablePermissions != null) {
acceptablePermissions.addAll(additionalAcceptablePermissions);
}
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("getAcceptablePermissions() - For one of the permissions " + permissions
+ " one can accept any of " + acceptablePermissions);
}
return new ArrayList<Permission>(acceptablePermissions);
}
@Override
public List<Permission> getReachablePermissions(Permission permission) {
return getReachablePermissions(ImmutableSet.of(permission));
}
@Override
public List<Permission> getReachablePermissions(Collection<Permission> permissions) {
if (permissions == null || permissions.size() == 0) {
return new ArrayList<Permission>(0);
}
Set<Permission> reachablePermissions = new HashSet<Permission>();
for (Permission permission : permissions) {
reachablePermissions.add(permission);
Set<Permission> additionalReachablePermissions = permissionsReachableInOneOrMoreStepsMap.get(permission);
if (additionalReachablePermissions != null) {
reachablePermissions.addAll(additionalReachablePermissions);
}
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("getReachablePermissions() - From the permissions " + permissions
+ " one can reach " + reachablePermissions + " in zero or more steps.");
}
return new ArrayList<Permission>(reachablePermissions);
}
/**
* Parse input and build the map for the permissions reachable in one step: the higher permission will become a key that
* references a set of the reachable lower permissions.
*/
private void buildOneStepRelationsMaps() {
String parsingRegex = "(\\s*([^\\s>]+)\\s*\\>\\s*([^\\s>]+))";
Pattern pattern = Pattern.compile(parsingRegex);
Matcher permissionHierarchyMatcher = pattern.matcher(permissionHierarchyStringRepresentation);
permissionsReachableInOneStepMap = HashMultimap.create();
permissionsAcceptableInOneStepMap = HashMultimap.create();
while (permissionHierarchyMatcher.find()) {
String higherPermissionString = permissionHierarchyMatcher.group(2);
Permission higherPermission = permissionFactory.buildFromName(higherPermissionString);
if (higherPermission == null) {
LOGGER.error(String.format("Unable to build permission %1$s: risk of inconsistent hierarchy", higherPermissionString));
continue;
}
String lowerPermissionString = permissionHierarchyMatcher.group(3);
Permission lowerPermission = permissionFactory.buildFromName(lowerPermissionString);
if (lowerPermission == null) {
LOGGER.error(String.format("Unable to build permission %1$s: risk of inconsistent hierarchy", lowerPermissionString));
continue;
}
permissionsReachableInOneStepMap.put(higherPermission, lowerPermission);
permissionsAcceptableInOneStepMap.put(lowerPermission, higherPermission);
LOGGER.debug("buildPermissionsAcceptableInOneStepMap() - From permission "
+ higherPermission + " one can reach permission " + lowerPermission + " in one step.");
}
}
private SetMultimap<Permission, Permission> buildClosures(SetMultimap<Permission, Permission> oneStepRelations) {
SetMultimap<Permission, Permission> closures = HashMultimap.create();
// iterate over all higher permissions from permissionsAcceptableInOneStepMap
Iterator<Permission> permissionIterator = oneStepRelations.keySet().iterator();
while (permissionIterator.hasNext()) {
Permission permission = (Permission) permissionIterator.next();
Set<Permission> permissionsToVisitSet = new HashSet<Permission>();
if (oneStepRelations.containsKey(permission)) {
permissionsToVisitSet.addAll(oneStepRelations.get(permission));
}
Set<Permission> visitedPermissionsSet = new HashSet<Permission>();
while (!permissionsToVisitSet.isEmpty()) {
// take a permission from the permissionsToVisit set
Permission aPermission = (Permission) permissionsToVisitSet.iterator().next();
permissionsToVisitSet.remove(aPermission);
visitedPermissionsSet.add(aPermission);
if (closures.containsKey(aPermission)) {
Set<Permission> newClosure = (Set<Permission>) closures.get(aPermission);
// definition of a cycle: you can reach the permission you are starting from
if (permissionsToVisitSet.contains(permission) || visitedPermissionsSet.contains(permission)) {
throw new CycleInPermissionHierarchyException();
} else {
// no cycle
permissionsToVisitSet.addAll(newClosure);
}
}
}
closures.putAll(permission, visitedPermissionsSet);
}
return closures;
}
}