Spring Security hasPermission for Collection<Object>
I have a couple of workarounds.
1. The first one is to use my own MethodSecurityExpressionHandler
and MethodSecurityExpressionRoot
.
Creating a CustomMethodSecurityExpressionRoot
and define a method which will be our new expression for Collection
handling. It will extend SecurityExpressionRoot
to include default expressions:
public class CustomMethodSecurityExpressionRoot extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {
private final PermissionEvaluator permissionEvaluator;
private final Authentication authentication;
private Object filterObject;
private Object returnObject;
private Object target;
public CustomMethodSecurityExpressionRoot(Authentication authentication, PermissionEvaluator permissionEvaluator) {
super(authentication);
this.authentication = authentication;
this.permissionEvaluator = permissionEvaluator;
super.setPermissionEvaluator(permissionEvaluator);
}
public boolean hasAccessToCollection(Collection<Object> collection, String permission) {
for (Object object : collection) {
if (!permissionEvaluator.hasPermission(authentication, object, permission))
return false;
}
return true;
}
@Override
public void setFilterObject(Object filterObject) {
this.filterObject = filterObject;
}
@Override
public Object getFilterObject() {
return filterObject;
}
@Override
public void setReturnObject(Object returnObject) {
this.returnObject = returnObject;
}
@Override
public Object getReturnObject() {
return returnObject;
}
@Override
public Object getThis() {
return target;
}
}
Create custom expression handler and inject CustomMethodSecurityExpressionRoot
:
public class CustomMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler {
private final PermissionEvaluator permissionEvaluator;
public CustomMethodSecurityExpressionHandler(PermissionEvaluator permissionEvaluator) {
this.permissionEvaluator = permissionEvaluator;
super.setPermissionEvaluator(permissionEvaluator);
}
@Override
protected MethodSecurityExpressionOperations createSecurityExpressionRoot(
Authentication authentication, MethodInvocation invocation) {
CustomMethodSecurityExpressionRoot root =
new CustomMethodSecurityExpressionRoot(authentication, permissionEvaluator);
root.setTrustResolver(new AuthenticationTrustResolverImpl());
root.setRoleHierarchy(getRoleHierarchy());
return root;
}
}
I also injected SecurityPermissionEvaluator
used in question, so it will be a single point of entry for custom and default expressions. As an alternate option we could inject and use PermissionService
directly.
Configuring our method-level security:
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Autowired
private PermissionService permissionService;
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
PermissionEvaluator permissionEvaluator = new SecurityPermissionEvaluator(permissionService);
return new CustomMethodSecurityExpressionHandler(permissionEvaluator);
}
}
Now we can use new expression in RestController
:
@PreAuthorize("hasAccessToCollection(#products, 'WRITE')")
@RequestMapping(value = "/saveCollection", method = RequestMethod.POST)
public Collection<Product> save(@RequestBody Collection<Product> products) {
return productService.save(products);
}
As a result a part with handling collection in PermissionService
could be omitted as we took out this logic to custom expression.
2. The second workaround is to call method directly using SpEL.
Now I'm using PermissionEvaluator
as Spring bean (any service could be used here, but I'm preferring single point of entry again)
@Component
public class SecurityPermissionEvaluator implements PermissionEvaluator {
@Autowired
private PermissionService permissionService;
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
if (!(targetDomainObject instanceof TopAppEntity))
throw new IllegalArgumentException();
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
return permissionService.isAuthorized(userDetails.getUser(), targetDomainObject, permission.toString());
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
try {
return permissionService.isAuthorized(userDetails.getUser(), targetId,
Class.forName(targetType), String.valueOf(permission));
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException("No class found " + targetType);
}
}
public boolean hasPermission(Authentication authentication, Collection<Object> targetDomainObjects, Object permission) {
for (Object targetDomainObject : targetDomainObjects) {
if (!hasPermission(authentication, targetDomainObject, permission))
return false;
}
return true;
}
}
Configuring method security:
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Autowired
private PermissionEvaluator permissionEvaluator;
@Autowired
private ApplicationContext applicationContext;
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler =
new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(permissionEvaluator);
// Pay attention here, or Spring will not be able to resolve bean
expressionHandler.setApplicationContext(applicationContext);
return expressionHandler;
}
}
Usage of the service in expression:
@PreAuthorize("@securityPermissionEvaluator.hasPermission(authentication, #products, 'WRITE')")
@RequestMapping(value = "/saveCollection", method = RequestMethod.POST)
public Collection<Product> save(@RequestBody Collection<Product> products) {
return productService.save(products);
}
Spring beans created by default with class name if no other name specified.
Summary: both approaches based on using custom services calling them directly or registering them as expressions and could handle the logic of collection before it will be sent to authority checking service, so we can omit the part of it:
@Service
public class PermissionService {
public boolean isAuthorized(User user, TopAppEntity domainEntity, String permission) {
// removed instanceof checks and can operate on domainEntity directly
if (domainEntity.getId() == null) {
// check authorities and give response
} else {
// check ACL and give response
}
}
}
Yes, there is a smart way. I can tell you what I did.
@Component("MySecurityPermissionEvaluator ")
@Scope(value = "session")
public class PermissionService {
@Autowired
private PermissionEvaluator permissionEvaluator;
public boolean myPermission(Object obj, String permission) {
boolean isAuthorized = false;
Authentication a = SecurityContextHolder.getContext()
.getAuthentication();
if (null == obj) {
return isAuthorized;
}
if (a.getAuthorities().size() == 0) {
logger.error("For this authenticated object, no authorities could be found !");
return isAuthorized;
} else {
logger.error("Authorities found " + a.getAuthorities());
}
try {
isAuthorized = myPermissionEval
.hasPermission(a, obj, permission);
} catch (Exception e) {
logger.error("exception while analysisng permissions");
}
return isAuthorized;
}
Please do not use hard coded permissions, Use this way instead,
import org.springframework.security.acls.domain.DefaultPermissionFactory;
public class MyPermissionFactory extends DefaultPermissionFactory {
public MyPermissionFactory() {
registerPublicPermissions(MyPermission.class);
}
}
To make custom permissions,
import org.springframework.security.acls.domain.BasePermission;
public class MyPermission extends BasePermission { //use this class for creating custom permissions
private static Map<String, Integer> customPerMap = new HashMap<String, Integer>();
static {
customPerMap.put("READ", 1);
customPerMap.put("WRITE", 2);
customPerMap.put("DELETE", 4);
customPerMap.put("PUT", 8);
}
/**
*Use the function while saving/ getting permission code
**/
public static Integer getCode(String permName) {
return customPerMap.get(permName.toUpperCase());
}
If you need to authenticate urls based on admin users or role hierarchy, use tag in Spring Authentication not Authorization.
Rest, you are using correctly, @PreAuthorize and @PreFilter both are correct and used acco to requirements.