Security Rules y permisos
Las reglas (firestore.rules) encarnan el aislamiento multi-tenant y la regla
de cero writes directos del cliente. El Admin SDK (Cloud Functions) las bypassa;
toda mutación operativa va por ahí.
Multi-tenant
Cada documento cuelga de /organizations/{orgId}/…. Un usuario solo lee los datos
de las orgs donde es miembro, verificado por:
exists(/organizations/{orgId}/members/{request.auth.uid})Patrón general
| Colección | Lectura | Escritura |
|---|---|---|
ontologyTypes, objects, actionTypes, dataSources | miembros | solo Cloud Functions |
actionExecutions, auditEvents, ingestionRuns | miembros | deny-all (inmutables) |
members | miembros | solo Cloud Functions |
savedViews | solo el owner | solo Cloud Functions |
dataSourceSecrets | deny-all (ni lectura) | deny-all |
/users/{uid} | solo el propio usuario | deny-all (allow update: if false) |
/accessRequests, /rateLimits | deny-all | deny-all (solo la Function escribe) |
Los secretos (API keys de REST, token de webhook) no viven en el doc legible
de la DataSource: se aíslan en /dataSourceSecrets (deny-all incluso para lectura),
porque las rules no pueden ocultar campos en una lectura.
Inmutabilidad
actionExecutions, auditEvents e ingestionRuns son append-only: create,
update y delete están en if false para todos, incluido admin. Es la garantía
del audit trail.
SavedViews privadas
allow read: if isMemberOf(orgId) && resource.data.owner == request.auth.uid;El listado del cliente consulta where('owner','==',uid), que matchea la regla
por-doc. La escritura va solo por callable.
Roles
Se resuelven en el backend leyendo el member doc (sin custom claims — decisión sellada, a revisar: ver la nota de Storage abajo):
| Rol | Alcance |
|---|---|
admin | todo + usuarios |
editor | Ontología / Actions / DataSources |
operator | ejecutar Actions, mutar datos operacionales |
viewer | lectura |
Storage
storage.rules cubre los uploads de CSV bajo el prefijo de la org
(organizations/{orgId}/datasource-uploads/). La Cloud Function valida el tenant
del path antes de leer (el Admin SDK bypassa las rules de Storage).
Gate de rol interino (2026-06-09). El diseño original leía el rol del miembro con
un firestore.get cross-service dentro de la Storage Rule. En prod ese
cross-service get devuelve DENY (probado con bisect: admin + text/csv → 403; sin
el get → 200), aunque en el emulador funciona. Por eso canUpload(orgId) quedó en
isSignedIn() (gate: signed-in + path tenant-scoped + content-type/size). La
autorización por rol real de la ingesta la hace la callable ingestDataSource
(requireOrgRole admin/editor/operator), que además revalida el tenant del path. La
lectura de Storage sigue if false.
Endurecimiento pendiente: rol por custom claims en el token para reponer el
gate por rol en Storage sin cross-service get. Detalle y plan en
operacion/troubleshooting → “Estado abierto”.
Auditar las rules: hay tests en apps/functions/src/rules/ (firestore-rules.test.ts)
que se corren contra el emulador. La lógica desplegada coincide byte-a-byte con el
repo.