🏠 ❯ Spring Boot ❯ Roles and Privileges in Spring Security

Roles and Privileges in Spring Security

In this post, we will take a look at Role Based Access Control (RBAC) with Spring boot.

Understanding RBAC

In an RBAC model there are three key entities. They are,

  1. User or Subject – The actors of the system who perform operations. It can represent a physical person, an automated account, or even another application.
  2. Role – Authority level defined by A job Title, Department or functional hierarchy.
  3. Privilege – An approval or permission to perform operations

With that being said, The following is an illustration of how these entities map to each other.

Basically, Users can perform operations. To perform operations they need to have certain permission or privileges. This is why privileges are assigned to roles and roles are assigned to users. Let’s see how we can implement these.

RBAC Entities

Let’s create the above objects to represented as database entities.

User Entity

@Data
@Entity
public class UserAccount {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    @Column(unique = true)
    private String username;
    private String password;
    private boolean active;
   @OneToMany(mappedBy = "user")
   private List<UserToRole> userToRoles;
}Code language: PHP (php)

UseRole entity

@Data
@Entity
public class UserRole {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String roleName;
   @OneToMany(mappedBy = "role")
   private List<UserRoleToPrivilege> userRoleToPrivileges;
}Code language: PHP (php)

UserPrivileges entity

@Data
@Entity
public class UserPrivilege {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String privilegeName;
}Code language: PHP (php)

UserRoleToPrivilege Entity

@Data
@Entity
public class UserRoleToPrivilege {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    @ManyToOne
    private UserRole role;
    @ManyToOne
    private UserPrivilege privilege;
}Code language: CSS (css)

UserToRole Entity

@Data
@Entity
public class UserToRole {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    @ManyToOne
    private UserAccount user;
    @ManyToOne
    private UserRole role;
}Code language: CSS (css)

Populate the database entries

With the above entities in place, let’s populate the database with the appropriate roles and privileges. For this test, I made the entries straight forward using a data.sql file.

insert into user_account(id, username, password, active) values (1, 'user1', '{noop}user1', 1);
insert into user_account(id, username, password, active) values (2, 'user2', '{noop}user2', 1);
insert into user_account(id, username, password, active) values (3, 'admin', '{noop}admin', 1);
insert into user_role(id, role_name) values (1, 'USER');
insert into user_role(id, role_name) values (2, 'ADMIN');
insert into user_to_role(id, user_id, role_id) values (1, 1, 1);
insert into user_to_role(id, user_id, role_id) values (2, 2, 1);
insert into user_to_role(id, user_id, role_id) values (3, 3, 2);
insert into user_privilege(id, privilege_name) values (1, 'canReadUser');
insert into user_privilege(id, privilege_name) values (2, 'canReadAdmin');
insert into user_role_to_privilege(id, role_id, privilege_id) values (1, 1, 1);
insert into user_role_to_privilege(id, role_id, privilege_id) values (2, 2, 1);
insert into user_role_to_privilege(id, role_id, privilege_id) values (3, 2, 2);Code language: JavaScript (javascript)

Note that I’m using a NoOpPasswordEncoder because the passwords are prepended with {noop}.

Spring Security userDetailsService

In our previous posts, We always used a single role called USER for all the users in the system. However, We need to make changes to pick these roles and privileges from the database. Here is a crude example of how to do just that.

@Component
public class DatabaseUserDetailsService implements UserDetailsService {

    private final
    UserAccountRepository userAccountRepository;

    public DatabaseUserDetailsService(UserAccountRepository userAccountRepository) {
        this.userAccountRepository = userAccountRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserAccount userAccount = userAccountRepository.findByUsername(username);
        if (userAccount == null) {
            throw new UsernameNotFoundException("User with username [" + username + "] not found in the system");
        }

        Set<GrantedAuthority> authorities = new HashSet<>();

        for (UserToRole userToRole : userAccount.getUserToRoles()) {
            authorities.add(new SimpleGrantedAuthority("ROLE_" + userToRole.getRole().getRoleName()));
            for (UserRoleToPrivilege userRoleToPrivilege : userToRole.getRole().getUserRoleToPrivileges()) {
                authorities.add(new SimpleGrantedAuthority(userRoleToPrivilege.getPrivilege().getPrivilegeName()));
            }
        }

       return new CustomUserDetails(userAccount.getUsername(), userAccount.getPassword(), userAccount.isActive(), authorities);
    }
}Code language: PHP (php)

One interesting thing to note here is that we added roles as well as privileges as authorities. However, all roles are prepended with ROLE_. This specific way is due to how security expressions like hasRole and hasAuthority work.

This way developers can use expressions to set role level and privilege level setup for url mappings which you will see below.

Securing API endpoints

With WebSecurityConfigurerAdapter, you can customize which URL is accessible by whom. Take a look at this configuration snippet.

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/user").access("hasAuthority('canReadUser')")
                .antMatchers("/admin").access("hasAuthority('canReadAdmin')")
                .anyRequest().authenticated()
                .and().httpBasic()
                .and().formLogin();
    }
}Code language: PHP (php)

Here, the admin user can access both /user and /admin because ADMIN role has both canReadUser and canReadAdmin privileges. However, user1 or user2 cannot access /admin as they would get a 403 Forbidden response.

With all the above in place, Let’s test the results.

$ curl -i -u "user1:user1" http://localhost:8080/user
HTTP/1.1 200
Set-Cookie: JSESSIONID=9BEC44655277BBDF6832817AFF4CAAA1; Path=/; HttpOnly
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/plain;charset=UTF-8
Content-Length: 11
Date: Tue, 29 Dec 2020 15:16:57 GMT

Hello user!Code language: PHP (php)
$ curl -i -u "user1:user1" http://localhost:8080/admin
HTTP/1.1 403
Set-Cookie: JSESSIONID=0910F6115CB28A9DF914D22052396448; Path=/; HttpOnly
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 29 Dec 2020 15:17:28 GMT

{
  "timestamp" : "2020-12-29T15:17:28.537+00:00",
  "status" : 403,
  "error" : "Forbidden",
  "message" : "",
  "path" : "/admin"
}Code language: PHP (php)

As you see, when user1 tries to access /admin endpoint they get 403 - Forbidden message.

This is the GitHub repository for you to follow this tutorial.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

2 Comments

  1. Excellent article, thank you for sharing such useful information.
    I think better to use @PreAuthorize(“hasPermission(#id, ‘Foo’, ‘read’)”) at controller would increase the readability.

    1. There is no golden way of doing RBAC. In my opinion, keeping the @PreAuthorize at the service level would greatly reduce complexity. If your service is going to be accessed by multiple controllers and controller methods, then all of them will still expect the same privileges. Wherever this deviates, you could very well annotate the controllers. Also, if you are sharing these service beans across multiple modules, you would want consistency across the whole platform of applications.