mingchuno
mingchuno

Reputation: 557

Spring Security Role Hierarchy not working using Java Config

First of all, I am new to Java Spring Framework. So forgive me if I did not provide enough info. I have tried to add RoleHierarchy into my app but it did not work. Below are the codes I have tried.


SecurityConfig.java

// These config is try to set up a user Role Hierarchy
@Bean
public RoleHierarchy roleHierarchy() {
  System.out.println("arrive public RoleHierarchy roleHierarchy()");
  RoleHierarchyImpl r = new RoleHierarchyImpl();
  r.setHierarchy("ROLE_ADMIN > ROLE_STAFF");
  r.setHierarchy("ROLE_STAFF > ROLE_USER");
  r.setHierarchy("ROLE_DEVELOPER > ROLE_USER");
  r.setHierarchy("ROLE_USER > ROLE_GUEST"); 
  return r;
}

@Bean
public AffirmativeBased defaultAccessDecisionManager(RoleHierarchy roleHierarchy){
  System.out.println("arrive public AffirmativeBased defaultAccessDecisionManager()");
  List<AccessDecisionVoter> decisionVoters = new ArrayList<>();

  // webExpressionVoter
  WebExpressionVoter webExpressionVoter = new WebExpressionVoter();
  DefaultWebSecurityExpressionHandler
      expressionHandler = new DefaultWebSecurityExpressionHandler();
  expressionHandler.setRoleHierarchy(roleHierarchy);
  webExpressionVoter.setExpressionHandler(expressionHandler);

  decisionVoters.add(webExpressionVoter);
  decisionVoters.add(roleHierarchyVoter(roleHierarchy));
  // return new AffirmativeBased(Arrays.asList((AccessDecisionVoter) webExpressionVoter));
  return new AffirmativeBased(decisionVoters);
}

@Bean
public RoleHierarchyVoter roleHierarchyVoter(RoleHierarchy roleHierarchy) {
  System.out.println("arrive public RoleHierarchyVoter roleHierarchyVoter");
  return new RoleHierarchyVoter(roleHierarchy);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
  // skipping some codes
  http
    // skipping some codes
    .accessDecisionManager(defaultAccessDecisionManager(roleHierarchy()))
  // skipping some codes
}

MethodSecurityConfig.java

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {

  @Inject
  private SecurityConfig securityConfig;

  @Override
  protected AuthenticationManager authenticationManager() throws Exception {
    return securityConfig.authenticationManagerBean();
  }

  @Override
  protected MethodSecurityExpressionHandler createExpressionHandler() {
    System.out.println("arrive protected MethodSecurityExpressionHandler createExpressionHandler()");
    DefaultMethodSecurityExpressionHandler d = new DefaultMethodSecurityExpressionHandler();
    d.setRoleHierarchy(securityConfig.roleHierarchy());
    return d;
  }

}

And I have a UserDetailsServiceImpl implements UserDetailsService that provide the principal, Authentication and GrantedAuthority

Finally I have some APIs:

@PreAuthorize("hasRole('ROLE_STAFF')")
@RequestMapping(value = "/api/v1/contactUs", method = RequestMethod.GET)

@PreAuthorize("hasRole('ROLE_DEVELOPER')")
@RequestMapping(value = "/api/v1/system", method = RequestMethod.GET)

The problem is now if I login as ROLE_STAFF, ROLE_DEVELOPER, ROLE_ADMIN, I got the following result.

| API       | ROLE_STAFF | ROLE_DEVELOPER | ROLE_ADMIN |
|-----------|------------|----------------|------------|
| contactUs | 200        | 403            | 403        |
| system    | 403        | 200            | 403        |

As you can see ROLE_STAFF and ROLE_DEVELOPER work just fine. But I want ROLE_ADMIN as a super role of both and it didn't work.

FYI, I am using spring-security 3.2.5.RELEASE

Upvotes: 12

Views: 18211

Answers (7)

J. Jerez
J. Jerez

Reputation: 794

Correct format is: ROLE_A > ROLE_B and ROLE_B > ROLE_C. see doc.

@Bean
public RoleHierarchy roleHierarchy() {
    RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
    roleHierarchy.setHierarchy(UserRole.ROLE_HIERARCHY);
    return roleHierarchy;
}

Upvotes: 0

Maran23
Maran23

Reputation: 256

Note: The accepted answer won't work in the newest version of Spring security (I think since release 5.2.1). This is because the 'and' (ROLE_1 > ROLE_2 and ROLE_2 > ROLE_3) notation was never an official standard. You could have written every word instead of 'and' and it would still work the same in the past versions.

Instead, in the new version you should now use '\n' (new line), e.g. ROLE_1 > ROLE_2\nROLE2 > ROLE_3 ...

Upvotes: 13

Vitaliy Polchuk
Vitaliy Polchuk

Reputation: 1898

For me the solution was having proper bean name for the instance of DefaultWebSecurityExpressionHandler. The name should be webSecurityExpressionHandler.

@Bean
public RoleHierarchyImpl roleHierarchy() {
    RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
    roleHierarchy.setHierarchy(Roles.getRoleHierarchy());
    return roleHierarchy;
}

@Bean
public DefaultWebSecurityExpressionHandler webSecurityExpressionHandler() {
    DefaultWebSecurityExpressionHandler expressionHandler = new DefaultWebSecurityExpressionHandler();
    expressionHandler.setRoleHierarchy(roleHierarchy());
    return expressionHandler;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .expressionHandler(webSecurityExpressionHandler())
            ...
}

Upvotes: 4

David Rz Ayala
David Rz Ayala

Reputation: 2235

Override the createExpressionHandler method so it returns a configured global expression handler

  @Configuration
  @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
  public class GlobalMethodSecurityConfig extends GlobalMethodSecurityConfiguration {
      @Autowired
      private RoleHierarchy roleHierarchy;

      @Override
      protected MethodSecurityExpressionHandler createExpressionHandler(){
          return methodSecurityExpressionHandler();
      }

      private DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler(){
          DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
          expressionHandler.setRoleHierarchy(roleHierarchy);
          return expressionHandler;
      }

      @Bean
      public RoleHierarchyImpl roleHierarchy() {
          RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
      roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_OWNER > ROLE_USER");
          return roleHierarchy;
      }

      @Bean
      public RoleHierarchyVoter roleVoter() {
          return new RoleHierarchyVoter(roleHierarchy);
      }

      @Configuration
      public static class WebSecurityConfig extends WebSecurityConfigurerAdapter {

          @Override
          protected void configure(HttpSecurity http) throws Exception {}
      }
  }

Upvotes: 1

user2210419
user2210419

Reputation: 41

I don't use Spring RoleHierarchy - because it doea not work for me. But Ussualy I do like this: Define Role interface

public static interface Role {
  String getName();
  List<String> getHierarchy();
}

List of my roles (to store in DB):

public interface AuthStates {
  // Spring security works fine only with ROLE_*** prefix
  String ANONYMOUS = "ROLE_ANONYMOUS";
  String AUTHENTICATED = "ROLE_AUTHENTICATED";
  String ADMINISTRATOR = "ROLE_ADMINISTRATOR";
}

Define Anonymous role as basic role class:

public static class Anonymous implements Role {
  private final String name;
  private final List<String> hierarchy = Lists.newArrayList(ANONYMOUS);

  public Anonymous() {
    this(ANONYMOUS);
  }

  protected Anonymous(String name) {
    this.name = name;
  }

  @Override
  public String getName() {
    return name;
  }

  @Override
  public List<String> getHierarchy() {
    return hierarchy;
  }

  protected void addHierarchy(String name) {
    hierarchy.add(name);
  }
}

Define Authenticated role (common user role):

public static class Authenticated extends Anonymous {
  public Authenticated() {
    this(AUTHENTICATED);
  }

  protected Authenticated(String name) {
    super(name);
    addHierarchy(AUTHENTICATED);
  }
}

Define Administrator role (at the top of the evolution):

public static class Administrator extends Authenticated {
  public Administrator() {
    this(ADMINISTRATOR);
  }

  protected Administrator(String name) {
    super(name);
    addHierarchy(ADMINISTRATOR);
  }
}

Optional - static factory class:

public static Role getRole(String authState) {
  switch (authState) {
    case ANONYMOUS: return new Anonymous();
    case AUTHENTICATED: return new Authenticated();
    case ADMINISTRATOR: return new Administrator();
    default: throw new IllegalArgumentException("Wrong auth state");
  }
}

In my CustomUserDetailsService (which implements UserDetailsService) I use role like this:

private Collection<GrantedAuthority> createAuthority(User user) {
  final List<GrantedAuthority> authorities = new ArrayList<>();
  AuthStates.Role userAuthState = AuthStates.getRole(user.getAuthState());
  for (String role : userAuthState.getHierarchy()) {
    authorities.add(new SimpleGrantedAuthority(role));
  }
  return authorities;
}

authorities

In controllers:

@PreAuthorize("hasRole('ROLE_AUTHENTICATED')")

Will allow user logged in as ROLE_AUTHENTICATED and ROLE_ADMINISTRATOR both.

Upvotes: -2

Lord Nighton
Lord Nighton

Reputation: 1720

Everytime I want to implement a hierarchy of roles with Spring Security and Java config, I use the following approach:

  1. We have to add a RoleHierarchyImpl bean into context (You see, that I use multiple roles to build a hierarchy):

    @Bean
    public RoleHierarchyImpl roleHierarchy() {
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_DBA ROLE_DBA > ROLE_USER ");
        return roleHierarchy;
    }
    
  2. Then we need to create web expression handler to pass obtained hierarchy to it:

    private SecurityExpressionHandler<FilterInvocation> webExpressionHandler() {
        DefaultWebSecurityExpressionHandler defaultWebSecurityExpressionHandler = new DefaultWebSecurityExpressionHandler();
        defaultWebSecurityExpressionHandler.setRoleHierarchy(roleHierarchy());
        return defaultWebSecurityExpressionHandler;
    }
    
  3. The final step is to add expressionHandler into http.authorizeRequests():

            @Override
            protected void configure(HttpSecurity http) throws Exception {
                http
                   .authorizeRequests()
                        .expressionHandler(webExpressionHandler())
                        .antMatchers("/admin/**").access("(hasRole('ROLE_ADMIN') or hasRole('ROLE_DBA')) and isFullyAuthenticated()")
                        .antMatchers("/dba").access("hasRole('ROLE_DBA') and isFullyAuthenticated()")
                        .antMatchers("/dba/**").access("hasRole('ROLE_USER')")
                        .and()
                   .requiresChannel()
                        .antMatchers("/security/**").requiresSecure()
                        .anyRequest().requiresInsecure()
                        .and()
                   .formLogin()
                        .loginPage("/login")
                        .failureUrl("/login?auth=fail")
                        .usernameParameter("username")
                        .passwordParameter("password")
                        .defaultSuccessUrl("/admin")
                        .permitAll()
                        .and()
                   .logout()
                            .logoutUrl("/logout")
                            .deleteCookies("remember-me")
                            .invalidateHttpSession(true)
                            .logoutSuccessUrl("/index")
                            .permitAll()
                            .and()
                   .csrf()
                            .and()
                   .rememberMe().tokenValiditySeconds(1209600)
                            .and()
                   .exceptionHandling().accessDeniedPage("/403")
                            .and()
                   .anonymous().disable()
                   .addFilter(switchUserFilter());
            }
    

Result: in this particular example we try to visit /dba section after we have logged in using admin user (ROLE_ADMIN). Before we created a hierarchy, we had an access denied result, but now we can visit this section without any problems.

Upvotes: 21

mingchuno
mingchuno

Reputation: 557

The issue is in the RoleHierachy, which should be like this:

@Bean
public RoleHierarchy roleHierarchy() {
  RoleHierarchyImpl r = new RoleHierarchyImpl();
  r.setHierarchy("ROLE_ADMIN > ROLE_STAFF and ROLE_ADMIN > ROLE_DEVELOPER and ROLE_STAFF > ROLE_USER and ROLE_DEVELOPER > ROLE_USER");
  return r;
}

keep calling setHierarchy() will override the setting before

Upvotes: 21

Related Questions