Skip to main content

Overview

The Authorization Service implements a comprehensive Role-Based Access Control (RBAC) system that provides fine-grained access management through a hierarchical model:
Users → Roles → Permissions → Modules

RBAC Entities

Users

Users are the core entity representing individuals who can authenticate and access the system. Domain Model: UserDomain (auth/domain/model/user/UserDomain.java:14)
public class UserDomain {
    private final UserId userId;            // UUID
    private UserNames names;                // Name, lastName, secondName
    private UserEmail email;                // Unique email
    private UserPassword password;          // BCrypt hashed
    private AccountStatus status;           // enabled, locked, expired
    private final Set<RoleDomain> roles;    // Assigned roles
    
    // Business methods
    public void addRole(RoleDomain role) { /*...*/ }
    public void removeRole(RoleDomain role) { /*...*/ }
}
Key Features:
  • Uses Value Objects (UserId, UserEmail) for type safety
  • Enforces business rules (can’t assign duplicate roles)
  • Immutable user ID
  • Rich account status tracking
User Ports:
  • Inbound: UserUseCasePort (auth/domain/port/in/UserUseCasePort.java:13) - Use cases
  • Outbound: UserRepositoryPort (auth/domain/port/out/UserRepositoryPort.java:13) - Persistence

Roles

Roles group permissions and are assigned to users. Domain Model: RoleDomain (auth/domain/model/role/RoleDomain.java:14)
public class RoleDomain {
    private final RoleId roleId;                      // UUID
    private RoleName name;                            // e.g., "ADMIN", "USER"
    private RoleDescription description;
    private final Set<PermissionDomain> permissions;  // Associated permissions
    private Status status;                            // ACTIVO, INACTIVO
    
    // Business methods
    public void addPermission(PermissionDomain permission) {
        // Check if permission already exists
        boolean exists = permissions.stream()
            .map(PermissionDomain::getPermissionId)
            .anyMatch(pid -> pid.equals(permission.getPermissionId()));
        
        if (exists) {
            throw new PermissionAlreadyAssignedException(
                "El permiso ya está asignado al rol"
            );
        }
        this.permissions.add(permission);
    }
    
    public void removePermission(PermissionDomain permission) { /*...*/ }
}
Examples:
  • ADMIN - Full system access
  • MANAGER - Manage users and view reports
  • OPERATOR - Execute operations
  • VIEWER - Read-only access

Permissions

Permissions represent specific actions or capabilities. Domain Model: PermissionDomain
public class PermissionDomain {
    private final PermissionId permissionId;  // UUID
    private PermissionName name;              // e.g., "READ_USERS"
    private PermissionDescription description;
    private ModuleDomain module;              // Associated module
    private Status status;
}
Naming Convention:
  • {ACTION}_{RESOURCE} format
  • Examples: READ_USERS, WRITE_USERS, DELETE_ROLES, EXECUTE_REPORTS

Modules

Modules organize permissions by functional area or feature. Domain Model: ModuleDomain
public class ModuleDomain {
    private final ModuleId moduleId;      // UUID
    private ModuleName name;              // e.g., "Users", "Reports"
    private ModuleDescription description;
    private Status status;
}
Examples:
  • Users - User management permissions
  • Roles - Role management permissions
  • Audit - Audit log permissions
  • Reports - Reporting permissions

Entity Relationships

How RBAC Works

1. User Gets Roles

Roles are assigned to users through the UserUseCasePort:
public interface UserUseCasePort {
    void assignRole(UUID userId, UUID roleId);
    void revokeRole(UUID userId, UUID roleId);
}
Example Request:
POST /api/users/{userId}/roles/{roleId}
Authorization: Bearer {admin-token}

2. Roles Contain Permissions

When a role is assigned permissions, the domain model enforces rules:
RoleDomain adminRole = roleRepository.findById(roleId);
PermissionDomain readUsersPermission = permissionRepository.findById(permId);

// Domain logic validates no duplicates
adminRole.addPermission(readUsersPermission);

roleRepository.save(adminRole);

3. Permissions Are Checked

Permissions are embedded in the JWT token during login:
public String generateToken(UserDomain user) {
    // Flatten permissions from all roles
    List<String> permissions = user.getRoles().stream()
        .flatMap(r -> r.getPermissions().stream())
        .map(p -> p.getName().value())
        .distinct()  // Remove duplicates
        .collect(Collectors.toList());
    
    return Jwts.builder()
        .claim("permissions", permissions)
        // ...
        .compact();
}

4. Access Control Enforcement

Permissions are enforced using Spring Security annotations:
@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @GetMapping
    @PreAuthorize("hasAuthority('PERM_READ_USERS')")
    public PaginatedResponse<UserResponse> search() {
        // Only users with READ_USERS permission can access
    }
    
    @PostMapping
    @PreAuthorize("hasAuthority('PERM_WRITE_USERS')")
    public UserResponse create(@RequestBody CreateUserRequest request) {
        // Only users with WRITE_USERS permission can access
    }
    
    @DeleteMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN')")
    public void delete(@PathVariable UUID id) {
        // Only users with ADMIN role can access
    }
}

Permission Aggregation

If a user has multiple roles with overlapping permissions, they are aggregated (union): Example:
User: john@example.com
├── Role: MANAGER
│   ├── READ_USERS
│   ├── WRITE_USERS
│   └── READ_REPORTS
└── Role: OPERATOR
    ├── READ_USERS (duplicate, ignored)
    ├── EXECUTE_OPERATIONS
    └── READ_AUDIT

Final Permissions:
- READ_USERS
- WRITE_USERS
- READ_REPORTS
- EXECUTE_OPERATIONS
- READ_AUDIT
The deduplication happens in the JWT generation (security/util/JwtUtil.java:40-44):
List<String> permissions = user.getRoles().stream()
    .flatMap(r -> r.getPermissions().stream())
    .map(p -> p.getName().value())
    .distinct()  // Remove duplicates
    .collect(Collectors.toList());

Domain-Driven Design

The RBAC implementation follows DDD principles:

Aggregates

User Aggregate:
  • Root: UserDomain
  • Entities: RoleDomain (referenced by ID in persistence)
  • Value Objects: UserId, UserEmail, UserNames, UserPassword
Role Aggregate:
  • Root: RoleDomain
  • Entities: PermissionDomain (referenced by ID in persistence)
  • Value Objects: RoleId, RoleName, RoleDescription

Invariants Enforced

  1. No duplicate roles per user (auth/domain/model/user/UserDomain.java:85-95):
public void addRole(RoleDomain role) {
    boolean exists = roles.stream()
        .map(RoleDomain::getRoleId)
        .anyMatch(rid -> rid.equals(role.getRoleId()));
    
    if (exists) {
        throw new RoleAlreadyAssignedException("El usuario ya tiene ese rol");
    }
    this.roles.add(role);
}
  1. No duplicate permissions per role (auth/domain/model/role/RoleDomain.java:52-64):
public void addPermission(PermissionDomain permission) {
    boolean exists = permissions.stream()
        .map(PermissionDomain::getPermissionId)
        .anyMatch(pid -> pid.equals(permission.getPermissionId()));
    
    if (exists) {
        throw new PermissionAlreadyAssignedException(
            "El permiso ya está asignado al rol"
        );
    }
    this.permissions.add(permission);
}
  1. Email uniqueness - Enforced at repository level
  2. Required fields - Validated by value objects:
public record UserEmail(String value) {
    public UserEmail {
        if (value == null || value.isBlank()) {
            throw new IllegalArgumentException("Email cannot be blank");
        }
        // Additional validation...
    }
}

Repository Adapters

The persistence layer implements the repository ports: User Repository (auth/adapter/out/jpa/UserRepositoryAdapter.java:27):
@Component
public class UserRepositoryAdapter implements UserRepositoryPort {
    
    @Override
    public UserDomain save(UserDomain domain) {
        // Convert domain to JPA entity
        User entityToSave = UserJPAMapper.fromDomain(domain);
        
        // Fetch role entities by ID (roles are separate aggregates)
        if (domain.getRoles() != null) {
            var roles = domain.getRoles().stream()
                .map(r -> roleRepository.getReferenceById(r.getRoleId().id()))
                .collect(Collectors.toSet());
            entityToSave.setRoles(roles);
        }
        
        var saved = userRepository.save(entityToSave);
        return UserJPAMapper.toDomain(saved);
    }
}
This maintains the separation between domain and persistence concerns.

Use Cases

Create User with Role

// 1. Create user
CreateUserRequest request = new CreateUserRequest(
    "john.doe@example.com",
    "password123",
    "John",
    "Doe",
    null
);
UserResponse user = userService.create(request);

// 2. Assign role
UUID userId = user.getUserId();
UUID roleId = /* MANAGER role ID */;
userService.assignRole(userId, roleId);

Create Role with Permissions

// 1. Create role
CreateRoleRequest roleRequest = new CreateRoleRequest(
    "MANAGER",
    "Can manage users and view reports"
);
RoleResponse role = roleService.create(roleRequest);

// 2. Assign permissions
UUID roleId = role.getRoleId();
UUID readUsersPermId = /* READ_USERS permission ID */;
UUID writeUsersPermId = /* WRITE_USERS permission ID */;

roleService.assignPermission(roleId, readUsersPermId);
roleService.assignPermission(roleId, writeUsersPermId);

Check User Permissions

Permissions are automatically checked via Spring Security:
// In controller
@GetMapping("/sensitive-data")
@PreAuthorize("hasAuthority('PERM_READ_SENSITIVE')")
public SensitiveData getData() {
    // If user doesn't have permission, returns 403 Forbidden
    return sensitiveDataService.getData();
}
Programmatic checks:
@Service
public class DataService {
    
    public void processData() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        
        boolean hasPermission = auth.getAuthorities().stream()
            .anyMatch(a -> a.getAuthority().equals("PERM_PROCESS_DATA"));
        
        if (!hasPermission) {
            throw new AccessDeniedException("Missing PROCESS_DATA permission");
        }
        
        // Process data...
    }
}

Status Management

All entities support status management:
public enum Status {
    ACTIVO,   // Active
    INACTIVO  // Inactive
}
Users have richer status via AccountStatus:
  • enabled - Can log in
  • accountNonExpired - Account not expired
  • accountNonLocked - Not locked out
  • credentialsNonExpired - Password not expired
Deactivating entities:
// Deactivate user
userService.deactivate(userId);  // Sets status to INACTIVO

// Reactivate user
userService.activate(userId);    // Sets status to ACTIVO
Inactive roles/permissions are not included in authorization checks.

Searching and Filtering

The RBAC system supports advanced search (auth/domain/port/out/UserRepositoryPort.java:20):
Page<UserDomain> searchByUsernameOrUserId(
    String email, 
    Status status, 
    Pageable pageable
);
Example:
GET /api/users/search?email=john&status=ACTIVO&page=0&size=20

Best Practices

Principle of Least Privilege

Grant users only the minimum permissions needed for their tasks

Role Hierarchy

Design roles hierarchically: VIEWER → OPERATOR → MANAGER → ADMIN

Module Organization

Group related permissions into modules for easier management

Audit Changes

Track all role and permission changes using the audit system

Next Steps

User Management API

Explore user management endpoints

Role Management API

Explore role management endpoints

Authentication

Learn how JWT tokens include permissions

Audit Logging

See how RBAC changes are audited

Build docs developers (and LLMs) love