Access is Always Denied in Spring Security - DenyAllPermissionEvaluator

14,056

Solution 1

Here's the long waited answer:

The documentation clearly describes:

To use hasPermission() expressions, you have to explicitly configure a PermissionEvaluator in your application context. This would look something like this:

so basically I was doing in my AclConfiguration which extends GlobalMethodSecurityConfiguration:

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
        expressionHandler.setPermissionEvaluator(new AclPermissionEvaluator(aclService()));
        expressionHandler.setPermissionCacheOptimizer(new AclPermissionCacheOptimizer(aclService()));
        return expressionHandler;
    }

Which was not getting processed by Spring!

I had to separate AclConfig and GlobalMethodSecurityConfiguration. When there are @Beans defined in the latter, the above method is not getting processed, which might be a bug (if not, any clarification on subject is welcome).

Solution 2

I upgraded my application to use Spring Security 4.2.1.RELEASE then afterwards I started to experience an unexpected access denied in all @PreAuthorize annotated methods, which was working just fine before the upgrade. I debugged the spring security code and I realized that the problem was that all roles to be checked were being prefixed with a default string "ROLE_" regardless of the fact that I had set my default prefix to empty, as shown in the code below.

auth.ldapAuthentication()
        .groupSearchBase(ldapProperties.getProperty("groupSearchBase"))
        .groupRoleAttribute(ldapProperties.getProperty("groupRoleAttribute"))
        .groupSearchFilter(ldapProperties.getProperty("groupSearchFilter"))

        //this call used to be plenty to override the default prefix
        .rolePrefix("")

        .userSearchBase(ldapProperties.getProperty("userSearchBase"))
        .userSearchFilter(ldapProperties.getProperty("userSearchFilter"))
        .contextSource(this.ldapContextSource);

All my controller methods were annotated with @PreAuthorize("hasRole('my_ldap_group_name')"), however, the framework was not taking my empty role prefix setting into account and thus it was using ROLE_my_ldap_group_name to check the actual role instead.

After I dug deep into the framework's code, I realized that the class org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler still had the default role prefix set to "ROLE_". I followed up the source of its value and I found out that it was first checking for a declared bean of the class org.springframework.security.config.core.GrantedAuthorityDefaults to look for a default prefix during first initialization of the bean org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer, however, as this initializer bean could not find it declared, it ended up using the aforementioned default prefix.

I believe this is not an expected behavior: Spring Security should have considered the same rolePrefix from ldapAuthentication, however, to solve this issue, it was necessary to add the bean org.springframework.security.config.core.GrantedAuthorityDefaults to my application context (I'm using annotation based configuration), as following:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class CesSecurityConfiguration extends WebSecurityConfigurerAdapter {

    private static final String ROLE_PREFIX = "";
    //... ommited code ...
    @Bean
    public GrantedAuthorityDefaults grantedAuthorityDefaults() {
        return new GrantedAuthorityDefaults(ROLE_PREFIX);
    }

}

Maybe you're getting the same problem - I could see that you're using DefaultMethodSecurityExpressionHandler and it also uses the bean GrantedAuthorityDefaults, so if you're using the same Spring Security version as me - 4.2.1.RELEASE you are probably running into the same issue.

Share:
14,056

Related videos on Youtube

Hasan Can Saral
Author by

Hasan Can Saral

Updated on September 15, 2022

Comments

  • Hasan Can Saral
    Hasan Can Saral about 1 year

    I have configured ACL in my Spring Boot application. The ACL configuration is as follows:

    @Configuration
    @ComponentScan(basePackages = "com.company")
    @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
    public class ACLConfigration extends GlobalMethodSecurityConfiguration {
    
        @Autowired
        DataSource dataSource;
    
        @Bean
        public EhCacheBasedAclCache aclCache() {
            return new EhCacheBasedAclCache(aclEhCacheFactoryBean().getObject(), permissionGrantingStrategy(), aclAuthorizationStrategy());
        }
    
        @Bean
        public EhCacheFactoryBean aclEhCacheFactoryBean() {
            EhCacheFactoryBean ehCacheFactoryBean = new EhCacheFactoryBean();
            ehCacheFactoryBean.setCacheManager(aclCacheManager().getObject());
            ehCacheFactoryBean.setCacheName("aclCache");
            return ehCacheFactoryBean;
        }
    
        @Bean
        public EhCacheManagerFactoryBean aclCacheManager() {
            return new EhCacheManagerFactoryBean();
        }
    
        @Bean
        public DefaultPermissionGrantingStrategy permissionGrantingStrategy() {
            ConsoleAuditLogger consoleAuditLogger = new ConsoleAuditLogger();
            return new DefaultPermissionGrantingStrategy(consoleAuditLogger);
        }
    
        @Bean
        public AclAuthorizationStrategy aclAuthorizationStrategy() {
            return new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("ROLE_ACL_ADMIN"));
        }
    
        @Bean
        public LookupStrategy lookupStrategy() {
            return new BasicLookupStrategy(dataSource, aclCache(), aclAuthorizationStrategy(), new ConsoleAuditLogger());
        }
    
        @Bean
        public JdbcMutableAclService aclService() {
            return new JdbcMutableAclService(dataSource, lookupStrategy(), aclCache());
        }
    
        @Bean
        public DefaultMethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler() {
            return new DefaultMethodSecurityExpressionHandler();
        }
    
        @Override
        public MethodSecurityExpressionHandler createExpressionHandler() {
            DefaultMethodSecurityExpressionHandler expressionHandler = defaultMethodSecurityExpressionHandler();
            expressionHandler.setPermissionEvaluator(new AclPermissionEvaluator(aclService()));
            expressionHandler.setPermissionCacheOptimizer(new AclPermissionCacheOptimizer(aclService()));
            return expressionHandler;
        }
    }
    

    References:

    and the security configuration is as follows:

    @Configuration
    @EnableWebSecurity
    public class CustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        @Bean
        public AuthenticationEntryPoint entryPoint() {
            return new LoginUrlAuthenticationEntryPoint("/authenticate");
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
    
            http
                    .csrf()
                    .disable()
                    .authorizeRequests()
                    .antMatchers("/authenticate/**").permitAll()
                    .anyRequest().fullyAuthenticated()
                    .and().requestCache().requestCache(new NullRequestCache())
                    .and().addFilterBefore(authenticationFilter(), CustomUsernamePasswordAuthenticationFilter.class);
        }
    
        @Override
        public void configure(WebSecurity web) throws Exception {
            web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
        }
    
        @Bean
        public CustomUsernamePasswordAuthenticationFilter authenticationFilter()
                throws Exception {
            CustomUsernamePasswordAuthenticationFilter authenticationFilter = new CustomUsernamePasswordAuthenticationFilter();
            authenticationFilter.setUsernameParameter("username");
            authenticationFilter.setPasswordParameter("password");
            authenticationFilter.setFilterProcessesUrl("/authenticate");
            authenticationFilter.setAuthenticationSuccessHandler(new CustomAuthenticationSuccessHandler());
            authenticationFilter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler());
            authenticationFilter.setAuthenticationManager(authenticationManagerBean());
            return authenticationFilter;
        }
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    }
    

    My CustomAuthenticationProvider class:

    @Component
    public class CustomAuthenticationProvider implements AuthenticationProvider {
    
        @Autowired
        private UsersService usersService;
    
        @Override
        public Authentication authenticate(Authentication authentication)
                throws AuthenticationException {
    
            String username = authentication.getName();
            String password = authentication.getCredentials().toString();
    
            User user = usersService.findOne(username);
    
            if(user != null && usersService.comparePassword(user, password)){
    
                return new UsernamePasswordAuthenticationToken(
                        user.getUsername(),
                        user.getPassword(),
                        AuthorityUtils.commaSeparatedStringToAuthorityList(
                                user.getUserRoles().stream().collect(Collectors.joining(","))));
            } else {
                return null;
            }
        }
    
        @Override
        public boolean supports(Class<?> authentication) {
            return authentication.equals(UsernamePasswordAuthenticationToken.class);
        }
    }
    

    Here's my CustomUsernamePasswordAuthenticationToken:

    public class CustomUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
                throws AuthenticationException {
    
            if(!request.getMethod().equals("POST"))
                throw new AuthenticationServiceException(String.format("Authentication method not supported: %s", request.getMethod()));
    
            try {
    
                CustomUsernamePasswordAuthenticationForm form = new ObjectMapper().readValue(request.getReader(), CustomUsernamePasswordAuthenticationForm.class);
    
                String username = form.getUsername();
                String password = form.getPassword();
    
                if(username == null)
                    username = "";
    
                if(password == null)
                    password = "";
    
                UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
    
                setDetails(request, token);
    
                return getAuthenticationManager().authenticate(token);
    
            } catch (IOException exception) {
                throw new CustomAuthenticationException(exception);
            }
        }
    
        private class CustomAuthenticationException extends RuntimeException {
            private CustomAuthenticationException(Throwable throwable) {
                super(throwable);
            }
        }
    }
    

    Apart from the above, I have CustomAuthenticationFailureHandler, CustomAuthenticationSuccessHandler, CustomNoRedirectStrategy and CustomUsernamePasswordAuthenticationForm which I skipped for the sake of this question's length.

    And I am using MySQL schema that can be found here.

    I am adding entries to my acl related tables as follows:

    INSERT INTO acl_class VALUES (1, com.company.project.domain.users.User)
    INSERT INTO acl_sid VALUES (1, 1, "demo")
    

    (I have a user with username demo)

    INSERT INTO acl_object_identity VALUES (1, 1, 1, NULL, 1, 0)
    INSERT INTO acl_entry VALUES (1, 1, 1, 1, 1, 1, 1, 1)
    

    But all I am getting is:

    Denying user demo permission 'READ' on object com.company.project.domain.users.User@4a49e9b4
    

    in my

    @PostFilter("hasPermission(filterObject, 'READ')")
    

    I am suspecting of several issues here:

    1. The hasPermission expression: I have substituted it with 'READ' and '1', but to no extent.
    2. My database entries are not right
    3. I am not implementing a custom permission evaluator. Is this required, or is expressionHandler.setPermissionEvaluator(new AclPermissionEvaluator(aclService())); enough?

    Update

    Sample method where @PostFilter is used:

    @RequestMapping(method = RequestMethod.GET)
        @PostFilter("hasPermission(filterObject, 'READ')")
        List<User> find(@Min(0) @RequestParam(value = "limit", required = false, defaultValue = "10") Integer limit,
                        @Min(0) @RequestParam(value = "page", required = false, defaultValue = "0") Integer page,
                        @RequestParam(value = "email", required = false) String email,
                        @RequestParam(value = "firstName", required = false) String firstName,
                        @RequestParam(value = "lastName", required = false) String lastName,
                        @RequestParam(value = "userRole", required = false) String userRole) {
    
            return usersService.find(
                    limit,
                    page,
                    email,
                    firstName,
                    lastName,
                    userRole);
        }
    

    Update #2:

    The question now reflects everything set up in regards to authentication/authorization/ACL.

    Update #3:

    I am now very close to resolve the issue, the only thing left is to resolve this:

    https://stackoverflow.com/questions/42996579/custom-permissionevaluator-not-called-although-set-as-permissionevaluator-deny

    If anyone could help me with that question, I can finally have a write up of what I have went through to resolve this.

    • denov
      denov over 6 years
      is the method that the @PostFilter is on a public that implements an interface?
    • Hasan Can Saral
      Hasan Can Saral over 6 years
      No, it's on a @RestController or @Controller though. I really suspect of database entries, or a component not being present.
    • Stefan Haberl
      Stefan Haberl over 6 years
      How does the method look like, where you put the @PostFilter annotation? Do you get any Stacktrace in your server log?
    • Stefan Haberl
      Stefan Haberl over 6 years
      I'm not that into ACL, but did you maybe mix up the usages of @PreAuthorize and @PostFilter? See concretepage.com/spring/spring-security/…
    • Hasan Can Saral
      Hasan Can Saral over 6 years
      Not really. As the official documentation (as well as the link you shared) suggests, @PostFilter filters the return object based on if the authenticated user has the rights to perform the action stated in the expression. This is exactly what I am trying to achieve. The problem is that every object is filtered regardless the ACL entries in the db.
  • Hasan Can Saral
    Hasan Can Saral almost 7 years
    Thanks for the answer, I appreciate it. I am aware that Spring requires ROLE_ prefix and all my roles start with ROLE_. So I hardly think that it should have anything to do with role names. Plus, I don't filter or authorize based on a role, but instead I am using acl to check for permissions on object/user basis.
  • s_bighead
    s_bighead almost 7 years
    Ok, that's what I had to share, I hope you'll figure out the problem, I've never use ACL...
  • Hasan Can Saral
    Hasan Can Saral over 6 years
    I don't quite use UserDetails nor UserDetailsService. I only return a UsernamePasswordAuthenticationToken from my class that implements AuthenticationProvider. Do I have to for this to work?
  • denov
    denov over 6 years
    add more details. it sounds like you don't have the Authentication object setup correctly.
  • Hasan Can Saral
    Hasan Can Saral over 6 years
    I agree. I have updated the question to reflect anything that I have configured for authentication/authorization and ACL.
  • denov
    denov over 6 years
    you should add a toString() method on your user object to it prints out something more useful than the memory location. eg, the username.
  • denov
    denov over 6 years
    i'd also double check that UsernamePasswordAuthenticationToken is being created correctly in CustomUsernamePasswordAuthenticationFilter
  • Hasan Can Saral
    Hasan Can Saral over 6 years
    CustomUsernamePasswordAuthenticationFilter works as it should, UsernamePasswordAuthenticationToken is created correctly. Denying user demo permission 'READ' on object com.company.project.domain.users.User also means that ACL knows which user is authenticated. But I have discovered that I don't even have any hits on any acl related table in db to check if the user is authorized on a resource. Shouldn't I?
  • Hasan Can Saral
    Hasan Can Saral over 6 years
    I figured out I cannot use AclPermissionEvaluator with hasPermission expression. Please look at here. Simply every permission is evaluated by DenyAllPermissionEvaluator. Other than that, everything seems fine. Any thoughts on that?
  • Melvin Sy
    Melvin Sy over 3 years
    Thank you for your thorough investigation and a well constructed answer! This solved my problem when I upgraded to spring-security-acl 5.3.0.RELEASE
  • Ankit
    Ankit over 2 years
    looks like a little inconsistent behavior to me. Spent hours fixing something I broke while refactoring. Later I realized when trying your solution: 1. was using wrong PermissionEvaluator 2. adding @component worked without adding explicite configuration.
  • Emmy
    Emmy over 2 years
    Thank you! This totally saved me. I upgraded to spring-boot 2.4.4 (uses spring 5.3.5) from 2.2.7 and suddenly the @Secured annotation would no longer work. Apparently setting the role prefix by extending GlobalMethodSecurityConfiguration and overriding accessDecisionManager is no longer enough. After setting the grantedAuthorityDefaults as you suggested it works again!