Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

'Invalid bean definition' when migrating Spring Boot 2.0.6 to 2.1.0 with EvaluationContextExtensionSupport and custom PermissionEvaluator

In Spring Boot 2.1.0 EvaluationContextExtensionSupport is deprecated and https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/query/spi/EvaluationContextExtensionSupport.html says to Implement EvaluationContextExtension directly

Even though it is only deprecated, it instantly starting failing on this upgrade with this stacktrace:

Caused by: org.springframework.beans.factory.support.BeanDefinitionOverrideException: Invalid bean definition with name 'methodSecurityInterceptor' defined in class path resource [org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.class]: Cannot register bean definition [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration; factoryMethodName=methodSecurityInterceptor; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.class]] for bean 'methodSecurityInterceptor': There is already [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=methodSecurityConfiguration; factoryMethodName=methodSecurityInterceptor; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [ournamespace/configuration/MethodSecurityConfiguration.class]] bound.
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.registerBeanDefinition(DefaultListableBeanFactory.java:894)
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForBeanMethod(ConfigurationClassBeanDefinitionReader.java:274)
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass(ConfigurationClassBeanDefinitionReader.java:141)
...and so on

I don't explicitly override this bean, so I'm guessing this is just a side effect of what we're doing in our current code. If I do allow bean overriding with spring.main.allow-bean-definition-overriding=true as per https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.1-Release-Notes#bean-overriding then I simply get another exception.

java.lang.IllegalStateException: Duplicate key org.springframework.data.spel.ExtensionAwareEvaluationContextProvider$EvaluationContextExtensionAdapter@10dfbbbb at java.util.stream.Collectors.lambda$throwingMerger$0(Collectors.java:133) ~[na:1.8.0_162]

However, I don't even want to override any bean behavior, the goal is to get a custom permission evaluator working again the way Spring intends it to work.

This is how it did work in the last version:

In Spring Boot 2.0.6 we had the following to get our custom PermissionEvaluator class to work:

A class that extended EvaluationContextExtensionSupport

import org.springframework.data.repository.query.spi.EvaluationContextExtensionSupport;
import org.springframework.security.access.expression.SecurityExpressionRoot;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

public class SecurityEvaluationContextExtension extends EvaluationContextExtensionSupport {

    @Override
    public String getExtensionId() {
        return "security";
    }

    @Override
    public SecurityExpressionRoot getRootObject() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            return new SecurityExpressionRoot(authentication) {
        };
    }
}

And then a class where the expression handler is created with our permission evaluator, and with a @Bean with a EvaluationContextExtension

import ournamespace.security.CustomPermissionEvaluator;
import ournamespace.security.SecurityEvaluationContextExtension;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.repository.query.spi.EvaluationContextExtension;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;

@Configuration
@RequiredArgsConstructor
public class MethodSecurityConfiguration extends GlobalMethodSecurityConfiguration {

    private final CustomPermissionEvaluator permissionEvaluator;

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        DefaultMethodSecurityExpressionHandler expressionHandler =
                new DefaultMethodSecurityExpressionHandler();
        expressionHandler.setPermissionEvaluator(permissionEvaluator);
        return expressionHandler;
    }

    @Bean
    EvaluationContextExtension securityExtension() {
        return new SecurityEvaluationContextExtension();
    }
}

And finally we have this in an otherwise mostly empty class:

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
   ...
}

This is because the custom permission evaluator never did apply to all methods if we just put this in the MethodSecurityConfiguration class. The server in question is an oauth2 resource server so we don't configure anything else in the WebSecurityConfigurerAdapter. We also implement our own UserDetails and we extend the DefaultUserAuthenticationConverter, if this is in any way relevant for the new solution.

I have tried implementing EvaluationContextExtension class directly, as stated in the deprecation warning. It's just a simple modification by changing the extends interface to implements EvaluationContextExtension. I have also tried changing to the seemingly newer package org.springframework.data.spel.spi

I've tried deleting our own SecurityEvaluationContextExtension and returning https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/data/repository/query/SecurityEvaluationContextExtension.html as a bean directly but for some reason that data package isn't available in Spring Boot 2.1.0

I've tried just removing the definition of that bean altogether.

All these things result in various 'invalid bean definition' errors on startup.

Does anyone know where to find a migration guide or any other resource on how this is supposed to work now?

Just for reference sake, the actual CustomPermissionEvaluator class:

import ournamespace.configuration.Constants;
import ournamespace.exception.InternalException;
import ournamespace.model.Account;
import ournamespace.model.Member;
import ournamespace.model.Project;
import ournamespace.repository.MemberRepository;
import ournamespace.service.ServiceUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import java.io.Serializable;

import static ournamespace.model.MemberStatus.JOINED;
import static ournamespace.model.ProjectRole.*;

@RequiredArgsConstructor
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {

    private final MemberRepository memberRepository;

    @Override
    public boolean hasPermission(Authentication auth, Object targetDomainObject, Object permission) {
        if (targetDomainObject == null)
            return false;

        if (!(permission instanceof String))
            return false;

        if (auth == null)
            return false;

        Account account = ServiceUtil.getAccount(auth);

        if (targetDomainObject instanceof Project)
            return hasPermissionOnProject(account, (Project) targetDomainObject, (String) permission);
        //and so on
    }
}

And an example on how it's used:

public interface ProjectRepository extends PagingAndSortingRepository<Project, UUID> {

    @Override
    @PreAuthorize("hasPermission(#project, " + Constants.WRITE + ")")
    <S extends Project> S save(@Param("project") S project);
}
like image 901
Sebastiaan van den Broek Avatar asked Nov 21 '18 10:11

Sebastiaan van den Broek


2 Answers

I took your code and created a sample app from it. I posted it here:

https://github.com/jzheaux/stackoverflow-53410526

Your @EnableGlobalMethodSecurity annotation is on WebSecurityConfigurerAdapter. You also have a class that extends GlobalMethodSecurityConfiguration. This can cause some ordering issues at startup at times, which may be what you are seeing => two MethodSecurityExpressionHandlers get created as well as two EvaluationContextExtensions.

Whether this is precisely the case or not (I'm guessing that it is), when I matched your @EnableGlobalMethodSecurity with your custom GlobalMethodSecurityConfiguration, things started up fine.

Also, though, it seems that your custom EvaluationContextExtension is very similar to the Spring Security default. You might consider removing that class as well as the corresponding bean method, if you can, since Spring Boot exposes one automatically when you have spring-boot-starter-security and spring-security-data as dependencies.

like image 164
jzheaux Avatar answered Oct 05 '22 23:10

jzheaux


You're overriding methodSecurityInterceptor bean. It was working previously as bean overriding was allowed.

Bean overriding has been disabled by default to prevent a bean being accidentally overridden. If you are relying on overriding, you will need to set spring.main.allow-bean-definition-overriding to true.

Spring-Boot-2.1-Release-Notes#bean-overriding

like image 39
Sukhpal Singh Avatar answered Oct 05 '22 23:10

Sukhpal Singh