Control de Acceso
Guard: apps/api/src/common/guards/access-level.guard.ts
Módulos afectados: paths, courses, guide, practice y cualquier módulo que use @RequireAccessLevel()
Spec Playwright: apps/e2e/tests/access-control/ — ⚠️ parcial (practice.spec.ts existe)
Ver también: Modelo de Control de Acceso para el código real del guard y la jerarquía numérica.
Matriz de acceso (referencia rápida)
Section titled “Matriz de acceso (referencia rápida)”| Rol | Accede hasta | Nivel numérico |
|---|---|---|
| Anónimo (sin sesión) | PUBLIC | — |
FREE | FREE_REGISTERED | 0 |
STARTER | PAID_STARTER | 1 |
PRO | PAID_PRO | 2 |
ELITE | PAID_ELITE | 3 |
CORPORATE_USER | CORPORATE | 2 (corp) |
CORPORATE_ADMIN | CORPORATE | 3 (corp) |
CONTENT_ADMIN | Todo | 10 |
SUPER_ADMIN | Todo | 10 |
User Stories
Section titled “User Stories”US-ACC-001: Anónimo solo accede a contenido PUBLIC
Section titled “US-ACC-001: Anónimo solo accede a contenido PUBLIC”Como visitante sin sesión
Quiero poder ver contenido marcado como PUBLIC
Para conocer la plataforma antes de registrarme
Criterios: AC-ACC-001 · Prioridad: Alta · ✅ Implementado
US-ACC-002: FREE accede hasta FREE_REGISTERED
Section titled “US-ACC-002: FREE accede hasta FREE_REGISTERED”Como usuario registrado con plan gratuito (FREE)
Quiero acceder a contenido FREE_REGISTERED
Para explorar la plataforma sin pagar
Criterios: AC-ACC-002 · Prioridad: Alta · ✅ Implementado
US-ACC-003: STARTER accede hasta PAID_STARTER
Section titled “US-ACC-003: STARTER accede hasta PAID_STARTER”Como suscriptor Starter
Quiero acceder a contenido hasta nivel PAID_STARTER
Para aprovechar el catálogo de mi suscripción
Criterios: AC-ACC-003 · Prioridad: Alta · ✅ Implementado
US-ACC-004: PRO accede hasta PAID_PRO
Section titled “US-ACC-004: PRO accede hasta PAID_PRO”Como suscriptor Pro
Quiero acceder a contenido hasta nivel PAID_PRO
Para aprovechar el contenido avanzado
Criterios: AC-ACC-004 · Prioridad: Alta · ✅ Implementado
US-ACC-005: ELITE accede hasta PAID_ELITE
Section titled “US-ACC-005: ELITE accede hasta PAID_ELITE”Como suscriptor Elite
Quiero acceder a todo el catálogo hasta PAID_ELITE
Para aprovechar el acceso completo
Criterios: AC-ACC-005 · Prioridad: Alta · ✅ Implementado
US-ACC-006: CORPORATE accede a contenido CORPORATE
Section titled “US-ACC-006: CORPORATE accede a contenido CORPORATE”Como usuario corporativo (CORPORATE_USER / CORPORATE_ADMIN)
Quiero acceder a contenido marcado como CORPORATE
Para ver el catálogo personalizado de mi empresa
Criterios: AC-ACC-006 · Prioridad: Alta · ✅ Implementado
US-ACC-007: Contenido bloqueado muestra CTA de upgrade
Section titled “US-ACC-007: Contenido bloqueado muestra CTA de upgrade”Como usuario que intenta acceder a contenido superior a su nivel Quiero ver una invitación a mejorar mi plan (no un error genérico) Para entender qué suscripción necesito y poder upgradear
Criterios: AC-ACC-007 · Prioridad: Alta · ✅ Implementado
US-ACC-008: La API rechaza acceso con 403 + código estándar
Section titled “US-ACC-008: La API rechaza acceso con 403 + código estándar”Como sistema backend
Quiero que la API devuelva 403 + ACCESS_LEVEL_REQUIRED para cualquier acceso no autorizado
Para garantizar que la seguridad no depende solo de la UI
Criterios: AC-ACC-008 · Prioridad: Alta — BLOCKING · ✅ Implementado
US-ACC-009: CONTENT_ADMIN y SUPER_ADMIN acceden a todo
Section titled “US-ACC-009: CONTENT_ADMIN y SUPER_ADMIN acceden a todo”Como administrador de contenido o super administrador Quiero acceder a cualquier pieza de contenido sin restricción de nivel Para crear, revisar y editar el catálogo completo
Criterios: AC-ACC-009 · Prioridad: Alta · ✅ Implementado
US-ACC-010: Lección preview accesible por debajo del nivel del módulo padre
Section titled “US-ACC-010: Lección preview accesible por debajo del nivel del módulo padre”Como visitante o usuario FREE Quiero poder ver lecciones de preview dentro de módulos de pago Para evaluar el contenido antes de suscribirme
Criterios: AC-ACC-010 · Prioridad: Media · ✅ Implementado
Acceptance Criteria
Section titled “Acceptance Criteria”AC-ACC-001: Acceso anónimo
Section titled “AC-ACC-001: Acceso anónimo”US: US-ACC-001
AC-ACC-001.1 — Anónimo accede a PUBLIC
Dado que soy visitante sin sesiónCuando solicito acceder a contenido con nivel PUBLICEntonces recibo el contenido (status 200)AC-ACC-001.2 — Anónimo rechazado en FREE_REGISTERED
Dado que soy visitante sin sesiónCuando solicito acceder a contenido FREE_REGISTERED o superiorEntonces la API responde con status 401 o 403Y no recibo el contenidoAC-ACC-002: Acceso FREE
Section titled “AC-ACC-002: Acceso FREE”US: US-ACC-002
AC-ACC-002.1 — FREE accede a PUBLIC y FREE_REGISTERED
Dado que tengo sesión con rol FREECuando solicito contenido PUBLIC o FREE_REGISTEREDEntonces recibo el contenido (status 200)AC-ACC-002.2 — FREE rechazado en PAID_STARTER
Dado que tengo sesión con rol FREECuando solicito contenido con nivel PAID_STARTEREntonces la API responde 403 con código "ACCESS_LEVEL_REQUIRED"AC-ACC-003: Acceso STARTER
Section titled “AC-ACC-003: Acceso STARTER”AC-ACC-003.1 — STARTER accede hasta su nivel
Dado que tengo sesión con rol STARTERCuando solicito contenido PUBLIC, FREE_REGISTERED o PAID_STARTEREntonces recibo el contenido (status 200)AC-ACC-003.2 — STARTER rechazado en PAID_PRO
Dado que tengo sesión con rol STARTERCuando solicito contenido PAID_PROEntonces la API responde 403 con código "ACCESS_LEVEL_REQUIRED"AC-ACC-004: Acceso PRO
Section titled “AC-ACC-004: Acceso PRO”AC-ACC-004.1 — PRO accede hasta su nivel
Dado que tengo sesión con rol PROCuando solicito contenido hasta PAID_PROEntonces recibo el contenido (status 200)AC-ACC-004.2 — PRO rechazado en PAID_ELITE
Dado que tengo sesión con rol PROCuando solicito contenido PAID_ELITE o CORPORATEEntonces la API responde 403AC-ACC-005: Acceso ELITE
Section titled “AC-ACC-005: Acceso ELITE”AC-ACC-005.1 — ELITE accede hasta su nivel
Dado que tengo sesión con rol ELITECuando solicito contenido hasta PAID_ELITEEntonces recibo el contenido (status 200)AC-ACC-005.2 — ELITE rechazado en CORPORATE
Dado que tengo sesión con rol ELITECuando solicito contenido CORPORATEEntonces la API responde 403AC-ACC-006: Acceso CORPORATE
Section titled “AC-ACC-006: Acceso CORPORATE”AC-ACC-006.1 — CORPORATE_USER accede a contenido CORPORATE y niveles inferiores
Dado que tengo sesión con rol CORPORATE_USERCuando solicito contenido de cualquier nivel incluyendo CORPORATEEntonces recibo el contenido (status 200)AC-ACC-007: CTA de upgrade en UI
Section titled “AC-ACC-007: CTA de upgrade en UI”US: US-ACC-007
AC-ACC-007.1 — FREE ve upgrade CTA en lección PAID_STARTER
Dado que tengo sesión con rol FREECuando navego a la página de una lección PAID_STARTEREntonces el elemento con data-testid "upgrade-cta" está visibleY el elemento con data-testid "lesson-content" no está visibleAC-ACC-007.2 — El contenido real nunca llega al cliente
Dado que tengo sesión con rol FREE y navego a una lección PAID_PROCuando la página intenta cargar el contenido via APIEntonces la API devuelve 403 y el contenido nunca se hydrata en el clienteY no existe en el DOM ningún fragmento del contenido bloqueadoAC-ACC-008: Estructura del error 403
Section titled “AC-ACC-008: Estructura del error 403”US: US-ACC-008
AC-ACC-008.1 — Formato estándar del error
Dado que cualquier usuario accede a contenido por encima de su nivelCuando la API evalúa la solicitudEntonces responde con status 403Y el cuerpo incluye "code": "ACCESS_LEVEL_REQUIRED" (o "INSUFFICIENT_ACCESS_LEVEL")Y el cuerpo incluye "requiredLevel" indicando el nivel mínimo necesarioAC-ACC-008.2 — Guard no bypasseable por headers o parámetros
Dado que tengo sesión con rol FREECuando solicito contenido PAID_PRO con headers adicionales (x-override-level, x-admin, etc.)Entonces la API sigue respondiendo con status 403AC-ACC-009: Admin accede a todo
Section titled “AC-ACC-009: Admin accede a todo”US: US-ACC-009
AC-ACC-009.1 — CONTENT_ADMIN accede a cualquier nivel
Dado que tengo sesión con rol CONTENT_ADMINCuando solicito contenido con cualquier nivel (incluyendo CORPORATE y PAID_ELITE)Entonces recibo el contenido (status 200)AC-ACC-010: Lección preview
Section titled “AC-ACC-010: Lección preview”US: US-ACC-010
AC-ACC-010.1 — Lección PUBLIC dentro de módulo PAID_PRO accesible para FREE
Dado que existe una lección PUBLIC dentro de un módulo PAID_PROY tengo sesión con rol FREECuando solicito esa lección específicaEntonces recibo el contenido (status 200)Y el acceso se evalúa por el nivel de la lección, no del módulo padreTest Cases
Section titled “Test Cases”TC-ACC-001: Matriz completa Rol × Nivel de contenido
Section titled “TC-ACC-001: Matriz completa Rol × Nivel de contenido”AC: AC-ACC-001 a AC-ACC-006 · Prioridad: Alta — BLOCKING · Tipo: API
Spec: apps/e2e/tests/access-control/lesson-access-matrix.spec.ts — 📋 Pendiente
| Usuario | PUBLIC | FREE_REG | STARTER | PRO | ELITE | CORPORATE |
|---|---|---|---|---|---|---|
| Anónimo | ✅ 200 | ❌ 401 | ❌ 401 | ❌ 401 | ❌ 401 | ❌ 401 |
| FREE | ✅ 200 | ✅ 200 | ❌ 403 | ❌ 403 | ❌ 403 | ❌ 403 |
| STARTER | ✅ 200 | ✅ 200 | ✅ 200 | ❌ 403 | ❌ 403 | ❌ 403 |
| PRO | ✅ 200 | ✅ 200 | ✅ 200 | ✅ 200 | ❌ 403 | ❌ 403 |
| ELITE | ✅ 200 | ✅ 200 | ✅ 200 | ✅ 200 | ✅ 200 | ❌ 403 |
| CORP_USER | ✅ 200 | ✅ 200 | ✅ 200 | ✅ 200 | ✅ 200 | ✅ 200 |
| CONTENT_ADMIN | ✅ 200 | ✅ 200 | ✅ 200 | ✅ 200 | ✅ 200 | ✅ 200 |
// Patrón recomendado para automatizar la matriz completa:const contentIds = { PUBLIC: LESSON_PUBLIC_ID, FREE_REG: LESSON_FREE_ID, STARTER: LESSON_STARTER_ID, PRO: LESSON_PRO_ID, ELITE: LESSON_ELITE_ID, CORPORATE: LESSON_CORPORATE_ID,}const users = [ { fixture: fixtures.anonymous, canAccess: ['PUBLIC'] }, { fixture: fixtures.free, canAccess: ['PUBLIC', 'FREE_REG'] }, { fixture: fixtures.starter, canAccess: ['PUBLIC', 'FREE_REG', 'STARTER'] }, { fixture: fixtures.pro, canAccess: ['PUBLIC', 'FREE_REG', 'STARTER', 'PRO'] }, { fixture: fixtures.elite, canAccess: ['PUBLIC', 'FREE_REG', 'STARTER', 'PRO', 'ELITE'] }, { fixture: fixtures.corpUser, canAccess: Object.keys(contentIds) }, { fixture: fixtures.contentAdmin, canAccess: Object.keys(contentIds) },]for (const { fixture, canAccess } of users) { for (const [level, id] of Object.entries(contentIds)) { const res = await apiClient.get(`/lessons/${id}`, fixture.token) const shouldAccess = canAccess.includes(level) expect(res.status).toBe(shouldAccess ? 200 : canAccess.includes('PUBLIC') ? 403 : 401) }}TC-ACC-002: Estructura del error 403
Section titled “TC-ACC-002: Estructura del error 403”AC: AC-ACC-008.1 · Prioridad: Alta — BLOCKING · Tipo: API
Spec: apps/e2e/tests/access-control/error-format.spec.ts — 📋 Pendiente
const res = await apiClient.get(`/lessons/${LESSON_PRO_ID}`, fixtures.free.token)expect(res.status).toBe(403)expect(res.data).toMatchObject({ code: expect.stringMatching(/ACCESS_LEVEL_REQUIRED|INSUFFICIENT_ACCESS_LEVEL/), requiredLevel: expect.any(String),})TC-ACC-003: UI muestra upgrade CTA para contenido bloqueado
Section titled “TC-ACC-003: UI muestra upgrade CTA para contenido bloqueado”AC: AC-ACC-007.1 · Prioridad: Alta · Tipo: E2E
Spec: apps/e2e/tests/access-control/upgrade-cta.spec.ts — 📋 Pendiente
await loginAs(page, fixtures.free)await page.goto(`/learn/lessons/${LESSON_STARTER_ID}`)await expect(page.getByTestId('upgrade-cta')).toBeVisible()await expect(page.getByTestId('lesson-content')).not.toBeVisible()TC-ACC-004: Contenido bloqueado no llega al cliente (DOM)
Section titled “TC-ACC-004: Contenido bloqueado no llega al cliente (DOM)”AC: AC-ACC-007.2 · Prioridad: Alta — BLOCKING · Tipo: E2E
Spec: apps/e2e/tests/access-control/content-not-in-dom.spec.ts — 📋 Pendiente
const apiResponses: number[] = []page.on('response', r => { if (r.url().includes(`/lessons/${LESSON_PRO_ID}`)) apiResponses.push(r.status())})await loginAs(page, fixtures.free)await page.goto(`/learn/lessons/${LESSON_PRO_ID}`)expect(apiResponses).toContain(403)expect(apiResponses).not.toContain(200)TC-ACC-005: Guard no bypasseable por headers
Section titled “TC-ACC-005: Guard no bypasseable por headers”AC: AC-ACC-008.2 · Prioridad: Alta — BLOCKING · Tipo: API
Spec: apps/e2e/tests/access-control/bypass-attempts.spec.ts — 📋 Pendiente
const bypassAttempts = [ {}, { 'x-override-level': 'PAID_PRO' }, { 'x-access-level': 'SUPER_ADMIN' }, { 'x-role': 'SUPER_ADMIN' },]for (const headers of bypassAttempts) { const res = await apiClient.get(`/lessons/${LESSON_PRO_ID}`, fixtures.free.token, { headers }) expect(res.status).toBe(403)}TC-ACC-006: Lección preview accesible para FREE dentro de módulo PAID_PRO
Section titled “TC-ACC-006: Lección preview accesible para FREE dentro de módulo PAID_PRO”AC: AC-ACC-010.1 · Prioridad: Media · Tipo: API
Spec: apps/e2e/tests/access-control/preview-lessons.spec.ts — 📋 Pendiente
// LESSON_PREVIEW_ID debe ser una lección con level PUBLIC dentro de módulo PAID_PROconst res = await apiClient.get(`/lessons/${LESSON_PREVIEW_ID}`, fixtures.free.token)expect(res.status).toBe(200)TC-ACC-007: Spec existente — practice.spec.ts
Section titled “TC-ACC-007: Spec existente — practice.spec.ts”AC: AC-ACC-001 a AC-ACC-006 · Prioridad: Alta — BLOCKING · Tipo: E2E
Spec: apps/e2e/tests/access-control/practice.spec.ts — ✅ Existe
Spec actualmente existente que cubre acceso al módulo de práctica. Ampliar con los casos de la matriz TC-ACC-001 que aún no estén cubiertos.