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.

Ir para a instalação

Informações da origem

Repositório
Ntizar/NtizarBrainMasterMind
Última atividade na origem
26 de junho de 2026 às 12:05
Idioma detectado do SKILL.md
espanhol
Estrelas
2
Forks
0

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
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
Ver no GitHub