workspace/projects/gamilit/docs/01-fase-alcance-inicial/EAI-001-fundamentos/especificaciones/ET-AUTH-001-rbac.md
rckrdmrd ea1879f4ad feat: Initial workspace structure with multi-level Git configuration
- Configure workspace Git repository with comprehensive .gitignore
- Add Odoo as submodule for ERP reference code
- Include documentation: SETUP.md, GIT-STRUCTURE.md
- Add gitignore templates for projects (backend, frontend, database)
- Structure supports independent repos per project/subproject level

Workspace includes:
- core/ - Reusable patterns, modules, orchestration system
- projects/ - Active projects (erp-suite, gamilit, trading-platform, etc.)
- knowledge-base/ - Reference code and patterns (includes Odoo submodule)
- devtools/ - Development tools and templates
- customers/ - Client implementations template

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-08 10:44:23 -06:00

19 KiB

ET-AUTH-001: RBAC (Role-Based Access Control)

📋 Metadata

Campo Valor
ID ET-AUTH-001
Módulo Autenticación y Autorización
Tipo Especificación Técnica
Estado Implementado
Versión 1.0
Fecha creación 2025-11-07

🔗 Referencias

Requerimiento Funcional

📄 RF-AUTH-001: Sistema de Roles de Usuario

Implementación DDL

🗄️ Archivos relacionados:

ENUM Principal:

-- apps/database/ddl/00-prerequisites.sql:30-32
CREATE TYPE auth_management.gamilit_role AS ENUM (
    'student',       -- Estudiante regular
    'admin_teacher', -- Profesor/Administrador
    'super_admin'    -- Super administrador del sistema
);

Tablas:

  • auth_management.profiles:15 - role auth_management.gamilit_role NOT NULL
  • auth.users:15 - role auth_management.gamilit_role DEFAULT 'student'
  • system_configuration.feature_flags:20 - allowed_roles auth_management.gamilit_role[]

Funciones:

  • gamilit.get_current_user_role():196-198 - Retorna rol del usuario en contexto
  • gamilit.is_admin():223-225 - Verifica si usuario es admin

RLS Policies (7 implementadas):

  1. progress_tracking.module_progressmodule_progress_select_teacher
  2. progress_tracking.learning_sessionslearning_sessions_select_teacher
  3. progress_tracking.exercise_attemptsexercise_attempts_select_teacher
  4. progress_tracking.exercise_submissionsexercise_submissions_select_teacher
  5. educational_content.modulesmodules_select_teacher
  6. educational_content.exercisesexercises_select_teacher
  7. gamification_system.user_statsuser_stats_select_teacher

Mapeo Completo

📊 Ver en: Mapeo Requerimientos → Implementación


🏗️ Arquitectura de RBAC

Diseño General

┌─────────────────────────────────────────────────────────────┐
│                     CAPA FRONTEND                           │
│  ┌─────────────┐  ┌──────────────┐  ┌─────────────────┐   │
│  │ RoleBadge   │  │ RoleBasedUI  │  │ ProtectedRoute  │   │
│  └─────────────┘  └──────────────┘  └─────────────────┘   │
└────────────────────────┬────────────────────────────────────┘
                         │ HTTP + JWT (role claim)
┌────────────────────────▼────────────────────────────────────┐
│                     CAPA BACKEND                            │
│  ┌─────────────┐  ┌──────────────┐  ┌─────────────────┐   │
│  │ RolesGuard  │  │ @Roles()     │  │ JwtStrategy     │   │
│  └─────────────┘  └──────────────┘  └─────────────────┘   │
└────────────────────────┬────────────────────────────────────┘
                         │ SQL Queries
┌────────────────────────▼────────────────────────────────────┐
│                   CAPA DATABASE (PostgreSQL)                │
│  ┌──────────────────────────────────────────────────────┐  │
│  │  RLS POLICIES (Row Level Security)                   │  │
│  │  - students_view_own_data                            │  │
│  │  - teachers_view_classroom_students                  │  │
│  │  - superadmin_view_all (bypass RLS)                  │  │
│  └──────────────────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────────────────┐  │
│  │  ENUM: auth_management.gamilit_role                  │  │
│  │  VALUES: student | admin_teacher | super_admin       │  │
│  └──────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

📐 Matriz de Permisos

Tabla Detallada de Permisos

Recurso Acción student admin_teacher super_admin
Perfil Propio
Ver perfil
Editar perfil
Cambiar rol
Progreso
Ver propio progreso
Ver progreso de estudiantes (solo sus aulas)
Modificar progreso
Contenido Educativo
Ver contenido publicado
Ver contenido en draft (solo propio)
Crear contenido
Editar contenido (solo propio)
Aprobar contenido
Archivar contenido
Aulas
Unirse a aula (con invitación)
Crear aula
Gestionar aula (solo propias)
Asignar ejercicios (solo sus aulas)
Gamificación
Ver propias estadísticas
Ver stats de estudiantes (solo sus aulas)
Modificar XP/coins
Sistema
Ver configuración
Modificar configuración
Ver logs de auditoría
Gestionar usuarios

🔧 Implementación Técnica

1. DDL - Definición del ENUM

Ubicación: apps/database/ddl/00-prerequisites.sql:30-32

DO $$ BEGIN
    CREATE TYPE auth_management.gamilit_role AS ENUM (
        'student',       -- Estudiante regular
        'admin_teacher', -- Profesor/Administrador
        'super_admin'    -- Super administrador del sistema
    );
EXCEPTION WHEN duplicate_object THEN null; END $$;

COMMENT ON TYPE auth_management.gamilit_role IS 'Roles de usuario en la plataforma';

Decisiones de Diseño:

  • Schema explícito: auth_management. (no public.)
  • DO block con exception handling para idempotencia
  • Comentario descriptivo para documentación automática
  • Nombres descriptivos en inglés (consistencia con codebase)

2. Tabla: auth_management.profiles

Ubicación: apps/database/ddl/schemas/auth_management/tables/03-profiles.sql

CREATE TABLE auth_management.profiles (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,

    -- ROL DEL USUARIO
    role auth_management.gamilit_role NOT NULL DEFAULT 'student',

    -- Otros campos...
    display_name TEXT,
    avatar_url TEXT,
    bio TEXT,

    -- Timestamps
    created_at TIMESTAMPTZ NOT NULL DEFAULT gamilit.now_mexico(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT gamilit.now_mexico(),

    -- Constraints
    CONSTRAINT profiles_user_id_unique UNIQUE (user_id)
);

-- RLS Policies
ALTER TABLE auth_management.profiles ENABLE ROW LEVEL SECURITY;

-- Policy 1: Todos pueden ver perfiles básicos
CREATE POLICY "profiles_select_all" ON auth_management.profiles
    FOR SELECT
    TO authenticated
    USING (true);

-- Policy 2: Solo el propio usuario puede actualizar su perfil
CREATE POLICY "profiles_update_own" ON auth_management.profiles
    FOR UPDATE
    TO authenticated
    USING (user_id = gamilit.get_current_user_id())
    WITH CHECK (
        -- No puede cambiar su propio rol (solo super_admin puede)
        role = (SELECT role FROM auth_management.profiles WHERE id = profiles.id)
    );

-- Policy 3: Solo super_admin puede cambiar roles
CREATE POLICY "profiles_update_role_admin_only" ON auth_management.profiles
    FOR UPDATE
    TO authenticated
    USING (gamilit.get_current_user_role() = 'super_admin')
    WITH CHECK (true);

Notas Técnicas:

  • Default role: 'student' (seguro por defecto)
  • RLS impide cambio de rol propio (previene privilege escalation)
  • Trigger de auditoría registra cambios de rol (seguridad)

3. Backend - Enum TypeScript

Ubicación: apps/backend/src/shared/enums/gamilit-role.enum.ts

/**
 * Roles de usuario en Gamilit
 *
 * Debe estar sincronizado con:
 * - Database ENUM: auth_management.gamilit_role
 * - DDL: apps/database/ddl/00-prerequisites.sql:30-32
 */
export enum GamilitRole {
  /** Estudiante regular con acceso limitado */
  STUDENT = 'student',

  /** Profesor/Admin con permisos de gestión de aulas */
  ADMIN_TEACHER = 'admin_teacher',

  /** Super administrador con acceso completo */
  SUPER_ADMIN = 'super_admin',
}

/**
 * Mapeo de roles a nivel numérico (para comparaciones)
 */
export const RoleLevel: Record<GamilitRole, number> = {
  [GamilitRole.STUDENT]: 1,
  [GamilitRole.ADMIN_TEACHER]: 2,
  [GamilitRole.SUPER_ADMIN]: 3,
};

/**
 * Verifica si un rol tiene al menos el nivel requerido
 */
export function hasMinimumRole(
  userRole: GamilitRole,
  requiredRole: GamilitRole
): boolean {
  return RoleLevel[userRole] >= RoleLevel[requiredRole];
}

4. Backend - Guard de Roles

Ubicación: apps/backend/src/shared/guards/roles.guard.ts

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { GamilitRole } from '../enums/gamilit-role.enum';

/**
 * Guard que valida roles de usuario en endpoints
 *
 * Uso:
 * @Roles(GamilitRole.ADMIN_TEACHER, GamilitRole.SUPER_ADMIN)
 * @Get('admin/dashboard')
 * getAdminDashboard() { ... }
 */
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    // Obtener roles permitidos del decorator @Roles()
    const requiredRoles = this.reflector.getAllAndOverride<GamilitRole[]>('roles', [
      context.getHandler(),
      context.getClass(),
    ]);

    // Si no hay roles requeridos, permitir acceso
    if (!requiredRoles) {
      return true;
    }

    // Obtener usuario del request (inyectado por JwtStrategy)
    const { user } = context.switchToHttp().getRequest();

    // Validar que usuario tenga alguno de los roles permitidos
    return requiredRoles.some((role) => user.role === role);
  }
}

Decorator:

// apps/backend/src/shared/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { GamilitRole } from '../enums/gamilit-role.enum';

export const Roles = (...roles: GamilitRole[]) => SetMetadata('roles', roles);

Ejemplo de Uso:

// apps/backend/src/modules/admin/admin.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { RolesGuard } from '@shared/guards/roles.guard';
import { Roles } from '@shared/decorators/roles.decorator';
import { GamilitRole } from '@shared/enums/gamilit-role.enum';

@Controller('admin')
@UseGuards(RolesGuard)
export class AdminController {

  @Roles(GamilitRole.ADMIN_TEACHER, GamilitRole.SUPER_ADMIN)
  @Get('dashboard')
  getDashboard() {
    return { message: 'Admin dashboard' };
  }

  @Roles(GamilitRole.SUPER_ADMIN)
  @Get('system-config')
  getSystemConfig() {
    return { message: 'System configuration' };
  }
}

5. Frontend - Types y Componentes

Types:

// apps/frontend/src/types/auth.types.ts

/**
 * Roles de usuario (sincronizado con backend y database)
 */
export enum GamilitRole {
  STUDENT = 'student',
  ADMIN_TEACHER = 'admin_teacher',
  SUPER_ADMIN = 'super_admin',
}

export interface UserProfile {
  id: string;
  userId: string;
  role: GamilitRole;
  displayName: string;
  avatarUrl?: string;
  bio?: string;
  createdAt: string;
  updatedAt: string;
}

Componente: RoleBasedRoute

// apps/frontend/src/components/auth/RoleBasedRoute.tsx
import React from 'react';
import { Navigate } from 'react-router-dom';
import { useAuth } from '@/hooks/useAuth';
import { GamilitRole } from '@/types/auth.types';

interface RoleBasedRouteProps {
  children: React.ReactNode;
  allowedRoles: GamilitRole[];
  fallback?: string;
}

export const RoleBasedRoute: React.FC<RoleBasedRouteProps> = ({
  children,
  allowedRoles,
  fallback = '/unauthorized',
}) => {
  const { user } = useAuth();

  if (!user) {
    return <Navigate to="/login" replace />;
  }

  if (!allowedRoles.includes(user.role)) {
    return <Navigate to={fallback} replace />;
  }

  return <>{children}</>;
};

Uso:

// apps/frontend/src/App.tsx
import { RoleBasedRoute } from '@/components/auth/RoleBasedRoute';
import { GamilitRole } from '@/types/auth.types';

<Routes>
  <Route path="/dashboard" element={<Dashboard />} />

  <Route
    path="/admin"
    element={
      <RoleBasedRoute allowedRoles={[GamilitRole.ADMIN_TEACHER, GamilitRole.SUPER_ADMIN]}>
        <AdminPanel />
      </RoleBasedRoute>
    }
  />

  <Route
    path="/system-config"
    element={
      <RoleBasedRoute allowedRoles={[GamilitRole.SUPER_ADMIN]}>
        <SystemConfig />
      </RoleBasedRoute>
    }
  />
</Routes>

🔒 Row Level Security (RLS) Policies

Ejemplo Completo: progress_tracking.module_progress

-- apps/database/ddl/schemas/progress_tracking/tables/01-module_progress.sql

-- Habilitar RLS
ALTER TABLE progress_tracking.module_progress ENABLE ROW LEVEL SECURITY;

-- Policy 1: Estudiantes ven solo su progreso
CREATE POLICY "module_progress_select_student" ON progress_tracking.module_progress
    FOR SELECT
    TO authenticated
    USING (
        user_id = gamilit.get_current_user_id()
        AND gamilit.get_current_user_role() = 'student'
    );

-- Policy 2: Profesores ven progreso de estudiantes de sus aulas
CREATE POLICY "module_progress_select_teacher" ON progress_tracking.module_progress
    FOR SELECT
    TO authenticated
    USING (
        gamilit.get_current_user_role() = 'admin_teacher'
        AND user_id IN (
            -- Subquery: obtener estudiantes de aulas del profesor
            SELECT cm.user_id
            FROM social_features.classroom_members cm
            INNER JOIN social_features.classroom_members tcm
                ON cm.classroom_id = tcm.classroom_id
            WHERE tcm.user_id = gamilit.get_current_user_id()
                AND tcm.role = 'teacher'
                AND cm.role = 'student'
        )
    );

-- Policy 3: Super admin ve todo
CREATE POLICY "module_progress_select_admin" ON progress_tracking.module_progress
    FOR SELECT
    TO authenticated
    USING (gamilit.get_current_user_role() = 'super_admin');

Notas de Performance:

  • Subquery de profesor está indexado:
    • social_features.classroom_members(classroom_id, user_id, role)
  • Función gamilit.get_current_user_role() cachea resultado por sesión
  • Para super_admin, RLS hace bypass automático

📊 Performance y Escalabilidad

Consideraciones

  1. Caching de Rol:
-- Función cacheada para evitar múltiples lookups
CREATE OR REPLACE FUNCTION gamilit.get_current_user_role()
RETURNS auth_management.gamilit_role
LANGUAGE sql STABLE  -- STABLE = cacheable durante sesión
AS $$
    SELECT role
    FROM auth_management.profiles
    WHERE user_id = gamilit.get_current_user_id()
    LIMIT 1;
$$;
  1. Índices Requeridos:
-- Índice en columna role para filtrado rápido
CREATE INDEX idx_profiles_role ON auth_management.profiles(role);

-- Índice compuesto para RLS policies
CREATE INDEX idx_classroom_members_composite
    ON social_features.classroom_members(classroom_id, user_id, role);
  1. Estadísticas de Roles (estimadas):
  • student: ~95% de usuarios
  • admin_teacher: ~4% de usuarios
  • super_admin: <1% de usuarios

🧪 Testing

Unit Tests

// apps/backend/src/shared/guards/roles.guard.spec.ts
describe('RolesGuard', () => {
  it('should allow access if user has required role', () => {
    const user = { role: GamilitRole.ADMIN_TEACHER };
    const requiredRoles = [GamilitRole.ADMIN_TEACHER, GamilitRole.SUPER_ADMIN];

    const result = guard.canActivate(mockContext(user, requiredRoles));
    expect(result).toBe(true);
  });

  it('should deny access if user lacks required role', () => {
    const user = { role: GamilitRole.STUDENT };
    const requiredRoles = [GamilitRole.ADMIN_TEACHER];

    const result = guard.canActivate(mockContext(user, requiredRoles));
    expect(result).toBe(false);
  });
});

E2E Tests

// apps/backend/test/auth/roles.e2e-spec.ts
describe('RBAC E2E', () => {
  it('student cannot access admin endpoint', async () => {
    const student = await createUser({ role: GamilitRole.STUDENT });
    const token = await getAuthToken(student);

    const response = await request(app.getHttpServer())
      .get('/admin/dashboard')
      .set('Authorization', `Bearer ${token}`);

    expect(response.status).toBe(403);
  });

  it('admin_teacher can access admin endpoint', async () => {
    const teacher = await createUser({ role: GamilitRole.ADMIN_TEACHER });
    const token = await getAuthToken(teacher);

    const response = await request(app.getHttpServer())
      .get('/admin/dashboard')
      .set('Authorization', `Bearer ${token}`);

    expect(response.status).toBe(200);
  });
});

📚 Referencias

Documentos Relacionados

Estándares


Documento: docs/02-especificaciones-tecnicas/01-autenticacion-autorizacion/ET-AUTH-001-rbac.md Ruta relativa desde docs/: 02-especificaciones-tecnicas/01-autenticacion-autorizacion/ET-AUTH-001-rbac.md