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.
1. Ciclo de vida de un request
Sección titulada «1. Ciclo de vida de un request»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
waitUntily nunca bloquea la respuesta. - Si la inserción de auditoría falla (D1 transitorio), se registra como
warnen 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 parawrangler taily 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»- Plugin Moodle envía
GET /v1/tenants/acme/at-riskconAuthorization: Bearer <jwt>. authenticate()resuelverole=teacher,course_ids=[101, 102].policy.decide({ capability: 'atrisk.list', resource: { tenant_id: 'acme' } }):- Same tenant — no cross-tenant deny.
teacher ∈ requires(['superadmin','admin','teacher'])— allow.- Scope
courseconcourse_ids=[101,102]→clause = ' AND moodle_course_id IN (?2, ?3)',params = [101, 102]. - Allow path con
auditLevel: 'always'→ audit row enqueued.
- Handler ejecuta
SELECT … WHERE tenant_id = ?1 AND moodle_course_id IN (?2, ?3)conbind('acme', 101, 102). - Devuelve sólo students en cursos del docente.
2. Cadena de datos: del evento al informe
Sección titulada «2. Cadena de datos: del evento al informe»Moodle ── tracking.js ─┐ ├── HTTPS POST events ──→ ssea-ingestlocal_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.
Processor
Sección titulada «Processor»- 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.
Report worker
Sección titulada «Report worker»- 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.
Servicio al cliente
Sección titulada «Servicio al cliente»- 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.
Propagación de cambios desde Moodle
Sección titulada «Propagación de cambios desde Moodle»- 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_taskdel 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.
Para profundizar
Sección titulada «Para profundizar»- Roles y permisos — matriz canónica, envelope, auditoría.
- Arquitectura — componentes y almacenamiento.
- Tipos de informe — catálogo y capabilities.
- API — referencia de endpoints con capability anotada.