Ir al contenido

Flujo de datos

Esta página rastrea dos flujos complementarios: el ciclo de vida de un request (lectura/escritura) y la cadena de datos del evento al informe. Ambos descansan sobre la misma superficie de política que aplica el aislamiento por tenant y por rol.

Cliente (SPA / plugin)
│ HTTPS + JWT (Bearer)
1. authenticate()
├─ Verifica firma JWT (HS256)
├─ Extrae { tenant_id, role, user_id, moodle_user_id?, course_ids? }
└─ AuthContext queda inmutable durante el request
2. policy.decide({ ctx, capability, resource })
├─ Lee la regla de RULES[capability]
├─ Cross-tenant check: resource.tenant_id ≠ ctx.tenant_id → deny (cross_tenant)
├─ Role check: ctx.role ∈ rule.requires?
├─ Scope check (self): cuando aplica, resource.id = ctx.moodle_user_id
├─ Scope-data check: teacher con course_ids vacío → deny (missing_scope_data)
├─ Si allow:
│ └─ buildScopeClause(ctx, rule.scopes) → { clause, params }
├─ Audit row construido (ts, capability, decision, reason, ...)
└─ exec.waitUntil(env.DB.prepare(INSERT INTO policy_audit_log ...).run())
3. Handler ejecuta SQL
const sql = `SELECT … WHERE tenant_id = ?1 ${decision.scope.clause}`
const rows = await env.DB.prepare(sql).bind(tenantId, ...decision.scope.params).all();
4. Respuesta
├─ allow → JSON con datos (o stream para downloads)
└─ deny → envelope uniforme:
{ ok:false, code, error, request_id }

Garantías por diseño:

  • La parte síncrona de policy.decide() se completa en menos de 1 ms P95. No emite consultas a la base.
  • El insert de auditoría es fire-and-forget — viaja por waitUntil y nunca bloquea la respuesta.
  • Si la inserción de auditoría falla (D1 transitorio), se registra como warn en logs estructurados pero la respuesta al cliente no se ve afectada.
  • Cada decisión emite además una línea de log estructurado con { level, capability, role, decision, reason, request_id } — útil para wrangler tail y agregadores.

Ejemplo concreto — un teacher pide su lista de students at-risk

Sección titulada «Ejemplo concreto — un teacher pide su lista de students at-risk»
  1. Plugin Moodle envía GET /v1/tenants/acme/at-risk con Authorization: Bearer <jwt>.
  2. authenticate() resuelve role=teacher, course_ids=[101, 102].
  3. policy.decide({ capability: 'atrisk.list', resource: { tenant_id: 'acme' } }):
    • Same tenant — no cross-tenant deny.
    • teacher ∈ requires(['superadmin','admin','teacher']) — allow.
    • Scope course con course_ids=[101,102]clause = ' AND moodle_course_id IN (?2, ?3)', params = [101, 102].
    • Allow path con auditLevel: 'always' → audit row enqueued.
  4. Handler ejecuta SELECT … WHERE tenant_id = ?1 AND moodle_course_id IN (?2, ?3) con bind('acme', 101, 102).
  5. Devuelve sólo students en cursos del docente.
Moodle ── tracking.js ─┐
├── HTTPS POST events ──→ ssea-ingest
local_ssea observer ───┘ │
│ enqueue (Cola de eventos)
ssea-processor
├── deduplicación cross-batch
└── INSERT en OLTP
OLTP ────── cron horario ──── ssea-report-worker
┌─────────────────┼─────────────────┐
▼ ▼ ▼
artifact JSON artifact CSV snapshot KV
(R2: reports/) (R2: reports/) (TTL 35 min)
ssea-api
(policy.decide → scope filter → stream)
Dashboard / plugin
  • Un evento POSTeado por el plugin pasa por validación Zod, rate-limit por tenant (1000 req/min), hashing GDPR de IP (SHA-256 con salt) y se encola.
  • Latencia objetivo: P95 < 100 ms en el edge.
  • Drena la cola en lotes (hasta 100 mensajes).
  • Deduplica por hash de campos del evento.
  • Upserta en OLTP y actualiza agregados diarios/horarios.
  • Fallos van a una cola DLQ y se inspeccionan vía métricas operativas.
  • Cron horario recorre tenants activos y encola un job por (tenant × tipo de informe).
  • Cada job lee del OLTP, computa el agregado, y escribe artifact JSON + CSV en almacenamiento de objetos.
  • Publica el snapshot KV con TTL 35 minutos para tolerar saltos puntuales del cron.
  • El dashboard pide GET /v1/tenants/:id/reports/:type/latest. La API consulta KV → si vacío, lee el último artifact de almacenamiento → re-filtra filas según el alcance del solicitante → responde.
  • Las descargas (reports.download.csv|json) hacen lo mismo — re-filtran al entregar, no asumen que el artifact ya estuviera filtrado.
  • Cambios de rol o de matrícula efectuados en Moodle se reflejan en el OLTP cuando la siguiente sincronización corre.
  • Cadencia normal: ≤ 5 minutos (la process_queue_task del plugin tira cambios cada 5 min).
  • Cambios masivos (matrículas, sync inicial) corren en migrate_data_task, diaria de madrugada.
  • Para revocaciones que requieren efecto inmediato — por ejemplo, retirar a un teacher de un curso que ya no debería ver — opera desde la consola de admin del dashboard. Esa ruta escribe directamente al OLTP y surte efecto en el siguiente request, sin esperar a Moodle.