Fine-Grained Security: Implementing Role-Based Access Control in Java

Role-Based Access Control (RBAC) is a fundamental security pattern that restricts system access to authorized users based on their organizational roles. Instead of assigning permissions directly to users, RBAC assigns permissions to roles, and then roles to users. This approach simplifies security management, enhances auditability, and ensures consistent enforcement of access policies across Java applications.

Core Concepts of RBAC

Key Components:

  • Users - Individuals who access the system
  • Roles - Job functions with defined responsibilities
  • Permissions - Operations that can be performed on resources
  • Sessions - User interactions with the system
  • Constraints - Optional rules for dynamic access control

Basic RBAC Implementation

1. Domain Models

@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String email;
private String password;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
// Constructors, getters, setters
public User() {}
public User(String username, String email, String password) {
this.username = username;
this.email = email;
this.password = password;
}
public boolean hasRole(String roleName) {
return roles.stream()
.anyMatch(role -> role.getName().equals(roleName));
}
public Set<String> getPermissions() {
return roles.stream()
.flatMap(role -> role.getPermissions().stream())
.map(Permission::getName)
.collect(Collectors.toSet());
}
}
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "role_permissions",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "permission_id")
)
private Set<Permission> permissions = new HashSet<>();
// Constructors, getters, setters
public Role() {}
public Role(String name, String description) {
this.name = name;
this.description = description;
}
public boolean hasPermission(String permissionName) {
return permissions.stream()
.anyMatch(permission -> permission.getName().equals(permissionName));
}
}
@Entity
@Table(name = "permissions")
public class Permission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
private String resource;
private String action; // READ, WRITE, DELETE, etc.
// Constructors, getters, setters
public Permission() {}
public Permission(String name, String description, String resource, String action) {
this.name = name;
this.description = description;
this.resource = resource;
this.action = action;
}
}

2. Permission Constants

public final class Permissions {
// User permissions
public static final String USER_READ = "USER_READ";
public static final String USER_WRITE = "USER_WRITE";
public static final String USER_DELETE = "USER_DELETE";
// Product permissions
public static final String PRODUCT_READ = "PRODUCT_READ";
public static final String PRODUCT_WRITE = "PRODUCT_WRITE";
public static final String PRODUCT_DELETE = "PRODUCT_DELETE";
// Order permissions
public static final String ORDER_READ = "ORDER_READ";
public static final String ORDER_WRITE = "ORDER_WRITE";
public static final String ORDER_DELETE = "ORDER_DELETE";
// Admin permissions
public static final String ADMIN_ACCESS = "ADMIN_ACCESS";
public static final String SYSTEM_CONFIG = "SYSTEM_CONFIG";
private Permissions() {
// Utility class
}
}
public final class Roles {
public static final String ROLE_ADMIN = "ADMIN";
public static final String ROLE_MANAGER = "MANAGER";
public static final String ROLE_USER = "USER";
public static final String ROLE_VIEWER = "VIEWER";
private Roles() {
// Utility class
}
}

Spring Security RBAC Configuration

1. Security Configuration

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class RBACSecurityConfig {
private final UserDetailsService userDetailsService;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
public RBACSecurityConfig(UserDetailsService userDetailsService,
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint) {
this.userDetailsService = userDetailsService;
this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole(Roles.ROLE_ADMIN)
.requestMatchers("/api/management/**").hasAnyRole(Roles.ROLE_ADMIN, Roles.ROLE_MANAGER)
.requestMatchers(HttpMethod.GET, "/api/products/**").hasAuthority(Permissions.PRODUCT_READ)
.requestMatchers(HttpMethod.POST, "/api/products/**").hasAuthority(Permissions.PRODUCT_WRITE)
.requestMatchers(HttpMethod.DELETE, "/api/products/**").hasAuthority(Permissions.PRODUCT_DELETE)
.anyRequest().authenticated()
)
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) 
throws Exception {
return config.getAuthenticationManager();
}
}

2. Custom UserDetailsService

@Service
@Transactional
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String usernameOrEmail) 
throws UsernameNotFoundException {
User user = userRepository.findByUsernameOrEmail(usernameOrEmail, usernameOrEmail)
.orElseThrow(() -> 
new UsernameNotFoundException("User not found: " + usernameOrEmail));
return UserPrincipal.create(user);
}
@Transactional
public UserDetails loadUserById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User", "id", id));
return UserPrincipal.create(user);
}
}
public class UserPrincipal implements UserDetails {
private Long id;
private String username;
private String email;
private String password;
private Collection<? extends GrantedAuthority> authorities;
public UserPrincipal(Long id, String username, String email, String password,
Collection<? extends GrantedAuthority> authorities) {
this.id = id;
this.username = username;
this.email = email;
this.password = password;
this.authorities = authorities;
}
public static UserPrincipal create(User user) {
List<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
.collect(Collectors.toList());
// Add permissions as authorities
user.getPermissions().forEach(permission -> 
authorities.add(new SimpleGrantedAuthority(permission)));
return new UserPrincipal(
user.getId(),
user.getUsername(),
user.getEmail(),
user.getPassword(),
authorities
);
}
// UserDetails methods
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
// Getters
public Long getId() { return id; }
public String getEmail() { return email; }
}

Method-Level Security

1. Annotation-Based Security

@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping
@PreAuthorize("hasAuthority('" + Permissions.PRODUCT_READ + "')")
public ResponseEntity<List<Product>> getAllProducts() {
List<Product> products = productService.findAll();
return ResponseEntity.ok(products);
}
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('" + Permissions.PRODUCT_READ + "')")
public ResponseEntity<Product> getProduct(@PathVariable Long id) {
Product product = productService.findById(id);
return ResponseEntity.ok(product);
}
@PostMapping
@PreAuthorize("hasAuthority('" + Permissions.PRODUCT_WRITE + "')")
public ResponseEntity<Product> createProduct(@Valid @RequestBody Product product) {
Product savedProduct = productService.save(product);
return ResponseEntity.status(HttpStatus.CREATED).body(savedProduct);
}
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('" + Permissions.PRODUCT_WRITE + "')")
public ResponseEntity<Product> updateProduct(@PathVariable Long id, 
@Valid @RequestBody Product product) {
Product updatedProduct = productService.update(id, product);
return ResponseEntity.ok(updatedProduct);
}
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('" + Permissions.PRODUCT_DELETE + "')")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productService.delete(id);
return ResponseEntity.noContent().build();
}
// Role-based access
@GetMapping("/reports")
@PreAuthorize("hasAnyRole('" + Roles.ROLE_ADMIN + "', '" + Roles.ROLE_MANAGER + "')")
public ResponseEntity<ProductReport> getProductReport() {
ProductReport report = productService.generateReport();
return ResponseEntity.ok(report);
}
// Custom security expression
@GetMapping("/{id}/sensitive")
@PreAuthorize("@productSecurity.canAccessSensitiveData(#id, principal)")
public ResponseEntity<SensitiveProductData> getSensitiveData(@PathVariable Long id) {
SensitiveProductData data = productService.getSensitiveData(id);
return ResponseEntity.ok(data);
}
}

2. Custom Security Expressions

@Component("productSecurity")
public class ProductSecurity {
private final ProductService productService;
public ProductSecurity(ProductService productService) {
this.productService = productService;
}
public boolean canAccessSensitiveData(Long productId, UserPrincipal principal) {
// Users can access sensitive data if they are admins 
// or managers of the product's department
if (principal.getAuthorities().stream()
.anyMatch(auth -> auth.getAuthority().equals("ROLE_" + Roles.ROLE_ADMIN))) {
return true;
}
Product product = productService.findById(productId);
String userDepartment = getUserDepartment(principal.getId());
return product.getDepartment().equals(userDepartment) &&
principal.getAuthorities().stream()
.anyMatch(auth -> auth.getAuthority().equals("ROLE_" + Roles.ROLE_MANAGER));
}
public boolean canModifyProduct(Long productId, UserPrincipal principal) {
Product product = productService.findById(productId);
// Only product owners or admins can modify
return product.getCreatedBy().equals(principal.getId()) ||
principal.getAuthorities().stream()
.anyMatch(auth -> auth.getAuthority().equals("ROLE_" + Roles.ROLE_ADMIN));
}
private String getUserDepartment(Long userId) {
// Retrieve user's department from service
return userService.getUserDepartment(userId);
}
}

Advanced RBAC Patterns

1. Hierarchical Roles

@Service
public class RoleHierarchyService {
private final RoleRepository roleRepository;
public RoleHierarchyService(RoleRepository roleRepository) {
this.roleRepository = roleRepository;
}
public Set<String> getReachableRoles(String roleName) {
Set<String> reachableRoles = new HashSet<>();
reachableRoles.add(roleName);
Role role = roleRepository.findByName(roleName)
.orElseThrow(() -> new RoleNotFoundException(roleName));
// Get parent roles recursively
getParentRoles(role, reachableRoles);
return reachableRoles;
}
public Set<String> getReachablePermissions(String roleName) {
Set<String> permissions = new HashSet<>();
Set<String> reachableRoles = getReachableRoles(roleName);
for (String role : reachableRoles) {
Role roleEntity = roleRepository.findByName(role).orElseThrow();
permissions.addAll(
roleEntity.getPermissions().stream()
.map(Permission::getName)
.collect(Collectors.toSet())
);
}
return permissions;
}
private void getParentRoles(Role role, Set<String> reachableRoles) {
if (role.getParentRole() != null) {
String parentRoleName = role.getParentRole().getName();
reachableRoles.add(parentRoleName);
getParentRoles(role.getParentRole(), reachableRoles);
}
}
}
@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
String hierarchy = 
"ROLE_ADMIN > ROLE_MANAGER \n" +
"ROLE_MANAGER > ROLE_USER \n" +
"ROLE_USER > ROLE_VIEWER";
roleHierarchy.setHierarchy(hierarchy);
return roleHierarchy;
}
@Bean
public DefaultWebSecurityExpressionHandler webSecurityExpressionHandler() {
DefaultWebSecurityExpressionHandler expressionHandler = new DefaultWebSecurityExpressionHandler();
expressionHandler.setRoleHierarchy(roleHierarchy());
return expressionHandler;
}

2. Dynamic Permission Evaluation

@Service
public class PermissionEvaluationService {
public boolean hasPermission(Authentication authentication, 
Object targetDomainObject, 
Object permission) {
if (authentication == null || !authentication.isAuthenticated()) {
return false;
}
// Check role-based permissions
if (hasRolePermission(authentication, permission.toString())) {
return true;
}
// Check instance-based permissions
return hasInstancePermission(authentication, targetDomainObject, permission.toString());
}
private boolean hasRolePermission(Authentication authentication, String permission) {
return authentication.getAuthorities().stream()
.anyMatch(authority -> authority.getAuthority().equals(permission));
}
private boolean hasInstancePermission(Authentication authentication, 
Object targetDomainObject, 
String permission) {
if (targetDomainObject instanceof Product) {
return hasProductPermission(authentication, (Product) targetDomainObject, permission);
} else if (targetDomainObject instanceof Order) {
return hasOrderPermission(authentication, (Order) targetDomainObject, permission);
}
return false;
}
private boolean hasProductPermission(Authentication authentication, 
Product product, 
String permission) {
UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
switch (permission) {
case "READ":
return true; // All authenticated users can read products
case "WRITE":
return product.getCreatedBy().equals(principal.getId()) ||
hasAdminRole(authentication);
case "DELETE":
return hasAdminRole(authentication);
default:
return false;
}
}
private boolean hasAdminRole(Authentication authentication) {
return authentication.getAuthorities().stream()
.anyMatch(auth -> auth.getAuthority().equals("ROLE_ADMIN"));
}
}

RBAC Management Service

@Service
@Transactional
public class RBACManagementService {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final PermissionRepository permissionRepository;
public RBACManagementService(UserRepository userRepository,
RoleRepository roleRepository,
PermissionRepository permissionRepository) {
this.userRepository = userRepository;
this.roleRepository = roleRepository;
this.permissionRepository = permissionRepository;
}
// Role management
public Role createRole(String name, String description, Set<String> permissionNames) {
if (roleRepository.existsByName(name)) {
throw new RoleAlreadyExistsException(name);
}
Set<Permission> permissions = permissionRepository.findByNameIn(permissionNames);
Role role = new Role(name, description);
role.setPermissions(permissions);
return roleRepository.save(role);
}
public Role updateRolePermissions(Long roleId, Set<String> permissionNames) {
Role role = roleRepository.findById(roleId)
.orElseThrow(() -> new RoleNotFoundException(roleId));
Set<Permission> permissions = permissionRepository.findByNameIn(permissionNames);
role.setPermissions(permissions);
return roleRepository.save(role);
}
// User role assignment
public User assignRoleToUser(Long userId, String roleName) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException(userId));
Role role = roleRepository.findByName(roleName)
.orElseThrow(() -> new RoleNotFoundException(roleName));
user.getRoles().add(role);
return userRepository.save(user);
}
public User removeRoleFromUser(Long userId, String roleName) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException(userId));
user.getRoles().removeIf(role -> role.getName().equals(roleName));
return userRepository.save(user);
}
// Permission management
public Permission createPermission(String name, String description, 
String resource, String action) {
if (permissionRepository.existsByName(name)) {
throw new PermissionAlreadyExistsException(name);
}
Permission permission = new Permission(name, description, resource, action);
return permissionRepository.save(permission);
}
// Bulk operations
public void initializeDefaultRolesAndPermissions() {
// Create default permissions
Permission userRead = createPermissionIfNotExists(
Permissions.USER_READ, "Read user data", "USER", "READ");
Permission userWrite = createPermissionIfNotExists(
Permissions.USER_WRITE, "Write user data", "USER", "WRITE");
// ... create other permissions
// Create default roles
Role adminRole = createRoleIfNotExists(Roles.ROLE_ADMIN, "System Administrator", 
Set.of(Permissions.USER_READ, Permissions.USER_WRITE, Permissions.USER_DELETE,
Permissions.ADMIN_ACCESS, Permissions.SYSTEM_CONFIG));
Role userRole = createRoleIfNotExists(Roles.ROLE_USER, "Regular User",
Set.of(Permissions.USER_READ, Permissions.PRODUCT_READ, Permissions.ORDER_READ));
Role viewerRole = createRoleIfNotExists(Roles.ROLE_VIEWER, "Data Viewer",
Set.of(Permissions.PRODUCT_READ, Permissions.ORDER_READ));
}
private Permission createPermissionIfNotExists(String name, String description,
String resource, String action) {
return permissionRepository.findByName(name)
.orElseGet(() -> createPermission(name, description, resource, action));
}
private Role createRoleIfNotExists(String name, String description, Set<String> permissions) {
return roleRepository.findByName(name)
.orElseGet(() -> createRole(name, description, permissions));
}
}

Testing RBAC Implementation

@SpringBootTest
@Transactional
class RBACIntegrationTest {
@Autowired
private ProductController productController;
@Autowired
private UserRepository userRepository;
@Autowired
private RoleRepository roleRepository;
@Test
@WithMockUser(authorities = {Permissions.PRODUCT_READ})
void shouldAllowProductReadWithCorrectPermission() {
// When
ResponseEntity<List<Product>> response = productController.getAllProducts();
// Then
assertEquals(HttpStatus.OK, response.getStatusCode());
}
@Test
@WithMockUser(authorities = {Permissions.USER_READ})
void shouldDenyProductReadWithoutPermission() {
// When/Then
assertThrows(AccessDeniedException.class, () -> {
productController.getAllProducts();
});
}
@Test
@WithMockUser(roles = {Roles.ROLE_ADMIN})
void shouldAllowAdminAccess() {
// When
ResponseEntity<ProductReport> response = productController.getProductReport();
// Then
assertEquals(HttpStatus.OK, response.getStatusCode());
}
@Test
void shouldResolveRoleHierarchy() {
// Given
RoleHierarchyService hierarchyService = new RoleHierarchyService(roleRepository);
// When
Set<String> reachablePermissions = hierarchyService.getReachablePermissions(Roles.ROLE_MANAGER);
// Then
assertTrue(reachablePermissions.contains(Permissions.PRODUCT_READ));
assertTrue(reachablePermissions.contains(Permissions.PRODUCT_WRITE));
}
}

Best Practices for RBAC in Java

  1. Principle of Least Privilege - Assign minimum necessary permissions
  2. Regular Audits - Review and clean up role assignments periodically
  3. Separation of Duties - Prevent conflicting permissions in the same role
  4. Centralized Management - Use consistent RBAC across all services
  5. Logging and Monitoring - Track permission changes and access attempts
  6. Testing - Comprehensive security testing for all role combinations

Conclusion

Implementing Role-Based Access Control in Java applications provides a robust, maintainable, and scalable security framework. By leveraging Spring Security's powerful annotation-based security and custom permission evaluation, you can create fine-grained access control that meets complex business requirements.

Key benefits of this approach include:

  • Centralized security policy management
  • Flexible permission model that adapts to changing requirements
  • Clean separation of security concerns from business logic
  • Comprehensive auditing and compliance capabilities
  • Scalable architecture suitable for microservices and monolithic applications

Whether you're building a simple web application or a complex enterprise system, a well-implemented RBAC system ensures that your application remains secure, maintainable, and compliant with organizational security policies.


Leave a Reply

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


Macro Nepal Helper