Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Security hasPermission for Collection<Object>

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?

like image 256
J-Alex Avatar asked May 29 '17 23:05

J-Alex


People also ask

What is hasRole and hasAnyRole?

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/*").

What is @PreAuthorize hasAuthority?

@PreAuthorize annotation can also be used with hasAuthority(). When using hasAuthority() expression, you will need to provide a complete authority name.

How does Spring Security hasRole work?

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.

How do I ensure security in spring boot?

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.


3 Answers

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         }     } } 
like image 73
J-Alex Avatar answered Sep 30 '22 17:09

J-Alex


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.

like image 36
hi.nitish Avatar answered Sep 30 '22 17:09

hi.nitish


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.

like image 29
benkuly Avatar answered Sep 30 '22 15:09

benkuly