Spring Security and Non-flat Roles Inheritance Architecture
Semyon Kirekov
Posted on February 23, 2023
Table of contents
- Business requirements and domain model
- Roles, enums, and inheritance
- Unit testing roles inheritance
- Defining JPA entities
- Creating custom Authentication implementation
- Why does getAuthorities() return empty set?
- UserId, and volatile authenticated flag
- Creating custom AuthenticationProvider
- Defining Spring Security config
- Declaring REST API methods
- Creating custom role checking service
- Combining PreAuthorize and custom role checking service
- Short and elegant enum references in SpEL expressions
- Integration testing and validating security
Then it comes to authorization, roles always come into play. Flat ones are straightforward. User possess a set of privileges that you simply check for containing the required element. But what if a user can have different roles for specific entities? For example, I can be an editor in the particular social media post but only a viewer in another one. Also, there might be inheritance rules. If an admin grant me the editor role, I automatically become a viewer. But if I'm an editor, I'm not becoming an admin. What's the approach to handle the scenario to remain code clean and maintainable? Don't worry, I'm giving your answers.
In this article, I'm telling you:
- What's the best approach to handle roles inheritance in Java?
- How to test the stated hierarchy?
- How to apply the solution in Spring Security and Spring Data JPA?
You can find the code examples and the entire project setup in this repository.
Business requirements and domain model
Supposing we’re developing a simple social media platform. Look at the diagram below. Here I described the business entities we’re working with in this article.
There are 3 core entities:
-
User
is the one who reads existing posts and creates new ones. -
Community
is the feed to submit new posts. -
Post
is an individual part of media. Some users can view it, edit it, and delete it.
Also, we have role a model which is a bit more complicated than the plain privileges assignment. For example, a user can owe an EDITOR
role for the particular post but has only a VIEWER
for another one. Or user may be an ADMIN
for the ‘Cats and dogs’ community but just a MODERATOR
for the ‘Motorcycles and Heavy metal’ one.
The roles provide such privileges:
-
CommunityRole.ADMIN
gives ultimate access to the community and the containing posts. -
CommunityRole.MODERATOR
provides an ability to add new posts and remove old ones. -
PostRole.EDITOR
allows to edit the content of the particular post. -
PostRole.REPORTER
gives the credit to send reports about inappropriate attitude in the comments. -
PostRole.VIEWER
grants an access to view the post and leave comments.
However, business also wants the inheritance model. For example, if I have the root role, it means I also can do actions that any child provides. Look at the schema below to understand the approach.
Suppose that I'm a MODERATOR
in some community. That means, I'm EDITOR
, REPORTER
, and VIEWER
as well for any other post in the community. On the contrary, if somebody granted me with the REPORTER
role for the post, it doesn't mean I have the rights to edit it (I need the EDITOR
for that) or add new posts (the MODERATOR
role provides access for it).
That's a convenient approach. You don't have to check the set of many roles for every possible operation. You just have to validate the presence of the lowest authority. If I'm an ADMIN
in the community, then I'm also a VIEWER
for any post there. So, due to the inheritance model, you have to check the VIEWER
and that's it.
Anyway, it doesn't seem like an simple task to implement it in the code. Besides, the VIEWER
model has two parents: the REPORTER
and the EDITOR
. The Java doesn't allow multiple inheritance. Meaning that we need a special approach.
Roles, enums, and inheritance
Roles are the perfect candidates for the enum. Look at the code snippet below.
public enum CommunityRoleType {
ADMIN, MODERATOR
}
public enum PostRoleType {
VIEWER, EDITOR, REPORTER
}
How we can build an inheritance model with these simple enums? At first, we need an interface to bind the roles together. Look at the declaration below.
public interface Role {
boolean includes(Role role);
}
The Role
interface will be the root of any CommunityRoleType
and PostRoleType
value as well. The includes
method tells us whether the supplied role equals to the current one or contains it in its children.
Look at the modified PostRoleType
code below.
public enum PostRoleType implements Role {
VIEWER, EDITOR, REPORTER;
private final Set<Role> children = new HashSet<>();
static {
REPORTER.children.add(VIEWER);
EDITOR.children.add(VIEWER);
}
@Override
public boolean includes(Role role) {
return this.equals(role) || children.stream().anyMatch(r -> r.includes(role));
}
}
We store the children of the particular role in the regular Java HashSet
as private final
field. What intriguing is how these children appear. By default, the set is empty for every enum value. But the static initializer block comes into play. You can treat it as the two-phase constructor. On this block, we just need to assign proper children to the required parents. The includes
method is also rather simple. If the passed role equals to the current one, return true
. Otherwise, perform the check recursively for every present child node.
An approach for the CommunityRoleType
is similar. Look at the code example below.
public enum CommunityRoleType implements Role {
ADMIN, MODERATOR;
private final Set<Role> children = new HashSet<>();
static {
ADMIN.children.add(MODERATOR);
MODERATOR.children.addAll(List.of(PostRoleType.EDITOR, PostRoleType.REPORTER));
}
@Override
public boolean includes(Role role) {
return this.equals(role) || children.stream().anyMatch(r -> r.includes(role));
}
}
As you can see, the MODERATOR
role has two children: the PostRoleType.EDITOR
and the PostRoleType.REPORTER
. Due to the fact that both CommunityRoleType
and the PostRoleType
share the same interface, they can all be part of the one inheritance hierarchy.
A slight detail remains. We need to know the root of the hierarchy to perform the access validation. The easiest way is just declaring a static method that returns the required nodes. Look at the updated Role
interface definition below.
public interface Role {
boolean includes(Role role);
static Set<Role> roots() {
return Set.of(CommunityRoleType.ADMIN);
}
}
I return the
Set<Role>
instead ofRole
because theoretically there might be several roots. So, it's unnecessary to restrict the roots count to 1 on the method signature.Some of you may ask why not just using Spring Security Role Hierarchy component? After all, it's an out-of-box solution. It suites well for the plain role model which is not the case in our context. I'll reference this point again later in the article.
Unit testing roles inheritance
Let's test our role hierarchy. Firstly, we need to check that there are no cycles which can cause StackOverflowError. Look at the test below.
@Test
void shouldNotThrowStackOverflowException() {
final var roots = Role.roots();
final var existingRoles = Stream.concat(
stream(PostRoleType.values()),
stream(CommunityRoleType.values())
).toList();
assertDoesNotThrow(
() -> {
for (Role root : roots) {
for (var roleToCheck : existingRoles) {
root.includes(roleToCheck);
}
}
}
);
}
The idea is checking all roots and all existing roles for includes
combinations. None of them should throw any exception.
Next move is validating the inheritance. Here are the test cases:
- The
CommunityRoleType.ADMIN
should include any other role and theCommunityRoleType.ADMIN
itself. - The
CommunityRoleType.MODERATOR
should include thePostRoleType.EDITOR
,PostRoleType.REPORTER
,PostRoleType.VIEWER
and theCommunityRoleType.MODERATOR
- The
PostRoleType.VIEWER
should not include thePostRoleType.REPORTER
. - The
CommunityRoleType.MODERATOR
should not include theCommunityRoleType.ADMIN
.
Look at the code example below to see the described test suites.
@ParameterizedTest
@MethodSource("provideArgs")
void shouldIncludeOrNotTheGivenRoles(Role root, Set<Role> rolesToCheck, boolean shouldInclude) {
for (Role role : rolesToCheck) {
assertEquals(
shouldInclude,
root.includes(role)
);
}
}
private static Stream<Arguments> provideArgs() {
return Stream.of(
arguments(
CommunityRoleType.ADMIN,
Stream.concat(
stream(PostRoleType.values()),
stream(CommunityRoleType.values())
).collect(Collectors.toSet()),
true
),
arguments(
CommunityRoleType.MODERATOR,
Set.of(PostRoleType.EDITOR, PostRoleType.VIEWER, PostRoleType.REPORTER, CommunityRoleType.MODERATOR),
true
),
arguments(
PostRoleType.VIEWER,
Set.of(PostRoleType.REPORTER),
false
),
arguments(
CommunityRoleType.MODERATOR,
Set.of(CommunityRoleType.ADMIN),
false
)
);
}
There are much more cases to cover. But I omit them for brevity.
Defining JPA entities
You can find all the JPA entities declarations here and the corresponding Flyway migrations here.
Anyway, I'm showing you the core artefacts of the system. Look at the PostRole
and the CommunityRole
JPA entities' declaration below.
@Entity
@Table(name = "community_role")
public class CommunityRole {
@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "user_id")
private User user;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "community_id")
private Community community;
@Enumerated(STRING)
private CommunityRoleType type;
}
@Entity
@Table(name = "post_role")
public class PostRole {
@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "user_id")
private User user;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "post_id")
private Post post;
@Enumerated(STRING)
private PostRoleType type;
}
As I've already pointed out, the CommunityRole
binds to the User
and the particular Community
whilst the PostRole
binds to the User
as well and the specific Post
. Therefore, the role model structure is not flat. It brings us some complexities with Spring Security. But don't worry, I'll show you how to nail them.
Because of the vertical role model the Spring Security Role Hierarchy is not going to work. We need a more complicated approach. So, let’s move forward.
Look at the required SQL migrations' set (I'm using PostgreSQL) below.
CREATE TABLE community_role
(
id BIGSERIAL PRIMARY KEY,
user_id BIGINT REFERENCES users (id) NOT NULL,
community_id BIGINT REFERENCES community (id) NOT NULL,
type VARCHAR(50) NOT NULL,
UNIQUE (user_id, community_id, type)
);
CREATE TABLE post_role
(
id BIGSERIAL PRIMARY KEY,
user_id BIGINT REFERENCES users (id) NOT NULL,
post_id BIGINT REFERENCES post (id) NOT NULL,
type VARCHAR(50) NOT NULL,
UNIQUE (user_id, post_id, type)
);
Creating custom Authentication implementation
To begin with, we need to create custom Authentication
interface implementation. Look at the code snippet below.
@RequiredArgsConstructor
public class PlainAuthentication implements Authentication {
private final Long userId;
private volatile boolean authenticated = true;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return emptySet();
}
@Override
public Long getPrincipal() {
return userId;
}
@Override
public String getName() {
return "";
}
@Override
public boolean isAuthenticated() {
return authenticated;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
authenticated = isAuthenticated;
}
@Override
public Object getCredentials() {
return "";
}
@Override
public Object getDetails() {
return null;
}
}
Why does getAuthorities() return empty set?
The first thing to notice is that the getAuthorities()
method always returns empty collection. Why is that? The Spring Security role model is flat, but we deal with a vertical one. If you want to put the community and the post roles belonging to the user, it may look like this:
CommunityRole_ADMIN_234
PostRole_VIEWER_896
There are three parts:
- The type of the role
- The role value
- The id of the community or the post that the role references to.
In such setup, the role checking mechanism also become cumbersome. Look at the @PreAuthorize
annotation usage example below.
@PreAuthorize("hasAuthority('PostRole_VIEWER_' + #postId)")
@GetMapping("/api/post/{postId}")
public PostResponse getPost(@PathVariable Long postId) { ... }
In my view, this code is awful smells. Not only there are string typings that may cause hard tracking bugs, we lost the inheritance idea. Do you remember that the CommunityRole.ADMIN
also includes the PostRole.VIEWER
? But here we check only the particular authority (Spring Security just calls the Collection.contains
method). Meaning that the Authentication.getAuthorities()
method has to contain all the children's roles as well. Therefore, you have to perform these steps:
- Select all the community roles and the post roles as well that the user possess
- Loop through each role down to its children and put each occurred node to another collection
- Eliminate the duplicates (or just use the
HashSet
) and return the result as the user authorities.
Not to mention the code complexity, there are also performance drawbacks. Every request comes with selecting all the user's roles from the database. But what if the user is an admin of the community? It's unnecessary to query post roles because they already have an ultimate access. But you have to do it to make the @PreAuthorize
annotation work as expected.
I thing it's clear now why I return empty collection of authorities. Later I'm explaining how to deal with that properly.
UserId, and volatile authenticated flag
Look at the PlainAuthentication
below. I leave only the getPrincipal
, and isAuthenticated/setAuthenticated
methods to discuss.
@RequiredArgsConstructor
public class PlainAuthentication implements Authentication {
private final Long userId;
private volatile boolean authenticated = true;
@Override
public Long getPrincipal() {
return userId;
}
@Override
public boolean isAuthenticated() {
return authenticated;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
authenticated = isAuthenticated;
}
...
}
The userId
points to the database row containing information about the user. We’ll use it later to retrieve the roles. The isAuthenticated/setAuthenticated
methods are part of the Spring Security contract. So, we have to implement them properly. I put volatile marker because the PlainAuthentication
object is mutable and multiple threads can access it. Better safe than sorry.
The
getPrincipal
method inherits from theAuthentication
interface and returnsObject
. However, Java allows to return the subclass of the signature definition, if you extend the base class or implement the interface. Therefore, make your code more secure and maintainable.The
Authentication
interface dictates to implement 3 other methods I haven't spoken about. Those aregetName
,getCredentials
, andgetDetails
. We use none of them later. Therefore, it's OK to return default values.
Creating custom AuthenticationProvider
In the beginning, we should provide custom AuthenticationProvider
to resolve the PlainAuthentication
declared previously from the user input. Look at the code snippet below.
@Component
@RequiredArgsConstructor
class DbAuthenticationProvider implements AuthenticationProvider {
private final UserRepository userRepository;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
final var password = authentication.getCredentials().toString();
if (!"password".equals(password)) {
throw new AuthenticationServiceException("Invalid username or password");
}
return userRepository.findByName(authentication.getName())
.map(user -> new PlainAuthentication(user.getId()))
.orElseThrow(() -> new AuthenticationServiceException("Invalid username or password"));
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.equals(authentication);
}
}
Of course, it's not a real production implementation. For the sake of simplicity, all users have the same password. If the user is present by the provided name, return its id as a PlainAuthentication
wrapper. Otherwise, throw AuthenticationServiceException
that will transform to the 401
status code afterwards.
Defining Spring Security config
Finally, time to add the Spring Security config. Here I’m using basic access authentication. Anyway, the role checking patterns I’m describing later remain the same for different authentication mechanisms. Look at the code example below.
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
@Bean
@SneakyThrows
public SecurityFilterChain securityFilterChain(HttpSecurity http) {
return http
.csrf().disable()
.cors().disable()
.authorizeHttpRequests(customizer -> customizer.anyRequest().authenticated())
.httpBasic()
.authenticationEntryPoint((request, response, authException) -> response.sendError(401))
.and()
.build();
}
}
Declaring REST API methods
The described domain can provide lots of possible operations. Though I’m going to list just 4 of them. That is sufficient to cover the whole role checking idea of mine. Those are:
- Create new community.
- Create post for the given community.
- Update the post by id.
- Get post by id.
Look at the code snippet below.
@RestController
@RequestMapping("/api")
public class Controller {
@PostMapping("/community")
@PreAuthorize("isAuthenticated()")
public CommunityResponse createCommunity(@RequestParam String name) { ... }
@PostMapping("/community/{communityId}/post")
// Must have CommunityRoleType.MODERATOR role
public PostResponse createPost(@PathVariable Long communityId, @RequestParam String name) { ... }
@PutMapping("/post/{postId}")
// Must have PostRoleType.EDITOR
public void updatePost(@PathVariable Long postId, @RequestParam String name) { ... }
@GetMapping("/post/{postId}")
// Must have PostRoleType.VIEWER role
public PostResponse getPost(@PathVariable Long postId) { ... }
}
As you can see, every user can create a new community (the creator automatically becomes an admin of the entity as well). However, I haven't implemented the required checks on the endpoints yet. At first we need the role checking service.
Creating custom role checking service
Look at the blueprint of the RoleService
below.
@Service("RoleService")
public class RoleService {
public boolean hasAnyRoleByCommunityId(Long communityId, Role... roles) { ... }
public boolean hasAnyRoleByPostId(Long postId, Role... roles) { ... }
}
I set the bean name inside the
@Service
annotation manually. Soon I'll explain the idea to you.
There are only two methods. The first one checks the required role presence by the communityId
and the second one by the postId
.
Look at the hasAnyRoleByCommunityId
implementation below. The other method has the same idea and you can check out the whole class by this link.
@Service("RoleService")
@RequiredArgsConstructor
public class RoleService {
private final CommunityRoleRepository communityRoleRepository;
private final PostRoleRepository postRoleRepository;
@Transactional
public boolean hasAnyRoleByCommunityId(Long communityId, Role... roles) {
final Long userId = ((PlainAuthentication) SecurityContextHolder.getContext().getAuthentication()).getPrincipal();
final Set<CommunityRoleType> communityRoleTypes =
communityRoleRepository.findRoleTypesByUserIdAndCommunityId(userId, communityId);
for (Role role : roles) {
if (communityRoleTypes.stream().anyMatch(communityRoleType -> communityRoleType.includes(role))) {
return true;
}
}
final Set<PostRoleType> postRoleTypes =
postRoleRepository.findRoleTypesByUserIdAndCommunityId(userId, communityId);
for (Role role : roles) {
if (postRoleTypes.stream().anyMatch(postRoleType -> postRoleType.includes(role))) {
return true;
}
}
return false;
}
...
}
Here is the algorithm:
- Get the current user
Authentication
by callingSecurityContextHolder.getContext().getAuthentication()
and cast it to thePlainAuthentication
because that's the only type our application working with. - Find all community roles by the
userId
and thecommunityId
. If any of the passed roles (according to the role inheritance model) are present in the set of the found community roles, returntrue
. Otherwise, go to the next step. - Find all post roles by the
userId
and thecommunityId
. If any of the passed roles (according to the role inheritance model) are present in the set of the found post roles, returntrue
. Otherwise, returnfalse
.
I also want to point out the performance benefits of the solution I'm proposing to you. The classic approach with retrieving all user authorities and calling further the
Authentication.getAuthorities
method requires us to pull everyCommunityRole
and eachPostRole
the user has from the database at once (note that the user might be a member of dozens of communities and posses hundreds of roles). But we do it much more efficiently:
- Pull the community roles only for the provided combination of
(userId, communityId)
.- Pull the post roles only for the provided combination of
(userId, communityId)
.If the first step succeeds, the method returns
true
and doesn't perform another database query.
Combining PreAuthorize and custom role checking service
Finally, we set almost everything and we’re ready to apply the role checking mechanism at the endpoints. Why almost? Look at the code snippet below and try to notice an improvement slot.
@RestController
@RequestMapping("/api")
public class Controller {
@PostMapping("/community")
@PreAuthorize("isAuthenticated()")
public CommunityResponse createCommunity(@RequestParam String name) { ... }
@PostMapping("/community/{communityId}/post")
@PreAuthorize("@RoleService.hasAnyRoleByCommunityId(#communityId, T(com.example.demo.domain.CommunityRoleType).MODERATOR)")
public PostResponse createPost(@PathVariable Long communityId, @RequestParam String name) { ... }
@PutMapping("/post/{postId}")
@PreAuthorize("@RoleService.hasAnyRoleByPostId(#postId, T(com.example.demo.domain.PostRoleType).EDITOR)")
public void updatePost(@PathVariable Long postId, @RequestParam String name) { ... }
@GetMapping("/post/{postId}")
@PreAuthorize("@RoleService.hasAnyRoleByPostId(#postId, T(com.example.demo.domain.PostRoleType).VIEWER)")
public PostResponse getPost(@PathVariable Long postId) { ... }
}
The @RoleService
is the reference to the RoleService
Spring bean by its name. Then I call the specific role checking method. The postId
and the communityId
are the method arguments and we have to prefix their usage with hash. The last parameters are varargs of the required roles. Because the Role
interface implementations are enums, we can reference the values by their fully qualified names.
As you have already guessed, this statement T(com.example.demo.domain.CommunityRoleType).MODERATOR
has 2 problems:
- It makes code harder to read and provokes copy-paste development.
- If you change the package name of any enum role, then you have to update the dependent API methods accordingly.
Short and elegant enum references in SpEL expressions
Thankfully, there is more concise solution. Look at the fixed CommunityRoleType
definition below.
public enum CommunityRoleType implements Role {
ADMIN, MODERATOR;
...
@Component("CommunityRole")
@Getter
static class SpringComponent {
private final CommunityRoleType ADMIN = CommunityRoleType.ADMIN;
private final CommunityRoleType MODERATOR = CommunityRoleType.MODERATOR;
}
}
You just need to create another Spring bean that encapsulates the enum values as fields. Then you can reference them the same way we did with the RoleService
.
The
PostRoleType
enhancing is similar. You can check out the code by this link.
Let's refactor the API methods a bit and see the final result. Look at the fixed Controller
below.
@RestController
@RequestMapping("/api")
public class Controller {
@PostMapping("/community")
@PreAuthorize("isAuthenticated()")
public CommunityResponse createCommunity(@RequestParam String name) { ... }
@PostMapping("/community/{communityId}/post")
@PreAuthorize("@RoleService.hasAnyRoleByCommunityId(#communityId, @CommunityRole.ADMIN)")
public PostResponse createPost(@PathVariable Long communityId, @RequestParam String name) { ... }
@PutMapping("/post/{postId}")
@PreAuthorize("@RoleService.hasAnyRoleByPostId(#postId, @PostRole.EDITOR)")
public void updatePost(@PathVariable Long postId, @RequestParam String name) { ... }
@GetMapping("/post/{postId}")
@PreAuthorize("@RoleService.hasAnyRoleByPostId(#postId, @PostRole.VIEWER)")
public PostResponse getPost(@PathVariable Long postId) { ... }
}
Much more elegant solution, don’t you think? The role checking is declarative now and even non-technical folks can understand it (maybe you wish to generate documentation about endpoints restrictions' rules).
Integration testing and validating security
If there are no tests, you cannot be sure that your code is working at all. So, let's write some. I'm going to verify these cases:
- If a user is not authenticated, creating a new community request should return
401
. - If a user is authenticated, they should create a new community and new post inside it successfully.
- If a user is authenticated but he has no rights to view the post, the request should return
403
I'm using Testcontainers to start PostgreSQL during tests. The explaining of its setup is out of the scope of this article. Anyway, you can check out the whole test suite by this link.
Look at the unauthorized request checking below.
@Test
void shouldReturn401IfUnauthorizedUserTryingToCreateCommunity() {
userRepository.save(User.newUser("john"));
final var communityCreatedResponse =
rest.postForEntity(
"/api/community?name={name}",
null,
CommunityResponse.class,
Map.of("name", "community_name")
);
assertEquals(UNAUTHORIZED, communityCreatedResponse.getStatusCode());
}
The test works as expected.
Nothing complicated here. Let’s move forward to checking community and post successful creation. Look at the code snippet down below.
@Test
void shouldCreateCommunityAndPostSuccessfully() {
userRepository.save(User.newUser("john"));
final var communityCreatedResponse =
rest.withBasicAuth("john", "password")
.postForEntity(
"/api/community?name={name}",
null,
CommunityResponse.class,
Map.of("name", "community_name")
);
assertTrue(communityCreatedResponse.getStatusCode().is2xxSuccessful());
final var communityId = communityCreatedResponse.getBody().id();
final var postCreatedResponse =
rest.withBasicAuth("john", "password")
.postForEntity(
"/api/community/{communityId}/post?name={name}",
null,
PostResponse.class,
Map.of("communityId", communityId, "name", "post_name")
);
assertTrue(postCreatedResponse.getStatusCode().is2xxSuccessful());
}
The john
user creates a new community and then adds a new post for it. Again, everything works smoothly.
Let's get to the final case. If user has no PostRoleType.VIEWER
for the particular post, the request of getting it should return 403. Look at the code block below.
@Test
void shouldReturn403IfUserHasNoAccessToViewThePost() {
userRepository.save(User.newUser("john"));
userRepository.save(User.newUser("bob"));
// john creates new community and post inside it
...
final var postViewResponse =
rest.withBasicAuth("bob", "password")
.getForEntity(
"/api/post/{postId}",
PostResponse.class,
Map.of("postId", postId)
);
assertEquals(FORBIDDEN, postViewResponse.getStatusCode());
}
The creation of community and post is the same as in the previous test. So, I'm omitting these parts to focus on important details.
There are two pre-configured users: john
and bob
. The john
user creates a new community and post whilst bob
tries to get the post by its id. As long as he doesn’t possess the required privileges, the server returns 403
code. Look at the result of the test running below.
And the final check comes. Let’s run all tests at once to validate that their behavior is deterministic.
Everything works like a charm. Splendid!
Conclusion
As you can see, Spring Security plays well with a complex role hierarchy and inheritance model. Just a few patterns and your code shines.
That's all I wanted to tell you about applying role model with Spring Security. If you have any questions or suggestions, please leave your comments down below. If the content of the article was useful, press the like button and share the link with you friends and colleagues.
Thank you very much for reading this long piece!
Resources
Posted on February 23, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.