Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Access is Always Denied in Spring Security - DenyAllPermissionEvaluator

Tags:

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:

  • SO Q1
  • SO Q2

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.

like image 782
Hasan Can Saral Avatar asked Jan 26 '17 11:01

Hasan Can Saral


People also ask

What is the default password for Spring Security?

The default username is: user and the default password will be printed in the console at the time when your Spring Boot project is starting.

How do I enable method level security in spring?

Method-level security is implemented by placing the @PreAuthorize annotation on controller methods (actually one of a set of annotations available, but the most commonly used). This annotation contains a Spring Expression Language (SpEL) snippet that is assessed to determine if the request should be authenticated.

How do I bypass Spring boot security?

To discard the security auto-configuration and add our own configuration, we need to exclude the SecurityAutoConfiguration class. However, there are also some particular cases in which this setup isn't quite enough. For example, almost each Spring Boot application is started with Actuator in the classpath.


2 Answers

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.

like image 168
s_bighead Avatar answered Oct 17 '22 08:10

s_bighead


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

like image 27
Hasan Can Saral Avatar answered Oct 17 '22 08:10

Hasan Can Saral