🏠Spring BootRoles 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 *