Overview
VidaPlus implements role-based access control (RBAC) to manage user permissions. The system supports three primary user roles, each with specific permissions and access levels.
User Roles
The system defines three user roles in vidaplus/utils/general.py:4:
class UserRole ( str , Enum ):
ADMIN = 'ADMIN'
PACIENTE = 'PACIENTE'
PROFISSIONAL = 'PROFISSIONAL'
Role Hierarchy
Admin Full system access with superuser privileges. Can manage all resources and users.
Profissional Healthcare professionals (doctors, nurses). Can manage consultations, prescriptions, and patient records.
Paciente Patients with access to their own medical records and appointments.
Permission System
Superuser Flag
All users inherit from BaseUser which includes permission flags (vidaplus/models/models.py:29):
@table_registry.mapped_as_dataclass
class BaseUser :
__tablename__ = 'users'
id : Mapped[ int ] = mapped_column( primary_key = True , init = False )
nome: Mapped[ str ] = mapped_column(String( 255 ))
email: Mapped[ str ] = mapped_column(String( 255 ), unique = True , nullable = False )
senha: Mapped[ str ] = mapped_column(String( 255 ), nullable = False )
telefone: Mapped[ str ] = mapped_column(String( 20 ), nullable = True )
created_at: Mapped[datetime] = mapped_column( server_default = func.now(), init = False )
tipo: Mapped[UserRole] = mapped_column(Enum(UserRole))
is_active: Mapped[ bool ] = mapped_column()
is_superuser: Mapped[ bool ] = mapped_column() # Admin permission flag
The is_superuser flag grants administrative privileges regardless of the user’s role type.
Authorization Patterns
Pattern 1: Superuser-Only Access
Restrict operations to administrators only:
@router.post ( '/' , status_code = HTTPStatus. CREATED , response_model = PacienteUserPublic)
def create_user ( user : PacienteUserSchema, session : Session, current_user : CurrentUser):
if not current_user.is_superuser:
raise HTTPException(
status_code = HTTPStatus. FORBIDDEN ,
detail = 'Apenas usuários com permissão de administrador podem criar pacientes.' ,
)
# ... create user logic
Pattern 2: Owner or Admin Access
Allow users to access their own resources, or admins to access any:
@router.get ( '/ {user_id} ' , response_model = PacienteUserPublic)
def get_user ( user_id : int , session : Session, current_user : CurrentUser):
user = session.get(PacienteUser, user_id)
if not user:
raise HTTPException(
status_code = HTTPStatus. NOT_FOUND , detail = 'User not found'
)
if not current_user.is_superuser and current_user.id != user_id:
raise HTTPException(
status_code = HTTPStatus. FORBIDDEN ,
detail = 'Você não tem permissão para acessar este usuário' ,
)
return user
Pattern 3: Resource-Based Access
Filter results based on user permissions:
@router.get ( '/' , response_model = UserList)
def get_users ( session : Session, filter_users : Annotated[FilterPage, Query()], current_user : CurrentUser):
pacientes = session.scalars(
select(PacienteUser)
.where(PacienteUser.tipo == UserRole. PACIENTE )
.offset(filter_users.offset)
.limit(filter_users.limit)
).all()
# Non-admin users only see themselves
if not current_user.is_superuser:
pacientes = [
paciente for paciente in pacientes if paciente.id == current_user.id
]
return { 'pacientes' : pacientes}
Pattern 4: Role-Specific Access
Restrict operations to specific roles with ownership validation:
@router.post ( '/' , status_code = HTTPStatus. CREATED , response_model = ConsultaSchemaPublic)
def create_consulta ( consulta : ConsultaSchema, session : Session, current_user : CurrentUser):
# Only the assigned professional or admins can create consultations
if (
not current_user.is_superuser
and current_user.id != consulta.profissional_id
):
raise HTTPException(
status_code = HTTPStatus. FORBIDDEN ,
detail = 'Você não tem permissão para criar esta consulta' ,
)
# ... create consultation logic
Permission Checks by Resource
Pacientes (Patients)
Operation Admin Professional Patient (Self) Patient (Other) Create ✅ ❌ ❌ ❌ Read ✅ ✅ ✅ ❌ Update ✅ ❌ ✅ ❌ Delete ✅ ❌ ✅ ❌
Consultas (Appointments)
Operation Admin Professional (Owner) Professional (Other) Patient (Owner) Patient (Other) Create ✅ ✅ ❌ ❌ ❌ Read ✅ ✅ ❌ ✅ ❌ Update ✅ ✅ ❌ ✅ ❌ Delete ✅ ✅ ❌ ❌ ❌
Prescrições (Prescriptions)
Similar to Consultas - only the assigned professional or admin can create/modify.
Implementing Authorization in Your App
Authenticate User
Obtain an access token and extract the user’s role from API responses.
Check Permissions Client-Side
Hide or disable UI elements based on user roles to improve UX.
Handle 403 Errors
Always handle 403 Forbidden responses gracefully, even with client-side checks.
Never Trust Client
Remember that server-side authorization is the source of truth.
Every authenticated endpoint receives the current user via dependency injection:
from typing import Annotated
from fastapi import Depends
from vidaplus.security import get_current_user
from vidaplus.models.models import BaseUser
CurrentUser = Annotated[BaseUser, Depends(get_current_user)]
@router.get ( '/me' )
def get_current_user_info ( current_user : CurrentUser):
return {
'id' : current_user.id,
'nome' : current_user.nome,
'email' : current_user.email,
'tipo' : current_user.tipo,
'is_superuser' : current_user.is_superuser,
'is_active' : current_user.is_active
}
User Types and Polymorphism
VidaPlus uses SQLAlchemy’s polymorphic inheritance for user types:
# Base user with polymorphic identity
class BaseUser :
__mapper_args__ = {
'polymorphic_on' : 'tipo' ,
'polymorphic_identity' : 'BASE' ,
}
tipo: Mapped[UserRole] = mapped_column(Enum(UserRole))
# Admin user
class AdminUser ( BaseUser ):
__mapper_args__ = {
'polymorphic_identity' : UserRole. ADMIN .value,
}
# Patient user with additional fields
class PacienteUser ( BaseUser ):
__mapper_args__ = {
'polymorphic_identity' : UserRole. PACIENTE .value,
}
cpf: Mapped[ str ] = mapped_column(String( 14 ), unique = True , nullable = False )
data_nascimento: Mapped[date] = mapped_column(Date)
# ... address fields
# Professional user with credentials
class ProfissionalUser ( BaseUser ):
__mapper_args__ = {
'polymorphic_identity' : UserRole. PROFISSIONAL .value,
}
crmCoren: Mapped[ str ] = mapped_column(String( 10 ), unique = True , nullable = False )
especialidade: Mapped[Especialidade] = mapped_column(Enum(Especialidade))
Common Authorization Errors
Status Code Description Cause 401 UnauthorizedNot authenticated Missing or invalid token 403 ForbiddenAuthenticated but not authorized Insufficient permissions for operation 404 Not FoundResource doesn’t exist May also be used to hide unauthorized resources
Some endpoints return 404 Not Found instead of 403 Forbidden to prevent information disclosure about resource existence.
Best Practices
Fail Secure Default to denying access. Explicitly grant permissions rather than denying them.
Check Early Validate permissions at the start of endpoint handlers before querying databases.
Consistent Messages Use consistent error messages to avoid leaking information about system internals.
Log Access Log authorization failures for security monitoring and debugging.
Next Steps
Authentication Learn how to authenticate users
Database Models Explore user models and relationships