Overview
Role-Based Access Control (RBAC) is a security paradigm that restricts system access to authorized users based on their roles within an organization. It provides a structured approach to managing permissions through role assignments.
Core Concepts
- Users: Individuals who access the system
- Roles: Job functions with defined permissions
- Permissions: Access rights to specific resources
- Resources: Protected system components (APIs, data, features)
Basic Implementation
1. Core Domain Models
import java.util.*;
import java.util.stream.Collectors;
// Permission enum defining all possible actions
public enum Permission {
// User permissions
USER_READ,
USER_WRITE,
USER_DELETE,
// Product permissions
PRODUCT_READ,
PRODUCT_WRITE,
PRODUCT_DELETE,
// Order permissions
ORDER_READ,
ORDER_WRITE,
ORDER_DELETE,
// Admin permissions
ADMIN_READ,
ADMIN_WRITE,
ADMIN_DELETE
}
// Role enum defining user roles with associated permissions
public enum Role {
GUEST(Set.of(Permission.USER_READ, Permission.PRODUCT_READ)),
CUSTOMER(Set.of(
Permission.USER_READ, Permission.USER_WRITE,
Permission.PRODUCT_READ,
Permission.ORDER_READ, Permission.ORDER_WRITE
)),
MANAGER(Set.of(
Permission.USER_READ,
Permission.PRODUCT_READ, Permission.PRODUCT_WRITE,
Permission.ORDER_READ, Permission.ORDER_WRITE, Permission.ORDER_DELETE
)),
ADMIN(EnumSet.allOf(Permission.class));
private final Set<Permission> permissions;
Role(Set<Permission> permissions) {
this.permissions = permissions;
}
public Set<Permission> getPermissions() {
return permissions;
}
public boolean hasPermission(Permission permission) {
return permissions.contains(permission);
}
}
// User entity with roles
public class User {
private String id;
private String username;
private String email;
private Set<Role> roles;
private boolean active;
public User(String id, String username, String email, Set<Role> roles) {
this.id = id;
this.username = username;
this.email = email;
this.roles = roles;
this.active = true;
}
public boolean hasRole(Role role) {
return roles.contains(role);
}
public boolean hasPermission(Permission permission) {
return roles.stream()
.anyMatch(role -> role.hasPermission(permission));
}
public boolean hasAnyPermission(Permission... permissions) {
return Arrays.stream(permissions)
.anyMatch(this::hasPermission);
}
public boolean hasAllPermissions(Permission... permissions) {
return Arrays.stream(permissions)
.allMatch(this::hasPermission);
}
// Getters and setters
public String getId() { return id; }
public String getUsername() { return username; }
public String getEmail() { return email; }
public Set<Role> getRoles() { return Collections.unmodifiableSet(roles); }
public boolean isActive() { return active; }
}
2. RBAC Service Implementation
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class RBACService {
private final Map<String, User> users = new ConcurrentHashMap<>();
private final Map<String, Set<String>> userRoles = new ConcurrentHashMap<>();
private final Map<String, Set<Permission>> rolePermissions = new ConcurrentHashMap<>();
public RBACService() {
initializeDefaultRoles();
}
private void initializeDefaultRoles() {
// Initialize with enum roles and permissions
for (Role role : Role.values()) {
rolePermissions.put(role.name(), role.getPermissions());
}
}
public boolean checkPermission(String userId, Permission permission) {
User user = users.get(userId);
if (user == null || !user.isActive()) {
return false;
}
return user.hasPermission(permission);
}
public boolean checkRole(String userId, Role role) {
User user = users.get(userId);
if (user == null || !user.isActive()) {
return false;
}
return user.hasRole(role);
}
public void assignRole(String userId, Role role) {
User user = users.get(userId);
if (user != null) {
user.getRoles().add(role);
}
}
public void revokeRole(String userId, Role role) {
User user = users.get(userId);
if (user != null) {
user.getRoles().remove(role);
}
}
public Set<Permission> getUserPermissions(String userId) {
User user = users.get(userId);
if (user == null || !user.isActive()) {
return Collections.emptySet();
}
return user.getRoles().stream()
.flatMap(role -> role.getPermissions().stream())
.collect(Collectors.toSet());
}
public void addUser(User user) {
users.put(user.getId(), user);
}
public Optional<User> getUser(String userId) {
return Optional.ofNullable(users.get(userId));
}
}
Spring Security Integration
1. Security Configuration
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
private final CustomUserDetailsService userDetailsService;
public SecurityConfig(CustomUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**").permitAll()
.requestMatchers("/user/**").hasAnyRole("CUSTOMER", "MANAGER", "ADMIN")
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/manager/**").hasAnyRole("MANAGER", "ADMIN")
.anyRequest().authenticated()
)
.userDetailsService(userDetailsService)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/login?logout")
.permitAll()
);
return http.build();
}
}
2. Custom UserDetailsService
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.stream.Collectors;
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final RBACService rbacService;
private final UserRepository userRepository;
public CustomUserDetailsService(RBACService rbacService, UserRepository userRepository) {
this.rbacService = rbacService;
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
return new CustomUserDetails(user);
}
public class CustomUserDetails implements UserDetails {
private final User user;
public CustomUserDetails(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getRoles().stream()
.flatMap(role -> role.getPermissions().stream())
.map(permission -> new SimpleGrantedAuthority(permission.name()))
.collect(Collectors.toSet());
}
@Override
public String getPassword() {
return user.getPassword(); // Assume User entity has password field
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return user.isActive();
}
public User getUser() {
return user;
}
}
}
Method-Level Security
1. Annotation-Based Security
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreFilter;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ProductService {
@PreAuthorize("hasRole('CUSTOMER') or hasRole('MANAGER') or hasRole('ADMIN')")
public List<Product> getAllProducts() {
// Implementation
return List.of();
}
@PreAuthorize("hasPermission('PRODUCT', 'READ')")
public Product getProductById(String id) {
// Implementation
return new Product();
}
@PreAuthorize("hasPermission('PRODUCT', 'WRITE')")
public Product createProduct(Product product) {
// Implementation
return product;
}
@PreAuthorize("hasPermission('PRODUCT', 'DELETE')")
public void deleteProduct(String id) {
// Implementation
}
@PreAuthorize("hasRole('MANAGER') or hasRole('ADMIN')")
@PostAuthorize("returnObject.owner == authentication.name or hasRole('ADMIN')")
public Product getProductDetails(String id) {
// Implementation
return new Product();
}
@PreAuthorize("hasRole('ADMIN')")
@PostFilter("filterObject.isActive()")
public List<Product> getAllProductsForAdmin() {
// Implementation
return List.of();
}
}
2. Custom Permission Evaluator
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import java.io.Serializable;
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
private final RBACService rbacService;
public CustomPermissionEvaluator(RBACService rbacService) {
this.rbacService = rbacService;
}
@Override
public boolean hasPermission(Authentication authentication,
Object targetDomainObject,
Object permission) {
if (authentication == null || !authentication.isAuthenticated()) {
return false;
}
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
String userId = userDetails.getUser().getId();
Permission requiredPermission = Permission.valueOf(permission.toString());
return rbacService.checkPermission(userId, requiredPermission);
}
@Override
public boolean hasPermission(Authentication authentication,
Serializable targetId,
String targetType,
Object permission) {
// For cases where we don't have the domain object, only its ID and type
return hasPermission(authentication, null, permission);
}
}
REST API Implementation
1. Controller with RBAC
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api")
public class ProductController {
private final ProductService productService;
private final RBACService rbacService;
public ProductController(ProductService productService, RBACService rbacService) {
this.productService = productService;
this.rbacService = rbacService;
}
@GetMapping("/products")
public ResponseEntity<List<Product>> getAllProducts(
@RequestHeader("Authorization") String authHeader) {
// Manual permission check
String userId = extractUserIdFromToken(authHeader);
if (!rbacService.checkPermission(userId, Permission.PRODUCT_READ)) {
return ResponseEntity.status(403).build();
}
List<Product> products = productService.getAllProducts();
return ResponseEntity.ok(products);
}
@PostMapping("/products")
public ResponseEntity<Product> createProduct(
@RequestHeader("Authorization") String authHeader,
@RequestBody Product product) {
String userId = extractUserIdFromToken(authHeader);
if (!rbacService.checkPermission(userId, Permission.PRODUCT_WRITE)) {
return ResponseEntity.status(403).build();
}
Product created = productService.createProduct(product);
return ResponseEntity.ok(created);
}
@DeleteMapping("/products/{id}")
public ResponseEntity<Void> deleteProduct(
@RequestHeader("Authorization") String authHeader,
@PathVariable String id) {
String userId = extractUserIdFromToken(authHeader);
if (!rbacService.checkPermission(userId, Permission.PRODUCT_DELETE)) {
return ResponseEntity.status(403).build();
}
productService.deleteProduct(id);
return ResponseEntity.noContent().build();
}
private String extractUserIdFromToken(String authHeader) {
// Extract and validate JWT token, return user ID
// Implementation depends on your authentication mechanism
return "user-id-from-token";
}
}
Advanced RBAC Features
1. Dynamic Role Management
import org.springframework.stereotype.Service;
import java.util.*;
@Service
public class DynamicRoleService {
private final Map<String, CustomRole> customRoles = new ConcurrentHashMap<>();
private final RBACService rbacService;
public DynamicRoleService(RBACService rbacService) {
this.rbacService = rbacService;
}
public CustomRole createRole(String roleName, Set<Permission> permissions, String description) {
CustomRole role = new CustomRole(roleName, permissions, description);
customRoles.put(roleName, role);
return role;
}
public void assignCustomRoleToUser(String userId, String roleName) {
CustomRole role = customRoles.get(roleName);
if (role != null) {
// Implementation to assign custom role to user
}
}
public void updateRolePermissions(String roleName, Set<Permission> newPermissions) {
CustomRole role = customRoles.get(roleName);
if (role != null) {
role.setPermissions(newPermissions);
// Notify systems about permission changes
notifyPermissionChange(roleName);
}
}
public Set<Permission> evaluateUserPermissions(String userId) {
// Get base permissions from RBAC roles
Set<Permission> permissions = rbacService.getUserPermissions(userId);
// Add permissions from custom roles
// Implementation depends on how custom roles are stored
return permissions;
}
private void notifyPermissionChange(String roleName) {
// Notify other services or clear caches
}
public static class CustomRole {
private final String name;
private Set<Permission> permissions;
private final String description;
private final Date createdDate;
private Date modifiedDate;
public CustomRole(String name, Set<Permission> permissions, String description) {
this.name = name;
this.permissions = new HashSet<>(permissions);
this.description = description;
this.createdDate = new Date();
this.modifiedDate = new Date();
}
// Getters and setters
public String getName() { return name; }
public Set<Permission> getPermissions() { return Collections.unmodifiableSet(permissions); }
public void setPermissions(Set<Permission> permissions) {
this.permissions = new HashSet<>(permissions);
this.modifiedDate = new Date();
}
public String getDescription() { return description; }
public Date getCreatedDate() { return createdDate; }
public Date getModifiedDate() { return modifiedDate; }
}
}
2. Hierarchical RBAC
@Service
public class HierarchicalRBACService {
private final Map<Role, Set<Role>> roleHierarchy = new ConcurrentHashMap<>();
public HierarchicalRBACService() {
initializeHierarchy();
}
private void initializeHierarchy() {
// ADMIN inherits from MANAGER
roleHierarchy.put(Role.ADMIN, Set.of(Role.MANAGER, Role.CUSTOMER, Role.GUEST));
// MANAGER inherits from CUSTOMER
roleHierarchy.put(Role.MANAGER, Set.of(Role.CUSTOMER, Role.GUEST));
// CUSTOMER inherits from GUEST
roleHierarchy.put(Role.CUSTOMER, Set.of(Role.GUEST));
}
public Set<Permission> getEffectivePermissions(User user) {
Set<Permission> effectivePermissions = new HashSet<>();
for (Role role : user.getRoles()) {
effectivePermissions.addAll(getAllPermissionsForRole(role));
}
return effectivePermissions;
}
private Set<Permission> getAllPermissionsForRole(Role role) {
Set<Permission> permissions = new HashSet<>(role.getPermissions());
Set<Role> inheritedRoles = roleHierarchy.get(role);
if (inheritedRoles != null) {
for (Role inheritedRole : inheritedRoles) {
permissions.addAll(getAllPermissionsForRole(inheritedRole));
}
}
return permissions;
}
public boolean hasEffectivePermission(User user, Permission permission) {
return getEffectivePermissions(user).contains(permission);
}
public void addInheritance(Role childRole, Role parentRole) {
roleHierarchy.computeIfAbsent(childRole, k -> new HashSet<>()).add(parentRole);
}
public void removeInheritance(Role childRole, Role parentRole) {
Set<Role> parents = roleHierarchy.get(childRole);
if (parents != null) {
parents.remove(parentRole);
}
}
}
3. Context-Aware RBAC
@Service
public class ContextAwareRBACService {
public boolean checkPermissionWithContext(User user, Permission permission, SecurityContext context) {
// Base permission check
if (!user.hasPermission(permission)) {
return false;
}
// Apply context-based rules
return evaluateContextRules(user, permission, context);
}
private boolean evaluateContextRules(User user, Permission permission, SecurityContext context) {
switch (permission) {
case ORDER_DELETE:
// Only allow order deletion during business hours
return isBusinessHours() && isUsersOrder(user, context.getOrderId());
case USER_WRITE:
// Users can only edit their own profile unless they're admin
return isOwnProfile(user, context.getTargetUserId()) ||
user.hasRole(Role.ADMIN);
case PRODUCT_WRITE:
// Only allow product modifications from specific IP ranges
return isValidIpRange(context.getClientIp());
default:
return true;
}
}
private boolean isBusinessHours() {
Calendar cal = Calendar.getInstance();
int hour = cal.get(Calendar.HOUR_OF_DAY);
return hour >= 9 && hour <= 17; // 9 AM to 5 PM
}
private boolean isUsersOrder(User user, String orderId) {
// Check if the order belongs to the user
// Implementation depends on your data model
return true;
}
private boolean isOwnProfile(User user, String targetUserId) {
return user.getId().equals(targetUserId);
}
private boolean isValidIpRange(String clientIp) {
// Check if IP is in allowed range
// Implementation depends on your requirements
return true;
}
public static class SecurityContext {
private String orderId;
private String targetUserId;
private String clientIp;
private String location;
private Date requestTime;
// Constructors, getters, and setters
public SecurityContext() {}
public String getOrderId() { return orderId; }
public void setOrderId(String orderId) { this.orderId = orderId; }
public String getTargetUserId() { return targetUserId; }
public void setTargetUserId(String targetUserId) { this.targetUserId = targetUserId; }
public String getClientIp() { return clientIp; }
public void setClientIp(String clientIp) { this.clientIp = clientIp; }
public String getLocation() { return location; }
public void setLocation(String location) { this.location = location; }
public Date getRequestTime() { return requestTime; }
public void setRequestTime(Date requestTime) { this.requestTime = requestTime; }
}
}
Testing RBAC Implementation
1. Unit Tests
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.security.test.context.support.WithMockUser;
import static org.junit.jupiter.api.Assertions.*;
class RBACServiceTest {
private RBACService rbacService;
private User testUser;
@BeforeEach
void setUp() {
rbacService = new RBACService();
testUser = new User("user1", "john_doe", "[email protected]",
Set.of(Role.CUSTOMER));
rbacService.addUser(testUser);
}
@Test
void testCustomerHasProductReadPermission() {
assertTrue(rbacService.checkPermission("user1", Permission.PRODUCT_READ));
}
@Test
void testCustomerDoesNotHaveProductDeletePermission() {
assertFalse(rbacService.checkPermission("user1", Permission.PRODUCT_DELETE));
}
@Test
void testAdminHasAllPermissions() {
User adminUser = new User("admin1", "admin", "[email protected]",
Set.of(Role.ADMIN));
rbacService.addUser(adminUser);
for (Permission permission : Permission.values()) {
assertTrue(rbacService.checkPermission("admin1", permission));
}
}
@Test
void testRoleAssignment() {
assertTrue(rbacService.checkRole("user1", Role.CUSTOMER));
rbacService.assignRole("user1", Role.MANAGER);
assertTrue(rbacService.checkRole("user1", Role.MANAGER));
}
}
@SpringBootTest
class SecurityIntegrationTest {
@Autowired
private ProductService productService;
@Test
@WithMockUser(roles = "CUSTOMER")
void testCustomerCanReadProducts() {
assertDoesNotThrow(() -> productService.getAllProducts());
}
@Test
@WithMockUser(roles = "GUEST")
void testGuestCannotWriteProducts() {
assertThrows(AccessDeniedException.class,
() -> productService.createProduct(new Product()));
}
}
Best Practices
1. Security Configuration Best Practices
@Component
public class RBACBestPractices {
// 1. Principle of Least Privilege
public void assignMinimalPermissions(User user, Set<Role> roles) {
// Only assign roles necessary for user's job function
user.getRoles().clear();
user.getRoles().addAll(roles);
}
// 2. Regular Permission Audits
public void auditUserPermissions() {
// Regularly review and clean up unnecessary permissions
// Implement logic to detect and remove stale permissions
}
// 3. Secure Defaults
public User createNewUser() {
// New users get minimal permissions by default
return new User(UUID.randomUUID().toString(), "newuser", "[email protected]",
Set.of(Role.GUEST));
}
// 4. Separation of Duties
public boolean violatesSeparationOfDuties(Role newRole, Set<Role> existingRoles) {
Set<Role> conflictingRoles = Set.of(Role.ADMIN, Role.MANAGER);
return conflictingRoles.contains(newRole) &&
existingRoles.stream().anyMatch(conflictingRoles::contains);
}
}
2. Performance Considerations
@Service
public class CachedRBACService {
private final RBACService rbacService;
private final CacheManager cacheManager;
public CachedRBACService(RBACService rbacService, CacheManager cacheManager) {
this.rbacService = rbacService;
this.cacheManager = cacheManager;
}
@Cacheable(value = "userPermissions", key = "#userId")
public Set<Permission> getUserPermissions(String userId) {
return rbacService.getUserPermissions(userId);
}
@CacheEvict(value = "userPermissions", key = "#userId")
public void clearUserPermissionsCache(String userId) {
// Cache evicted automatically by annotation
}
public boolean checkPermissionCached(String userId, Permission permission) {
Set<Permission> permissions = getUserPermissions(userId);
return permissions.contains(permission);
}
}
This comprehensive RBAC implementation provides a solid foundation for securing Java applications with role-based access control, including advanced features like dynamic roles, hierarchy, and context-aware permissions.