I have working application secured with method-level security:
RestController:
@PreAuthorize("hasPermission(#product, 'WRITE')")
@RequestMapping(value = "/save", method = RequestMethod.POST)
public Product save(@RequestBody Product product) {
return productService.save(product);
}
PermissionEvaluator:
public class SecurityPermissionEvaluator implements PermissionEvaluator {
private Logger log = LoggerFactory.getLogger(SecurityPermissionEvaluator.class);
private final PermissionService permissionService;
public SecurityPermissionEvaluator(PermissionService permissionService) {
this.permissionService = permissionService;
}
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
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) {
// almost the same implementation
}
}
And everything works fine until I implemented API which saves collection of objects. The logic of this service is to update existing entities and/or create new entities.
@PreAuthorize("hasPermission(#products, 'WRITE')")
@RequestMapping(value = "/saveCollection", method = RequestMethod.POST)
public Collection<Product> save(@RequestBody Collection<Product> products) {
return productService.save(products);
}
After this my permission service handles the collection object and looks like this now:
PemissionService:
public class PermissionService {
public boolean isAuthorized(User user, Object targetDomainObject, String permission) {
if (targetDomainObject instanceof TopAppEntity) {
if (((TopAppEntity) targetDomainObject).getId() == null) {
// check authorities and give response
} else {
// check ACL and give response
}
} else if(targetDomainObject instanceof Collection) {
boolean isAuthorized = false;
Collection targetDomainObjects = (Collection) targetDomainObject;
for (Object targetObject : targetDomainObjects) {
isAuthorized = isAuthorized(user, targetObject, permission);
if (!isAuthorized) break;
}
return isAuthorized;
}
}
}
My question is:
How I can handle collections using @PreAuthorize("hasPermission(#object, '...')")
more elegant way? Is there some implementations in Spring Security for handling collections? At least, how can I optimize PemissionService
for handling Collections
?
hasRole, hasAnyRole. These expressions are responsible for defining the access control or authorization to specific URLs and methods in our application: @Override protected void configure(final HttpSecurity http) throws Exception { ... . antMatchers("/auth/admin/*").
@PreAuthorize annotation can also be used with hasAuthority(). When using hasAuthority() expression, you will need to provide a complete authority name.
By default, Spring Security uses a thread-local copy of this class. This means each request in our application has its security context that contains details of the user making the request. To use it, we simply call the static methods in SecurityContextHolder: Authentication auth = SecurityContextHolder.
For adding a Spring Boot Security to your Spring Boot application, we need to add the Spring Boot Starter Security dependency in our build configuration file. Maven users can add the following dependency in the pom. xml file. Gradle users can add the following dependency in the build.
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.
You can use the @PreFilter
annotation.
So @PreFilter("hasPermission(filterTarget, '...')")
will call your PermissionService for each element of the Collection.
public class PermissionService() {
public boolean isAuthorized(User user, Object targetDomainObject, String permission) {
if (targetDomainObject instanceof TopAppEntity) {
if (((TopAppEntity) targetDomainObject).getId() == null) {
// check authorities and give response
} else {
// check ACL and give response
}
}
}
}
Note: this will not prevent a call of your controller method. It only gets an empty Collection.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With