Skip to main content

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)

OperationAdminProfessionalPatient (Self)Patient (Other)
Create
Read
Update
Delete

Consultas (Appointments)

OperationAdminProfessional (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

1

Authenticate User

Obtain an access token and extract the user’s role from API responses.
2

Check Permissions Client-Side

Hide or disable UI elements based on user roles to improve UX.
3

Handle 403 Errors

Always handle 403 Forbidden responses gracefully, even with client-side checks.
4

Never Trust Client

Remember that server-side authorization is the source of truth.

Getting Current User Information

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 CodeDescriptionCause
401 UnauthorizedNot authenticatedMissing or invalid token
403 ForbiddenAuthenticated but not authorizedInsufficient permissions for operation
404 Not FoundResource doesn’t existMay 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

Build docs developers (and LLMs) love