- 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>
19 KiB
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 NULLauth.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 contextogamilit.is_admin():223-225- Verifica si usuario es admin
RLS Policies (7 implementadas):
progress_tracking.module_progress→module_progress_select_teacherprogress_tracking.learning_sessions→learning_sessions_select_teacherprogress_tracking.exercise_attempts→exercise_attempts_select_teacherprogress_tracking.exercise_submissions→exercise_submissions_select_teachereducational_content.modules→modules_select_teachereducational_content.exercises→exercises_select_teachergamification_system.user_stats→user_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.(nopublic.) - ✅ 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
- 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;
$$;
- Í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);
- Estadísticas de Roles (estimadas):
student: ~95% de usuariosadmin_teacher: ~4% de usuariossuper_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