Skip to content

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.

RolAccede hastaNivel numérico
Anónimo (sin sesión)PUBLIC
FREEFREE_REGISTERED0
STARTERPAID_STARTER1
PROPAID_PRO2
ELITEPAID_ELITE3
CORPORATE_USERCORPORATE2 (corp)
CORPORATE_ADMINCORPORATE3 (corp)
CONTENT_ADMINTodo10
SUPER_ADMINTodo10

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

Como suscriptor Pro Quiero acceder a contenido hasta nivel PAID_PRO Para aprovechar el contenido avanzado

Criterios: AC-ACC-004 · Prioridad: Alta · ✅ Implementado

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


US: US-ACC-001

AC-ACC-001.1 — Anónimo accede a PUBLIC

Dado que soy visitante sin sesión
Cuando solicito acceder a contenido con nivel PUBLIC
Entonces recibo el contenido (status 200)

AC-ACC-001.2 — Anónimo rechazado en FREE_REGISTERED

Dado que soy visitante sin sesión
Cuando solicito acceder a contenido FREE_REGISTERED o superior
Entonces la API responde con status 401 o 403
Y no recibo el contenido

US: US-ACC-002

AC-ACC-002.1 — FREE accede a PUBLIC y FREE_REGISTERED

Dado que tengo sesión con rol FREE
Cuando solicito contenido PUBLIC o FREE_REGISTERED
Entonces recibo el contenido (status 200)

AC-ACC-002.2 — FREE rechazado en PAID_STARTER

Dado que tengo sesión con rol FREE
Cuando solicito contenido con nivel PAID_STARTER
Entonces la API responde 403 con código "ACCESS_LEVEL_REQUIRED"

AC-ACC-003.1 — STARTER accede hasta su nivel

Dado que tengo sesión con rol STARTER
Cuando solicito contenido PUBLIC, FREE_REGISTERED o PAID_STARTER
Entonces recibo el contenido (status 200)

AC-ACC-003.2 — STARTER rechazado en PAID_PRO

Dado que tengo sesión con rol STARTER
Cuando solicito contenido PAID_PRO
Entonces la API responde 403 con código "ACCESS_LEVEL_REQUIRED"

AC-ACC-004.1 — PRO accede hasta su nivel

Dado que tengo sesión con rol PRO
Cuando solicito contenido hasta PAID_PRO
Entonces recibo el contenido (status 200)

AC-ACC-004.2 — PRO rechazado en PAID_ELITE

Dado que tengo sesión con rol PRO
Cuando solicito contenido PAID_ELITE o CORPORATE
Entonces la API responde 403

AC-ACC-005.1 — ELITE accede hasta su nivel

Dado que tengo sesión con rol ELITE
Cuando solicito contenido hasta PAID_ELITE
Entonces recibo el contenido (status 200)

AC-ACC-005.2 — ELITE rechazado en CORPORATE

Dado que tengo sesión con rol ELITE
Cuando solicito contenido CORPORATE
Entonces la API responde 403

AC-ACC-006.1 — CORPORATE_USER accede a contenido CORPORATE y niveles inferiores

Dado que tengo sesión con rol CORPORATE_USER
Cuando solicito contenido de cualquier nivel incluyendo CORPORATE
Entonces recibo el contenido (status 200)

US: US-ACC-007

AC-ACC-007.1 — FREE ve upgrade CTA en lección PAID_STARTER

Dado que tengo sesión con rol FREE
Cuando navego a la página de una lección PAID_STARTER
Entonces el elemento con data-testid "upgrade-cta" está visible
Y el elemento con data-testid "lesson-content" no está visible

AC-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_PRO
Cuando la página intenta cargar el contenido via API
Entonces la API devuelve 403 y el contenido nunca se hydrata en el cliente
Y no existe en el DOM ningún fragmento del contenido bloqueado

US: US-ACC-008

AC-ACC-008.1 — Formato estándar del error

Dado que cualquier usuario accede a contenido por encima de su nivel
Cuando la API evalúa la solicitud
Entonces responde con status 403
Y el cuerpo incluye "code": "ACCESS_LEVEL_REQUIRED" (o "INSUFFICIENT_ACCESS_LEVEL")
Y el cuerpo incluye "requiredLevel" indicando el nivel mínimo necesario

AC-ACC-008.2 — Guard no bypasseable por headers o parámetros

Dado que tengo sesión con rol FREE
Cuando solicito contenido PAID_PRO con headers adicionales (x-override-level, x-admin, etc.)
Entonces la API sigue respondiendo con status 403

US: US-ACC-009

AC-ACC-009.1 — CONTENT_ADMIN accede a cualquier nivel

Dado que tengo sesión con rol CONTENT_ADMIN
Cuando solicito contenido con cualquier nivel (incluyendo CORPORATE y PAID_ELITE)
Entonces recibo el contenido (status 200)

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_PRO
Y tengo sesión con rol FREE
Cuando solicito esa lección específica
Entonces recibo el contenido (status 200)
Y el acceso se evalúa por el nivel de la lección, no del módulo padre

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

UsuarioPUBLICFREE_REGSTARTERPROELITECORPORATE
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)
}
}

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_PRO
const 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.