Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 5 years ago.
Improve this question
I have a problem to implement security in my application ...
I have custom authentication and use #PreAuthorize to handle my user authorization. This works fine. Now I want to implement Access Control for each user, which means in my application when two users, 'Admin' and 'John', could call method
#RequestMapping(value = "/load/{id}", method = RequestMethod.GET)
#ResponseBody
public StudentYearViewModel load(#PathVariable long id) {
return ModelMapper.map(iStudentService.loadByEntityId(id), StudentViewModel.class);
}
'Admin' can use this method for all Student instances but 'John' can see only his classmate!
All users could call this method (#PreAuthorize is not suitable) but their Access is limited HOW do it??
Now have general way?
is ACL best Way?(has best example?)
HDIV framework could help me solve my problem??
what is best solution???
You want to look at #PostFilter and #PreFilter. They work pretty much like #PreAuthorize, but can remove results from lists. You also want to assign different roles to your users, assuming you are not doing that already.
Global rules, like admin being able to see everything, you can implement by writing a concrete implementation of PermissionEvaluator. You then add that to the MethodSecurityExpressionHandler
Time for a simple example.
This code was written in a text editor. It may not compile and is only here to show the steps needed
A very simplistic PermissionEvaluator
public class MyPermissionEvaluator implements PermissionEvaluator {
private static final SimpleGrantedAuthority AUTHORITY_ADMIN = new SimpleGrantedAuthority('admin');
public boolean hasPermission(final Authentication authentication, final Object classId, final Object permission) {
boolean permissionGranted = false;
// admin can do anything
if (authentication.getAuthorities().contains(AUTHORITY_ADMIN)) {
permissionGranted = true;
} else {
// Check if the logged in user is in the same class
}
return permissionGranted;
}
#Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType,
Object permission) {
return false;
}
}
Then configure method security
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
#Bean
public MethodSecurityExpressionHandler methodSecurityExpressionHandler(final PermissionEvaluator permissionEvaluator){
DefaultMethodSecurityExpressionHandler securityExpressionHandler = new DefaultMethodSecurityExpressionHandler();
securityExpressionHandler.setPermissionEvaluator(permissionEvaluator);
return securityExpressionHandler;
}
#Bean
public PermissionEvaluator permissionEvaluator() {
return new MyPermissionEvaluator();
}
}
Now we can use our filter on a method
#PostFilter("hasPermission(filterObject.getClassId(), 'READ')")
#Override
public List<Student> getAll() {
return querySomeStudents();
}
hasPermission in the #PostFilter ACL will invoke hasPermission in MyPermissionEvaluator. filterObject refers to the individual items in the list. Wherever you code returns false, it will remove the item from the list.
Assign two different roles to Admin and John ROLE_ADMIN, ROLE_USER respectively. And then check role inside controller and call corresponding service method to return data according to their role.
#RequestMapping(value = "/load/{id}", method = RequestMethod.GET)
#ResponseBody
public StudentYearViewModel load(HttpServletRequest request, Authentication authentication, #PathVariable long id) {
if (request.isUserInRole("ROLE_ADMIN")) {
return ModelMapper.map(iStudentService.loadByEntityId(id), StudentViewModel.class); //return all records
} if (request.isUserInRole("ROLE_USER")) {
String username = authentication.getName(); //get logged in user i.e. john
return ModelMapper.map(iStudentService.loadByEntityId(id, username), StudentViewModel.class); //return records by username
}
}
Related
Prerequisite
I'm using Spring Security to authenticate my users with firebase. During authentication I also extract the users roles from the JWT token and convert them into SimpleGrantedAuthorities.
In most cases I can use #PreAuthorize("hasRole('ROLE_ADMIN')") to apply authorization to an endpoint. But now I have a more complex authorization scenario.
I have a Service that fetches a Product, but only users who have purchased the product are allowed to receive it.
fun fetchProduct(val id: Int, userId: String) {
val product = productRepository.findById(id)
// only users that purchased the product are allowed to fetch it!
if (!product.isAccessibleAtNoCharge(userId) && !purchaseCheck.hasUserPurchased(userId, product.id)) {
throw ForbiddenException("User has not purchased product")
}
return product
}
What I want to achieve
My requirement is, that users with the role ROLE_ADMIN can bypass that check, so that they can access products without purchasing them before.
What I have tried
My only idea so far is to retrieve the roles from SecurityContext like so:
fun fetchProduct(val id: Int, userId: String) {
val product = productRepository.findById(id)
val isAdmin = SecurityContextHolder.getContext().authentication.authorities.any { it.authority == "ROLE_ADMIN" }
if(isAdmin) {
return product
} else {
// check if purchased
...
}
}
Considerations
I have doubts that this solution is well testable, because of the static method call
I feel that the solution is not abstracted (mixing business logic and authorization logic)
Usually authorization seems to be done in the controller, which is not possible in this case?
Do you have suggestions for alternative solutions?
One way to abstract away the authorization logic is to create a PermissionEvaluator and put authorization logic there.
#Component
public class ProductPermissionEvaluator implements PermissionEvaluator {
#Override
public boolean hasPermission(Authentication authentication, Object id, Object role) {
Long productId = (Long) id;
Optional<Product> productOptional = productRepository.findById(productId);
boolean isAdmin = authentication.getAuthorities()
.stream()
.anyMatch(a -> a.getAuthority().equals(role));
boolean hasPurchased = // Logic to find if user has purchased the product
return isAdmin || hasPurchased;
}
#Override
public boolean hasPermission(Authentication authentication, Serializable id, String product, Object role) {
return false;
}
}
and use the PermissionEvaluator like this:
#PreAuthorize("hasPermission(#id, 'ROLE_ADMIN')")
public Product fetchProduct(Long id) { // Logic to fetch product }
You'll need to register the PermissionEvaluator as well, which you can do like this:
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecConfig extends GlobalMethodSecurityConfiguration {
#Autowired
private ProductPermissionEvaluator productPermissionEvaluator;
#Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
var expressionHandler = new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(productPermissionEvaluator);
return expressionHandler;
}
}
Additionally to the other answer, you can define your own bean with custom authorization logic for your products and call it inside an SPeL.
#Component
public class ProductPermissionEvaluator {
public boolean canFetchProduct(Long productId, Long userId) {
// perform your logic
}
}
#PreAuthorize("hasRole('ADMIN') || #productPermissionEvaluator.canFetchProduct(#id, #userId)")
fun fetchProduct(val id: Int, userId: String) {
...
}
This question already has answers here:
How to get active user's UserDetails
(9 answers)
Closed 5 years ago.
I would like to access some user details from session. I am using spring security and custom authentication by overriding loadUserByUsername(String username) method.
I am returning a user and would like to access it from within my controller. I tried the principal object but i can not reach to the companyId field of my ESecurityUser object.
Any help would be appreciated..
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
ESecurityUser user = new ESecurityUser();
user.setUsername("hello");
user.setPassword("world");
user.setCompanyId(199);
Set<EAuthority> authorities = new HashSet<EAuthority>();
EAuthority authority = new EAuthority();
authority.setAuthority("ROLE_ADMIN");
authorities.add(authority);
user.setAuthorities(authorities);;
return user;
}
Sample Controller Code
#RequestMapping("")
public String toPeriodicAdReport(#ModelAttribute("advertFormHelper") AdvertFormHelper advertFormHelper,
Model model,Principal principal) {
//What to write here so that i can access to authenticated user`s companyId field..
return "Test";
}
You can use the annotation #AuthenticationPrincipal to directly access ESecurityUser.
#RequestMapping("")
public String toPeriodicAdReport(#ModelAttribute("advertFormHelper") AdvertFormHelper advertFormHelper,
Model model, #AuthenticationPrincipal ESecurityUser principal) {
principal.getCompanyId();
return "Test";
}
You were not far...
The Principal that the SpringMVC machinery passed to a controller method is the Authentication token that identifies the user. You must use its getDetails() method to extract the ESecurityUser that you returned from your loadUserByUsername
Your code could become:
#RequestMapping("")
public String toPeriodicAdReport(#ModelAttribute("advertFormHelper") AdvertFormHelper advertFormHelper,
Model model,Principal principal) {
ESecurityUser user = (ESecurityUser) ((Authentication) principal).getDetails();
// stuff...
return "Test";
}
ESecurityUser e = (ESecurityUser)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
This is working for me..
I have two web applications (A and B).
The first web application (A) is used as a reverse proxy using spring-cloud.
I'm using spring-session to store the sessions in a redis database for both applications.
The problem
When I modify a field (e.g name) of the current (logged in) user, the current logged in user object is not updated immediately and as a result, when I'm trying to retrieve current logged in user in a next call (via #AuthenticationPrincipal) I get a non-updated user object.
My custom user details object:
public class CustomUserDetails extends my.package.User implements org.springframework.security.core.userdetails.UserDetails, java.io.Serializable {
// ...
}
How can I update the current user object immediately?
Recently I've had the similar issue and I resolved it in the following manner:
1.Created a custom Authentication class
public class MyCustomAuthentication implements Authentication {
private UserDetails userDetails;
public MyCustomAuthentication(UserDetails userDetails) {
this.userDetails = userDetails;
}
...
#Override
public Object getDetails() { return userDetails; }
#Override
public Object getPrincipal() { return userDetails; }
#Override
public boolean isAuthenticated() { return true; }
...
}
update userDetails object with some fresh data (I guess, 'name' in your case)
Set new authentication created from userDetails in SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(new MyCustomAuthentication(userDetails));
Hope you will find that helpful.
What I'm trying to accomplish is to access my Spring Session within a custom constraint
Sample scenario:
Custom constraint #UniqueEmail verifies that the email is not already in use in the system. This validation is performed on the edit endpoint and should be ignore if the user didn't change the email, 1st: because there's no need for it and 2nd: because querying the db for that email will actually return a result, which is the user itself, although there's no way of telling it without accessing the session
This works:
If I use model attributes with custom editor though #InitBinder I can set a property in the to-be-validated bean before the validation occurs like so
#InitBinder(value="myModelObj")
protected void initBinder(WebDataBinder binder, HttpSession session) {
User user = (User) session.getAttribute("user");
binder.registerCustomEditor(User.class, "user", new UidPropertyEditor(user));
}
#RequestMapping(...)
public String updateUser(#Valid #ModelAttribute("myModelObj") MyModelObj form){
...
}
MyModelObj has an attribute which will be replaced with the actual session user. Problems:
There must be a property in the bean to hold the user, even though it is not editable through the web form
The web form must submit this property as well, in my case using an input[type="hidden"] field (user can change it at will, we never trust what the user sends)
This does not work
The new endpoints have to use #RequestBody rather than #ModelAttribute, which means that (afaik) #InitBinder won't work anymore, hence losing access to the session object.
How (if possible) can I access the session from within the custom constraint?
public class EmailIsUniqueStringValidator implements ConstraintValidator<EmailIsUnique, String> {
#Autowired
private UserDAO userDAO;
HttpSession session; //Somehow initialized
#Override
public void boolean isValid(String email, ConstraintValidatorContext context) {
User user = (User) session.getAttribute("user");
if(user.getEmail().equals(email)){
return true; // No need to validate
}
else if(userDAO.emailInUse(email)) {
return false;
}
}
Non-ideal approach:
What I'm doing now is performing the session-dependant validations in the controller manually, which means I have 2 points where validation is performed.
There are some other interesting options in this post too, but if there was a way to access the session...
Thanks in advance
This can be achieved using RequestContextHolder like so:
public class EmailIsUniqueStringValidator implements ConstraintValidator<EmailIsUnique, String> {
#Autowired
private UserDAO userDAO;
HttpSession session;
#Override
public void boolean isValid(String email, ConstraintValidatorContext context) {
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
session = attr.getRequest().getSession();
User user = (User) session.getAttribute("user");
if(user.getEmail().equals(email)){
return true; // No need to validate
}
else if(userDAO.emailInUse(email)) {
return false;
}
}
I am using spring security for the authentication purposes in my project wherein after successful authentication, I get the principal object inside which the various details are stored.
This principal object is passed to various methods which allow the entries to be reflected in the database against the current user. In short, principal helps me in giving principal.getName() everywhere i need it.
But now when I login through spring social then I do not have principal object of Principal in hand, instead I have implemented MyPrincipal class --->
public class MyPrincipal implements Principal {
public String name;
public boolean flag;
public boolean isflag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public void setName(String name) {
this.name = name;
}
#Override
public String getName() {
return name;
}
}
Then in the social login handler, I am adding the current username and flag value to myPrincipal object, and forwarding the user to the same home page where the spring security forwards in case of normal login.
MyPrincipal myPrincipal = new MyPrincipal();
myPrincipal.name = username;
myPrincipal.socialFlag = true;
modelMap.addAttribute("myPrincipal", myPrincipal);
return new ModelAndView("forward:/home");
Adding this object in session by annotating class with
#SessionAttributes({"myPrincipal"})
Now from here on-wards I want the flow to be handed over to the home page with all the functionality working for the user correctly. But each method is taking Principal principal as argument, just like this -->
#RequestMapping(value = {"/home"}, method = RequestMethod.POST)
#ResponseBody
public ModelAndView test(ModelMap modelMap, Principal principal) {
String name = principal.getName();
}
There are two different things going around in both cases-
Normal login is giving me principal directly but social login is giving me it in session attributes.
I do not want to pass principal as parameters even in case of normal spring security login, instead here also I want to put it in session attribute.
How can I do this and where to make the changes when I have implemented my own authentication provider.
I don't think I fully understand...However, in general it shouldn't be necessary to pass principal instances around. Use org.springframework.security.core.context.SecurityContextHolder.getContext() to get a hold of the context then call SecurityContext.getAuthentication().getPrincipal() or SecurityContext.getAuthentication().getDetails().