Skip to main content

multi-tenant-rbac

Patrones de diseño para sistemas de permisos multi-tenant en SaaS. Roles por tenant, RLS en BD, herencia, grupos, permisos temporales, admin override, lock release automático. Anti-patrones típicos de SaaS B2B.

Aller à l'installation

Informations de source

Dépôt
Ntizar/NtizarBrainMasterMind
Dernière activité de la source
26 juin 2026 à 12:05
Langue détectée de SKILL.md
espagnol
Étoiles
2
Forks
0

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
multi-tenant-rbac
description
Patrones de diseño para sistemas de permisos multi-tenant en SaaS. Roles por tenant, RLS en BD, herencia, grupos, permisos temporales, admin override, lock release automático. Anti-patrones típicos de SaaS B2B.
version
1.0.0
author
Mastermind / David Antizar
tags
["saas","rbac","multi-tenant","permissions","security","rls","postgresql","architecture"]
# Multi-tenant RBAC — Patrones de diseño ## ¿Qué es esto? Una guía de patrones y anti-patrones para diseñar sistemas de permisos en aplicaciones SaaS multi-tenant. Basado en auditorías reales de arquitecturas SaaS que fallaron en producción. ## Principio fundamental > **Un sistema de permisos SaaS se diseña para el CASO LÍMITE, no para el caso feliz.** El caso feliz es "un admin configura permisos y todo funciona". El caso límite es "una cuenta temporal de inspector no se revocó y filtró datos de 3 ayuntamientos". --- ## Los 7 anti-patrones que matan SaaS ### 1. 🔴 Roles fijos en CHECK constraint ```sql -- ❌ ESTO MATA rol VARCHAR(50) NOT NULL CHECK (rol IN ( 'superadmin', 'admin', 'directivo', 'jefe_dept', 'operario', 'ciudadano' )); ``` **Problema:** Cada organización tiene estructura distinta. Un ayuntamiento necesita `inspector_medioambiental`, otro necesita `conductor`, otro `supervisor_rutas`. Roles fijos = forzar a todos los clientes al mismo molde. **Solución:** Tabla por tenant: ```sql -- ✅ CORRECTO CREATE TABLE roles_organizacion ( id UUID PRIMARY KEY, org_id UUID NOT NULL REFERENCES organizaciones(id), nombre VARCHAR(100) NOT NULL, nivel INTEGER NOT NULL DEFAULT 0, -- 0=admin, 50=operario, 99=lectura hereda_de UUID REFERENCES roles_organizacion(id), es_admin BOOLEAN DEFAULT false, UNIQUE (org_id, nombre) ); ``` ### 2. 🔴 Sin RLS en PostgreSQL **Problema:** Todo el control de acceso está en middleware (Node.js, Python, etc.). Si hay un bug en una ruta API, se exponen datos de TODOS los tenants. **Defensa en profundidad = 0.** **Solución:** RLS siempre activo: ```sql -- Función helper CREATE OR REPLACE FUNCTION app_org_id() RETURNS UUID AS $$ SELECT NULLIF(current_setting('app.org_id', true), '')::UUID; $$ LANGUAGE SQL STABLE; -- RLS en cada tabla ALTER TABLE activos ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON activos FOR ALL USING (org_id = app_org_id()); -- El middleware inyecta el contexto await pool.query(`SELECT set_config('app.org_id', $1, true)`, [req.user.org_id]); ``` ### 3. 🔴 Sin herencia de permisos **Problema:** Cada usuario/rol necesita permisos explícitos. Con 10 recursos × 4 acciones × 5 roles = 200 combinaciones. Si un operario necesita un permiso nuevo, se lo pones a él pero NO a su jefe. **Solución:** Herencia de roles via FK recursiva: ``` superadmin (nivel 0) → hereda de NADIE admin (nivel 10) → hereda de directivo directivo (nivel 20) → hereda de jefe_dept jefe_dept (nivel 30) → hereda de operario operario (nivel 50) → hereda de ciudadano ciudadano (nivel 99) → hereda de NADIE ``` Al consultar permisos, usar CTE recursivo para recorrer la cadena de herencia. Solo configuras permisos para el nodo base y excepciones. ### 4. 🔴 Sin distinción VER vs EDITAR **Problema:** Si un rol puede editar un recurso, también puede verlo. Un alcalde (directivo) necesita VER salarios pero NO editarlos. El modelo actual no lo permite. **Solución:** 4 niveles jerárquicos: ``` NONE (0) → No ve el recurso READ (1) → Solo GET, ni PUT ni POST ni DELETE WRITE (2) → GET + POST + PUT (crear y editar) ADMIN (3) → Todo + puede conceder permisos a otros ``` Cada nivel IMPLICA los inferiores: ADMIN tiene WRITE + READ. El middleware verifica: `nivel_requerido <= nivel_tiene`. ### 5. 🔴 Optimistic locking sin release automático **Problema:** Usuario A abre un activo → `version++`. Usuario A cierra el navegador. Nadie más puede editar ese activo NUNCA. En un equipo de 20 personas, en 1 semana están todos los activos bloqueados. **Solución:** Lock + timeout + cron: ```sql -- Añadir columnas de lock ALTER TABLE activos ADD COLUMN locked_by UUID REFERENCES usuarios(id); ALTER TABLE activos ADD COLUMN locked_at TIMESTAMPTZ; -- Cron que libera locks stale (cada 5 min) CREATE OR REPLACE FUNCTION release_stale_locks() RETURNS INTEGER AS $$ DECLARE released INTEGER; BEGIN UPDATE activos SET locked_by = NULL, locked_at = NULL WHERE locked_at IS NOT NULL AND locked_at < now() - INTERVAL '10 minutes' AND deleted_at IS NULL; GET DIAGNOSTICS released = ROW_COUNT; RETURN released; END; $$ LANGUAGE plpgsql; ``` ### 6. 🟡 Sin admin override auditable **Problema:** Cuando hay que saltarse la norma (ej: mover un activo en mantenimiento para una emergencia), el admin no tiene mecanismo. Acaba haciendo SQL directo en producción (¡peligro!). **Solución:** Flag de bypass + auditoría: ```sql ALTER TABLE usuarios ADD COLUMN bypass_security BOOLEAN DEFAULT false; ``` El middleware verifica: si `bypass_security = true`, salta los checks de permisos, pero el audit_log incluye `flag: 'BYPASS'`. El superadmin puede conceder bypass solo temporalmente (con expiración). ### 7. 🟡 Permisos con VARCHAR mágico sin estructura ```sql -- ❌ ESTO ROMPE SILENCIOSAMENTE alcance VARCHAR(100) -- 'dept:policia', 'zona:centro', 'global' ``` Un typo `'dept:polica'` vs `'dept:policia'` crea un permiso que nunca funciona. Sin error, sin alerta, sin logs. **Solución:** Estructurar el alcance: ```sql alcance_tipo VARCHAR(30) NOT NULL CHECK (alcance_tipo IN ( 'global', 'departamento', 'zona', 'propio' )), alcance_valor VARCHAR(100), -- 'policia', 'centro', NULL si global ``` --- ## Patrones que SÍ funcionan ### Grupos de usuarios Los permisos se asignan a grupos, no a usuarios. Un usuario hereda de TODOS sus grupos. ```sql CREATE TABLE grupos ( id UUID PRIMARY KEY, org_id UUID NOT NULL REFERENCES organizaciones(id), nombre VARCHAR(100) NOT NULL, UNIQUE (org_id, nombre) ); CREATE TABLE grupo_usuarios ( grupo_id UUID NOT NULL REFERENCES grupos(id), usuario_id UUID NOT NULL REFERENCES usuarios(id), PRIMARY KEY (grupo_id, usuario_id) ); -- Los permisos se asignan a grupos o a usuarios (caso excepcional) CREATE TABLE permisos_asignacion ( id UUID PRIMARY KEY, org_id UUID NOT NULL, recurso_categoria VARCHAR(100) NOT NULL, permiso_nivel VARCHAR(20) NOT NULL, alcance_tipo VARCHAR(30) NOT NULL, alcance_valor VARCHAR(100), grupo_id UUID REFERENCES grupos(id), usuario_id UUID REFERENCES usuarios(id), granted_by UUID NOT NULL, expira_en TIMESTAMPTZ, CHECK ( (grupo_id IS NOT NULL AND usuario_id IS NULL) OR (grupo_id IS NULL AND usuario_id IS NOT NULL) ) ); ``` ### Permisos temporales El 80% de las filtraciones de datos son cuentas temporales nunca revocadas. ```sql permisos_asignacion.expira_en TIMESTAMPTZ -- NULL = permanente ``` El middleware verifica en cada request: ```sql WHERE (expira_en IS NULL OR expira_en > now()) ``` Cron diario que notifica al admin_org sobre permisos próximos a expirar y permisos ya expirados. ### Rate limiting por acción de usuario Un operario no debería poder mover 10.000 activos en 1 minuto. - **Por rol:** operario = 100 acc/hora, admin = 1000 acc/hora - **Implementación:** Redis + SLIDING WINDOW - **Respuesta:** HTTP 429 con `Retry-After` header - **Configurable por tenant** (algunos necesitan más) ### Nuevos recursos data-driven Cuando se añade un módulo (ej: "drones"), no debe requerir release de software: 1. El módulo registra sus tipos de recurso en una tabla `asset_types` 2. Los permisos ya aceptan cualquier `recurso_categoria` 3. El admin configura permisos desde UI sin código nuevo --- ## Arquitectura de verificación (3 capas) ``` Capa 1: RLS en PostgreSQL (defensa última) └─ Filtra por org_id automáticamente Capa 2: Middleware de autorización └─ Verifica: ¿tiene permiso para esta acción en este recurso? └─ 4 niveles + herencia + grupos + permisos temporales Capa 3: Bypass flag + auditoría └─ Admin puede overridear pero queda registrado ``` Las 3 capas son independientes. Si falla la Capa 2, la Capa 1 sigue protegiendo. Si falla la Capa 1, la Capa 2 sigue protegiendo. --- ## Checklist para auditar un sistema de permisos - [ ] ¿Los roles son configurables por tenant? ¿O hay CHECK fijo? - [ ] ¿Hay RLS en PostgreSQL (o equivalente en BD)? - [ ] ¿Los roles heredan permisos? ¿O cada rol necesita config explícita? - [ ] ¿Hay distinción READ vs WRITE vs ADMIN? - [ ] ¿Los optimistic locks tienen release automático con timeout? - [ ] ¿Hay admin override auditable (bypass flag)? - [ ] ¿Los alcances de permisos están estructurados? ¿O son VARCHAR mágico? - [ ] ¿Hay grupos de usuarios? - [ ] ¿Hay permisos temporales con expiración? - [ ] ¿Hay rate limiting por usuario/rol? - [ ] ¿Los cambios en permisos se auditan? - [ ] ¿Nuevos tipos de recurso se pueden añadir sin código? --- ## Referencias - `terral-architecture` — Caso real de aplicación de estos patrones en TerrAn (ERP municipal SaaS) - `terral-architecture/references/audit-42-issues.md` — 14 issues de permisos encontrados en auditoría real
Voir sur GitHub