Access is Always Denied in Spring Security - DenyAllPermissionEvaluator

Hasan Can Saral :

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.

Hasan Can Saral :

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

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=447550&siteId=1