Control de acceso avanzado basado en roles en PostgreSQL: seguridad a nivel de fila y gestión de políticas
Utiliza la Seguridad a Nivel de Fila (RLS) de PostgreSQL con políticas RBAC para aislamiento de inquilinos, verificaciones de propiedad y un acceso a la aplicación más seguro.
Control de Acceso Avanzado Basado en Roles en PostgreSQL: Seguridad a Nivel de Fila y Gestión de Políticas
La Seguridad a Nivel de Fila (RLS) de PostgreSQL permite que tu base de datos filtre filas según el rol actual y el contexto de la sesión. Si tu aplicación atiende a múltiples inquilinos o combina acceso de administrador, gerente y usuario regular, RLS puede evitar que un simple WHERE tenant_id = ... olvidado se convierta en una fuga de datos.
RLS no reemplaza la autorización de la aplicación. Te proporciona una red de seguridad a nivel de base de datos para el límite más sensible: qué filas puede leer o escribir una conexión.
¿Por qué Seguridad a Nivel de Fila?
Problemas del Enfoque Tradicional
Filtrado a nivel de aplicación:
# Cada consulta debe recordar filtrar
query = "SELECT * FROM documents WHERE tenant_id = %s"
results = db.execute(query, [current_user_tenant_id])
Problemas:
- Fácil olvidar filtros (vulnerabilidades de seguridad)
- Duplicación de código en toda la aplicación
- Sin protección para acceso directo a la base de datos
- Requisitos de auditoría complejos
Beneficios de la Seguridad a Nivel de Fila
- Aplicación automática: Las políticas se aplican de forma transparente
- Reglas centralizadas: La lógica de seguridad en un solo lugar
- Aislamiento multiinquilino: Perfecto para aplicaciones SaaS
- Cumplimiento de auditoría: Garantías de seguridad integradas
- Rendimiento: Planificación eficiente de consultas
Configuración Básica de RLS
Paso 1: Habilitar RLS en la Tabla
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
title VARCHAR(200),
content TEXT,
owner_id INTEGER NOT NULL,
department VARCHAR(50),
is_public BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW()
);
-- Habilitar seguridad a nivel de fila
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
Paso 2: Crear Roles
-- Crear diferentes roles de usuario
CREATE ROLE regular_user;
CREATE ROLE department_manager;
CREATE ROLE admin;
-- Otorgar acceso a la tabla
GRANT SELECT, INSERT, UPDATE, DELETE ON documents TO regular_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON documents TO department_manager;
GRANT ALL ON documents TO admin;
Paso 3: Crear Políticas
-- Política: Los usuarios pueden ver sus propios documentos
CREATE POLICY user_own_documents ON documents
FOR SELECT
TO regular_user
USING (owner_id = current_setting('app.user_id')::INTEGER);
-- Política: Los usuarios pueden insertar documentos de su propiedad
CREATE POLICY user_insert_own ON documents
FOR INSERT
TO regular_user
WITH CHECK (owner_id = current_setting('app.user_id')::INTEGER);
-- Política: Los usuarios pueden actualizar sus propios documentos
CREATE POLICY user_update_own ON documents
FOR UPDATE
TO regular_user
USING (owner_id = current_setting('app.user_id')::INTEGER)
WITH CHECK (owner_id = current_setting('app.user_id')::INTEGER);
Paso 4: Establecer Variables de Sesión
En código de aplicación:
import psycopg2
# Conectar como rol
conn = psycopg2.connect(
host="localhost",
database="mydb",
user="regular_user",
password="password"
)
# Establecer contexto de usuario
cursor = conn.cursor()
cursor.execute("SELECT set_config('app.user_id', %s, false)", [str(current_user_id)])
# Ahora las consultas se filtran automáticamente
cursor.execute("SELECT * FROM documents")
# Solo devuelve documentos donde owner_id = current_user_id
Aplicación SaaS Multiinquilino
Configuración Completa Multiinquilino
-- Crear tabla de inquilinos
CREATE TABLE tenants (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- Crear tabla de usuarios
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL,
tenant_id INTEGER REFERENCES tenants(id),
role VARCHAR(20) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- Crear tabla de datos de aplicación
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL REFERENCES tenants(id),
customer_name VARCHAR(100),
amount DECIMAL(10,2),
status VARCHAR(20),
created_by INTEGER REFERENCES users(id),
created_at TIMESTAMP DEFAULT NOW()
);
-- Habilitar RLS
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- Política: Los usuarios solo ven pedidos de su inquilino
CREATE POLICY tenant_isolation ON orders
FOR ALL
TO public
USING (tenant_id = current_setting('app.tenant_id')::INTEGER)
WITH CHECK (tenant_id = current_setting('app.tenant_id')::INTEGER);
Integración con la Aplicación
Ejemplo en Django:
from django.db import connection
class TenantMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.user.is_authenticated:
with connection.cursor() as cursor:
cursor.execute(
"SELECT set_config('app.tenant_id', %s, false)",
[str(request.user.tenant_id)]
)
response = self.get_response(request)
return response
Ejemplo en Node.js:
const { Pool } = require('pg');
const pool = new Pool();
async function setTenantContext(tenantId) {
const client = await pool.connect();
try {
await client.query('SELECT set_config($1, $2, false)', ['app.tenant_id', String(tenantId)]);
return client;
} catch (err) {
client.release();
throw err;
}
}
// Uso
app.use(async (req, res, next) => {
if (req.user) {
req.dbClient = await setTenantContext(req.user.tenantId);
}
next();
});
Patrones Avanzados de Políticas
1. Control de Acceso Jerárquico
-- Los usuarios pueden ver documentos de su departamento y los públicos
CREATE POLICY department_access ON documents
FOR SELECT
TO regular_user
USING (
department = current_setting('app.user_department', true)
OR is_public = true
OR owner_id = current_setting('app.user_id')::INTEGER
);
2. Acceso Basado en Tiempo
CREATE TABLE subscriptions (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
content TEXT,
valid_from TIMESTAMP NOT NULL,
valid_until TIMESTAMP NOT NULL
);
ALTER TABLE subscriptions ENABLE ROW LEVEL SECURITY;
-- Política: Los usuarios solo pueden ver suscripciones activas
CREATE POLICY active_subscriptions ON subscriptions
FOR SELECT
TO public
USING (
user_id = current_setting('app.user_id')::INTEGER
AND NOW() BETWEEN valid_from AND valid_until
);
3. Acceso Basado en Roles con Lógica Compleja
CREATE TABLE projects (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
owner_id INTEGER,
team_id INTEGER,
visibility VARCHAR(20) -- 'private', 'team', 'public'
);
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
-- Política de visibilidad compleja
CREATE POLICY project_visibility ON projects
FOR SELECT
TO public
USING (
CASE current_setting('app.user_role', true)
WHEN 'admin' THEN true
WHEN 'manager' THEN (
team_id = current_setting('app.user_team_id')::INTEGER
OR visibility = 'public'
)
ELSE (
owner_id = current_setting('app.user_id')::INTEGER
OR visibility = 'public'
)
END
);
4. Listas de Acceso Compartido
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
title VARCHAR(200),
owner_id INTEGER NOT NULL
);
CREATE TABLE document_shares (
document_id INTEGER REFERENCES documents(id),
shared_with_user_id INTEGER,
permission VARCHAR(20), -- 'read', 'write'
PRIMARY KEY (document_id, shared_with_user_id)
);
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
-- Política: Los usuarios pueden ver documentos propios o compartidos
CREATE POLICY document_access ON documents
FOR SELECT
TO public
USING (
owner_id = current_setting('app.user_id')::INTEGER
OR id IN (
SELECT document_id
FROM document_shares
WHERE shared_with_user_id = current_setting('app.user_id')::INTEGER
)
);
-- Política: Los usuarios pueden modificar documentos propios o compartidos con permiso de escritura
CREATE POLICY document_modify ON documents
FOR UPDATE
TO public
USING (
owner_id = current_setting('app.user_id')::INTEGER
OR id IN (
SELECT document_id
FROM document_shares
WHERE shared_with_user_id = current_setting('app.user_id')::INTEGER
AND permission = 'write'
)
);
Acceso Administrativo y RLS
Los propietarios de tablas y superusuarios pueden omitir RLS a menos que se fuerce. Trata las rutas administrativas como una decisión de diseño separada, no como una ocurrencia tardía.
Opción 1: Atributo de Rol BYPASSRLS
CREATE ROLE admin_user WITH LOGIN PASSWORD 'secure_password';
ALTER ROLE admin_user BYPASSRLS;
Úsalo con moderación. BYPASSRLS es poderoso y no debe otorgarse a roles normales de aplicación.
Opción 2: Políticas Permisivas para Administradores
-- Crear política permisiva para el rol admin
CREATE POLICY admin_all_access ON documents
FOR ALL
TO admin
USING (true)
WITH CHECK (true);
Opción 3: Forzar RLS para Propietarios de Tablas
ALTER TABLE documents FORCE ROW LEVEL SECURITY;
FORCE ROW LEVEL SECURITY hace que los propietarios de tablas también sigan las políticas. Esto suele ser más seguro para tablas propiedad de la aplicación. Para trabajos de mantenimiento, usa un rol controlado separado en lugar de deshabilitar RLS dentro de un script ad hoc.
Tipos de Políticas: USING vs WITH CHECK
Cláusula USING
Controla qué filas existentes son visibles/modificables:
CREATE POLICY select_own ON documents
FOR SELECT
USING (owner_id = current_setting('app.user_id')::INTEGER);
-- El usuario solo puede SELECT filas donde owner_id coincida
Cláusula WITH CHECK
Controla qué filas nuevas/actualizadas están permitidas:
CREATE POLICY insert_own ON documents
FOR INSERT
WITH CHECK (owner_id = current_setting('app.user_id')::INTEGER);
-- El usuario solo puede INSERT filas donde owner_id coincida
Políticas Combinadas
CREATE POLICY update_own ON documents
FOR UPDATE
USING (owner_id = current_setting('app.user_id')::INTEGER)
WITH CHECK (owner_id = current_setting('app.user_id')::INTEGER);
Consideraciones de Rendimiento
1. Indexar Columnas de Políticas
-- Crear índices para columnas usadas en políticas
CREATE INDEX idx_documents_owner ON documents(owner_id);
CREATE INDEX idx_documents_tenant ON documents(tenant_id);
CREATE INDEX idx_documents_department ON documents(department);
2. Analizar Planes de Consulta
EXPLAIN ANALYZE
SELECT * FROM documents
WHERE title LIKE '%report%';
-- Verificar si el filtro de política se aplica eficientemente
3. Cuidado con Subconsultas en Políticas
Las subconsultas a veces son el modelo correcto para acceso compartido, pero necesitan índices y pruebas. Una política que consulta otra tabla por cada fila candidata puede volverse costosa.
Riesgoso sin índices:
CREATE POLICY slow_policy ON documents
USING (
id IN (
SELECT document_id
FROM shares
WHERE user_id = current_setting('app.user_id')::INTEGER
)
);
A menudo más claro con índices de soporte:
CREATE POLICY faster_policy ON documents
USING (
EXISTS (
SELECT 1 FROM shares
WHERE document_id = documents.id
AND user_id = current_setting('app.user_id')::INTEGER
)
);
Crea índices como shares(user_id, document_id) y verifica el plan con EXPLAIN (ANALYZE, BUFFERS).
Monitoreo y Auditoría
Ver Políticas Activas
-- Listar todas las políticas RLS
SELECT
schemaname,
tablename,
policyname,
permissive,
roles,
cmd,
qual,
with_check
FROM pg_policies
ORDER BY tablename, policyname;
Auditar Cambios en Políticas
Rastrea DDL de políticas en migraciones y registros de auditoría de la base de datos. Si tu entorno usa disparadores de eventos, pruébalos cuidadosamente contra tu versión de PostgreSQL e incluye eventos CREATE POLICY, ALTER POLICY y DROP POLICY. No confíes en un fragmento de disparador no probado como evidencia de cumplimiento.
Probar Efectividad de Políticas
-- Crear función de prueba
CREATE OR REPLACE FUNCTION test_user_access(
test_user_id INTEGER,
test_tenant_id INTEGER
)
RETURNS TABLE(document_id INTEGER, title VARCHAR) AS $$
BEGIN
-- Establecer variables de sesión
PERFORM set_config('app.user_id', test_user_id::text, false);
PERFORM set_config('app.tenant_id', test_tenant_id::text, false);
-- Devolver documentos accesibles
RETURN QUERY SELECT id, documents.title FROM documents;
END;
$$ LANGUAGE plpgsql SECURITY INVOKER;
-- Probar acceso
SELECT * FROM test_user_access(123, 5);
Solución de Problemas
Problema 1: Políticas No Aplicadas
Verificar si RLS está habilitado:
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public' AND tablename = 'documents';
Habilitar si está deshabilitado:
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
Problema 2: Variables de Sesión No Establecidas
Depurar configuraciones actuales:
SHOW app.user_id;
SHOW app.tenant_id;
-- O usar current_setting
SELECT current_setting('app.user_id', true);
Problema 3: Degradación del Rendimiento
Identificar políticas lentas:
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM documents WHERE title = 'test';
-- Buscar operaciones "Filter" con condiciones de política
Solución: Simplificar políticas o agregar índices
Problema 4: Acceso Denegado Inesperado
Probar como usuario específico:
SET ROLE regular_user;
SELECT set_config('app.user_id', '123', false);
SELECT * FROM documents;
-- Muestra lo que ese usuario puede ver realmente
RESET ROLE;
Mejores Prácticas
- Siempre establecer variables de sesión: Asegúrate de que los valores de
current_setting()estén poblados - Indexar columnas de políticas: Crea índices en todas las columnas usadas en USING/WITH CHECK
- Probar exhaustivamente: Verifica políticas con diferentes roles y escenarios
- Mantener políticas simples: La lógica compleja impacta el rendimiento
- Usar EXPLAIN: Analiza planes de consulta con RLS habilitado
- Documentar políticas: Comenta la lógica de políticas para mantenibilidad
- Separar preocupaciones: Usa diferentes políticas para SELECT, INSERT, UPDATE, DELETE
- Monitorear rendimiento: Configura alertas para consultas lentas
Conclusión
Comienza con una política simple: aislamiento de inquilinos en tus tablas de mayor riesgo. Establece el contexto de sesión con set_config, indexa cada columna usada por las políticas, y prueba lecturas y escrituras como roles reales de aplicación.
Una vez que el límite básico funcione, agrega políticas de propiedad, gerente o acceso compartido solo donde el producto las necesite. Las políticas pequeñas y probadas son más fáciles de razonar que una regla ingeniosa que intenta codificar todo tu modelo de permisos.