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?



  • 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.



最新帖子

最新内容

  • S

    Try using 'graypy'. It lets you post logs to graylog using TCP port 12201 for the input streams creating. It also lets you add additional parameters, that you can tag to a message using adapters. The documentation has examples for adapters.

    read more
  • S

    I am trying to send Python List as Log Message to Graylog. The approach that i am using is "Sending GELF messages via HTTP using curl" mentioned in http://docs.graylog.org/en/2.4/pages/gelf.html

    But when i send data as below:

    curl -X POST -H 'Content-Type: application/json' -d '{"log_type":"debug", "short_message": "[1,4,5,2]", "block_id":"TEST_LOGGING"}' 'http://<host>:12201/gelf'

    it works perfectly fine.

    Where as on sending message as below Logs nothing.

    curl -X POST -H 'Content-Type: application/json' -d '{"log_type":"debug", "short_message": [1,4,5,2], "block_id":"TEST_LOGGING"}' 'http://<host>:12201/gelf'

    I am unable to figure out the issue.

    read more
  • S

    Here My solution using the function Just assign a integer value to the string and then compare the integer value.

    CREATE function Weight(tag VARCHAR(200)) RETURNS INT DETERMINISTIC BEGIN DECLARE v INT; IF tag='High' THEN SET v=1; END IF; IF tag='Very High' THEN SET v=2; END IF; RETURN v; END;|

    Then use the query

    Select * From tablename Where max_pos> 4 AND max_neg is >1 AND Weight(intensity_level)>Weight(excitement_level);

    The Weight function above maybe not work in SQL, I used MySQL so change accordingly.

    read more
  • S

    try using DECODE. DECODE high, very high values with a number and alias and then select all from this result set, then compare the aliases

    read more
  • S

    You can create a table that contains intensity_level and excitement_level for each rollercoaster as integers and an ID that links them to your main table

    read more

推荐阅读

  • 1
  • 4
  • 9
  • 5
  • 1
  • 1
  • 1
  • 8