Search code examples
spring-securityauthorizationmanager

Custom method in @PreAuthorize


I'm on java 17. Newest Spring Security version 6.1.4.

I'm trying to create a custom AuthorizationManager and I think I managed to do that without too much fuss.

@Component
public class CustomAuthorizationManager implements AuthorizationManager<MethodInvocation> {

    @Autowired
    private RoleRepository roleRepository;

    private boolean onlyBUCheck;
    private GrantedAuthority buToCheck = null;
    private GrantedAuthority authorityToCheck = null;

    public CustomAuthorizationManager(){
        System.out.println("created through here");
    };

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation) {
        //calling buCheck and authorityCheck
    }

    private AuthorizationDecision buCheck(Authentication authentication){
        //Logic
    }

    private AuthorizationDecision authorityCheck(Authentication authentication){
        //Logic
    }

    public void partOfBU(String bu){
        Assert.notNull(bu, "role cannot be null");
        this.onlyBUCheck = true;
        this.buToCheck = () -> bu;
    }

    public void hasAuthority(String bu, String authority) {
        Assert.notNull(authority, "role cannot be null");
        this.onlyBUCheck = false;
        this.buToCheck = () -> bu;
        this.authorityToCheck = () -> authority;
    }

So after I created an endpoint to test it out.

    @PostMapping("/wtf")
    @PreAuthorize("customAuthorizationManager.partOfBU(bu.id())") //I get autocomplete here
    public ResponseEntity<Object> aaa(@P("bu") @RequestBody BusinessUnitDTO businessUnitDTO){
        System.out.println(businessUnitDTO);
        return new ResponseEntity<>(HttpStatus.OK);
    }

Since I'm getting autocomplete on the @PreAuthorize I guess everything was set up correctly. But when I run it the method partOfBU() is not executed. Just the check() method (which ofc fails cuz the stuff hasn't been set up).

If I just plainly do @EnableMethodSecurity(prePostEnabled = true) I'm getting this exception org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field 'customAuthorizationManager' cannot be found on object of type 'org.springframework.security.access.expression.method.MethodSecurityExpressionRoot' - maybe not public or not valid? From my basic understanding this means that customAuthorizationManager bean hasn't been injected into somewhere and cuz of that I can't use it in @PreAuthorize

So then I found this in the docs https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html#custom-authorization-managers

@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE) //Tried both with and without this. no idea what it does
    Advisor preAuthorize(CustomAuthorizationManager manager) {
        return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager);
    }
}

This solved the exception and now it's only that partOfBU() isn't executed even though I'm calling it there. (I guess cuz I injected it in a needed place)

Tried this too and I'm getting the same result so I guess the part after the dot is ignored for some reason.

    @PostMapping("/wtf")
    @PreAuthorize("customAuthorizationManager") //not calling the partOfBU() method
    public ResponseEntity<Object> aaa(@P("bu") @RequestBody BusinessUnitDTO businessUnitDTO){
        System.out.println(businessUnitDTO);
        return new ResponseEntity<>(HttpStatus.OK);
    }

Turns out @PreAuthorize doesn't care wtf is in there. It just calls attemptAuthorization() in AuthorizationManagerBeforeMethodInterceptor (where we set the custom AuthorizationManager)

private void attemptAuthorization(MethodInvocation mi) {
        this.logger.debug(LogMessage.of(() -> {
            return "Authorizing method invocation " + mi;
        }));
        AuthorizationDecision decision = this.authorizationManager.check(this.authentication, mi);
        this.eventPublisher.publishAuthorizationEvent(this.authentication, mi, decision);
        if (decision != null && !decision.isGranted()) {
            this.logger.debug(LogMessage.of(() -> {
                return "Failed to authorize " + mi + " with authorization manager " + this.authorizationManager + " and decision " + decision;
            }));
            throw new AccessDeniedException("Access Denied");
        } else {
            this.logger.debug(LogMessage.of(() -> {
                return "Authorized method invocation " + mi;
            }));
        }
    }

I think the mistake I've made is mixing up how method authorization manager and the one used in the config files with with requestMatchers should look. I was implementing mine while looking at the AuthorityAuthorizationManager implementation instead of the PreAuthorizeAuthorizationManager. Will try to make an implementation closer to the one in the PreAuthorizeAuthorizationManager and come back with results. (Just have to figure out how to use SpEL and the classes related to it)


Solution

  • Well managed to get it working. I'll post it here in case anyone is having the same problem in the future. This might be an old way to handle things (from what I understood from the spring docs)

    Now I followed the guide dan1st suggested pretty closely doing modifications on my own.

    I did it with a custom AuthorizationManager. There are other ways to substitute that part but this is how I did it.

    Firstly we start with a configuration class. (if you don't know wtf that is google spring configuration class)

    @Configuration
    @EnableMethodSecurity(prePostEnabled = false) //1
    class MethodSecurityConfig {
    
        @Bean
        public Advisor preAuthorize(CustomAuthorizationManager manager) { //2
            return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager);
        }
    
    }
    
    1. When we enable method security spring has a default implementation. And we want to overwrite it. So we disable it.
    2. This is the method that "injects" the CustomAuthorizationManager in the correct place. THIS ONLY DOES THE PreAuthorize METHOD. If you want to use the others you'll have to overwrite their methods as well. (Shown in old docs) For some reason in the newer versions they decided to make the example smaller and thus harder to understand

    Now how to make a custom AuthorizationManager. I was trying to copy from a few other implementations and I arrived at the conclusion that there is a difference between the method ones and the ones defined in the config class

    method ones: @PreAuthorize, @PostAuthorize, @PreFilter, @PostFilter and others...

    config ones: The ones defined in a configuration class like so

    @Configuration
    public class WebSecurityConfig {
    
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            return http
              .authorizeHttpRequests(requests -> {
                        requests.anyRequest().hasAuthority("test"); //.hasAuthority() here is what I'm talking about
              }).build();
        }
    

    If you want to do config ones I'd say try to copy from the AuthenticatedAuthorizationManager implementation. I haven't really delved too deep into those as I needed to do a method one.

    If you want to do a method one I was copying from PreAuthorizeAuthorizationManager. It looks pretty complex but I'll pass down everything I learned the last few days thinkering with it.

    The way I wanted to authorize my app I had two choices. Either keep a ton of info in the SecurityContextHolder or query the db each request which needed authorization. I chose the 2nd option.

    @Component("customAuthorizationManager")
    public class CustomAuthorizationManager implements AuthorizationManager<MethodInvocation> {
    
        private final RoleService roleService; 
        //Injecting RoleService
        public CustomAuthorizationManager(RoleService roleService) {
            this.roleService = roleService;
        }
    
        @Override
        public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation) {
    
            ExpressionParser parser = new SpelExpressionParser(); //1
            Expression expression = parser.parseExpression(invocation.getMethod().getAnnotation(PreAuthorize.class).value());//2
    
            CustomMethodSecurityExpressionHandler c = new CustomMethodSecurityExpressionHandler(roleService); //3
            EvaluationContext ec = c.createEvaluationContext(authentication,invocation); //4
            boolean granted = Boolean.TRUE.equals(expression.getValue(ec, Boolean.class)); //5
    
            return new AuthorizationDecision(granted);
        }
    }
    

    Now the check() method is the one that will be called. (Even if you call any other methods in the CustomAuthorizationManager in the @PreAuthorize spring wouldn't care and just call the check() method)

    So after I got that out of the way let me try to explain wtf is happening inside.

    1. I'm creating parser which will read the SpEL expression we have provided inside the @PreAuthorize
    2. Then with this complex thingy (and the parser we just created) we are getting the expression and saving it in a variable.
    3. (3 and 4). Now we have to implement how we'll handle the expression (I'll show the implementations later)
    4. (5) Afterwards we use the implementations we created at 3 and 4 to make a decision and return true or false

    I followed the above mentioned guide dan provided. Basically modifying what was there to my needs.

    public class CustomMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler {
    
        private final RoleService roleService;
        //Injecting the RoleService again (manually this time as this isn't a component)
        //and cuz it's needed further down the line
        public CustomMethodSecurityExpressionHandler(RoleService roleService) {
            this.roleService = roleService;
        }
    
        @Override
        public EvaluationContext createEvaluationContext(Supplier<Authentication> authentication, MethodInvocation mi) {
            StandardEvaluationContext context = (StandardEvaluationContext) super.createEvaluationContext(authentication, mi);
    
            MethodSecurityExpressionOperations delegate = (MethodSecurityExpressionOperations) context.getRootObject().getValue();
    
            CustomMethodSecurityExpressionRoot root = new CustomMethodSecurityExpressionRoot(delegate.getAuthentication());
            root.setRoleService(roleService);
            context.setRootObject(root);
    
            return context;
        }
    
    }
    

    Now the method we are overriding is where this differs from the article. I guess it might work with the same thing that article is using but I haven't tried it (cuz that implementation was supposed to be for @EnableGlobalMethodSecurity but in spring security 6 it's suggested to use @EnableMethodSecurity) I did it that way cuz that's how it was suggested in the newest spring docs (if you read more into it I think it's advised against doing it those 2 ways but eh)

    I guess most of the method is telling spring to use the custom thingies we created instead of the defaults

    You can also see the last custom thing we'll need to implement - CustomMethodSecurityExpressionRoot.

    public class CustomMethodSecurityExpressionRoot extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {
    
        private RoleService roleService;
        //No idea why these are needed. Just followed the guide
        private HttpServletRequest request;
        private Object filterObject;
        private Object returnObject;
        private Object target;
    
        public CustomMethodSecurityExpressionRoot(Authentication authentication) {
            super(authentication);
        }
        
        //Here is where you add the methods you'll be using in @PreAuthorize together with their logic
    
        //Custom method you'll be using in @PreAuthorize
        public boolean partOfBU(Long buIdToCheck){
            //your logic
        }
        
        //Custom method you'll be using in @PreAuthorize
        public boolean authorityCheck(Long buIdToCheck, String authorityToCheck){
            //your logic
        }
    
        public void setRoleService(RoleService roleService){
            this.roleService = roleService;
        }
    
        @Override
        public void setFilterObject(Object filterObject) {
            this.filterObject = filterObject;
        }
    
        @Override
        public Object getFilterObject() {
            return this.filterObject;
        }
    
        @Override
        public void setReturnObject(Object returnObject) {
            this.returnObject = returnObject;
        }
    
        @Override
        public Object getReturnObject() {
            return this.returnObject;
        }
    
        @Override
        public Object getThis() {
            return target;
        }
    }
    

    Basically here's where you'll be adding the custom methods you'll be using. No idea why the other stuff is used or even if it is needed.

    And now how to call this?

        @PostMapping("/wtf")
        @PreAuthorize("partOfBU(#bu.id())") //1
        public ResponseEntity<Object> aaa(@P("bu") @RequestBody BusinessUnitDTO businessUnitDTO){
            return new ResponseEntity<>(HttpStatus.OK);
        }
    
    1. The method call using a value from the parameter (explained in the docs (pretty well for a change))

    And that's the whole shabang. Probably the way I've done is old but it at least works