Implementar RBAC
Cómo implementar control de acceso basado en roles con roles jerárquicos, grants de permisos y middleware de enforce en Python, Node.js y Java.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Descripción General
Role-Based Access Control (RBAC) asigna permisos a roles, y roles a usuarios. Un usuario hereda todos los permisos de sus roles. Este modelo es simple, auditable y escala a cientos de roles sin la complejidad de sistemas basados en atributos. RBAC es la elección correcta cuando las decisiones de acceso dependen principalmente de la función laboral.
Cuándo Usar
- Tu aplicación tiene funciones laborales claramente definidas (admin, editor, viewer, auditor)
- El control de acceso cambia infrecuentemente
- Necesitas una traza de auditoría fácil de explicar a stakeholders no técnicos
- Frameworks de compliance (SOC 2, ISO 27001) requieren matrices de control de acceso documentadas
Cuándo NO Usar
- Las decisiones de acceso dependen de tiempo, ubicación o dispositivo — usa ABAC
- Usuarios necesitan permisos diferentes para diferentes proyectos — usa ACLs a nivel de recurso
- El mismo usuario actúa en nombre de múltiples organizaciones — usa RBAC multi-tenant
Implementación Paso a Paso
Python (Flask + SQLAlchemy)
from functools import wraps
from flask import Flask, g, jsonify
from sqlalchemy import Column, Integer, String, Table, ForeignKey
from sqlalchemy.orm import relationship, declarative_base
Base = declarative_base()
user_roles = Table('user_roles', Base.metadata,
Column('user_id', Integer, ForeignKey('users.id')),
Column('role_id', Integer, ForeignKey('roles.id'))
)
role_permissions = Table('role_permissions', Base.metadata,
Column('role_id', Integer, ForeignKey('roles.id')),
Column('permission_id', Integer, ForeignKey('permissions.id'))
)
class Permission(Base):
__tablename__ = 'permissions'
id = Column(Integer, primary_key=True)
resource = Column(String(100), nullable=False)
action = Column(String(50), nullable=False)
class Role(Base):
__tablename__ = 'roles'
id = Column(Integer, primary_key=True)
name = Column(String(50), unique=True)
parent_id = Column(Integer, ForeignKey('roles.id'), nullable=True)
permissions = relationship('Permission', secondary=role_permissions)
parent = relationship('Role', remote_side=[id], backref='children')
def get_all_permissions(self):
perms = set(self.permissions)
if self.parent:
perms.update(self.parent.get_all_permissions())
return perms
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
roles = relationship('Role', secondary=user_roles)
def has_permission(self, resource, action):
for role in self.roles:
for perm in role.get_all_permissions():
if perm.resource == resource and perm.action == action:
return True
return False
def require_permission(resource, action):
def decorator(f):
@wraps(f)
def wrapped(*args, **kwargs):
if not g.user or not g.user.has_permission(resource, action):
return jsonify({"error": "Forbidden"}), 403
return f(*args, **kwargs)
return wrapped
return decorator
@app.route('/orders/<int:id>', methods=['PUT'])
@require_permission('orders', 'write')
def update_order(id):
return jsonify({"message": f"Updated order {id}"})
Node.js (Express + Prisma)
// middleware/rbac.js
const prisma = require('../prisma/client');
function requirePermission(resource, action) {
return async (req, res, next) => {
const user = await prisma.user.findUnique({
where: { id: req.user.id },
include: {
roles: {
include: {
permissions: true,
parent: { include: { permissions: true } }
}
}
}
});
const hasPermission = user.roles.some(role => {
const rolePerms = [...role.permissions];
if (role.parent) rolePerms.push(...role.parent.permissions);
return rolePerms.some(p => p.resource === resource && p.action === action);
});
if (!hasPermission) return res.status(403).json({ error: 'Forbidden' });
next();
};
}
// Uso
app.put('/orders/:id', authenticate, requirePermission('orders', 'write'), updateOrder);
Java (Spring Security)
@Entity
public class Role {
@Id @GeneratedValue private Long id;
@Column(unique = true, nullable = false) private String name;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "role_permissions",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "permission_id"))
private Set<Permission> permissions = new HashSet<>();
@ManyToOne @JoinColumn(name = "parent_id") private Role parent;
public Set<Permission> getAllPermissions() {
Set<Permission> all = new HashSet<>(permissions);
if (parent != null) all.addAll(parent.getAllPermissions());
return all;
}
}
@Entity
public class Permission {
@Id @GeneratedValue private Long id;
@Column(nullable = false) private String resource;
@Column(nullable = false) private String action;
}
@Entity
public class User {
@Id @GeneratedValue private Long id;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();
public boolean hasPermission(String resource, String action) {
return roles.stream()
.flatMap(r -> r.getAllPermissions().stream())
.anyMatch(p -> p.getResource().equals(resource) && p.getAction().equals(action));
}
}
@PreAuthorize("hasAuthority('orders:write')")
public Order updateOrder(Long orderId, OrderUpdateRequest req) { }
Mejores Prácticas
- Define permisos como pares recurso + acción.
orders:reades más claro queVIEWERy permite composición fina. Los roles agrupan permisos, no los reemplazan. - Implementa jerarquía de roles, no duplicación. Si
editorpuede hacer todo lo queviewer, haz queeditorherede devieweren lugar de copiar permisos. - Deniega por defecto. Sin permiso explícito, la respuesta es siempre
false. No uses lógica “allow unless denied”. - Cachea lookups de permisos. Recorrer jerarquías y hacer joins en cada request es costoso. Cachea el conjunto efectivo de permisos por usuario.
- Audita cambios de permisos. Loggea cada grant, revoke y asignación de rol con quién hizo el cambio y cuándo.
Errores Comunes
- Usar nombres de rol como permisos.
if (user.role == 'admin')hardcodea lógica de negocio en código. Agregar un rolsupervisorrompe todos los checks. - No considerar jerarquía de roles. Un usuario con rol
admintambién es implícitamenteeditoryviewersi la jerarquía lo configura. - Guardar roles en JWTs sin expiración. Un usuario degradado retiene acceso de admin hasta que el JWT expire. Usa tokens de vida corta o una lista de revocación.
- Ignorar el principio de least privilege. Roles por defecto con permisos amplios exponen datos que deberían estar restringidos.
- No testear lógica de autorización. Los tests de unidad verifican lógica de negocio pero raramente testean que
viewerno pueda llamarDELETE /users/1.