Authorization, ownership y policies en Express.js | Nicolás Garzón
Authorization evalúa si un actor puede ejecutar una acción sobre un recurso dentro de un contexto. Un token válido o un rol general nunca reemplazan esta decisión completa.
Texto
Copiar actor + action + resource + context → allow | denyTexto
Copiar actor: assistant-manager-42
acción: orders.cancel
recurso: order-123
contexto: branch-7, estado confirmed, horario actualLa decisión puede depender de rol, ownership, tenant, estado y reglas temporales.
Sin autorización contextual aparecen dos clases de escalación:
Horizontal: acceder a datos de otro usuario o tenant con el mismo nivel.
Vertical: ejecutar capacidades de un rol superior.
Validar solo que existe una session evita acceso anónimo, pero no estos ataques.
Texto
Copiar authentication
→ quién eres
authorization
→ qué puedes hacer aquí y ahoraUna identidad puede estar autenticada y aun así recibir 403 o 404.
Texto
Copiar cashier → orders:create, payments:collect
manager → orders:cancel, reports:readEs comprensible, pero roles demasiado amplios producen explosión o privilegios excesivos.
Texto
Copiar actor.branchId == resource.branchId
AND actor.employmentStatus == activeEs flexible, pero policies complejas son difíciles de auditar.
Comprueba relación directa:
Texto
Copiar order.customerId == actor.idNo basta en sistemas con delegación, roles o recursos compartidos.
Decide a partir de relaciones, por ejemplo usuario miembro de equipo propietario. Puede requerir un motor especializado cuando el grafo crece.
Los modelos suelen combinarse.
TypeScript
Copiar type CancelOrderPolicyInput = {
actor: Actor;
order: Order;
} ;
function canCancelOrder ( { actor, order } : CancelOrderPolicyInput) : boolean {
if ( actor. tenantId !== order. tenantId) return false ;
if ( ! actor. permissions. includes ( 'orders:cancel' ) ) return false ;
if ( actor. branchIds. includes ( order. branchId) === false ) return false ;
return order. status === 'confirmed' ;
} La policy recibe datos internos confiables, no req.body.role.
Puede exigir un permiso general para evitar trabajo innecesario.
Debe comprobar acceso contextual sobre recurso y estado reales.
Debe scopear queries por tenant o actor cuando corresponda.
RLS o constraints pueden añadir otra capa.
La duplicación selectiva es defensa, no necesariamente mala arquitectura. Las capas deben tener responsabilidades distintas.
Evita cargar un recurso ajeno y luego olvidar la policy:
SQL
Copiar SELECT *
FROM orders
WHERE id = $1
AND business_id = $2
AND branch_id = ANY ( $3 ) ; Esto reduce exposición, aunque el caso de uso todavía aplica reglas de estado.
El actor sabe que el recurso existe, pero no tiene permiso.
Oculta existencia y reduce enumeración.
Elige una política consistente por tipo de recurso. No uses 404 para esconder fallos internos.
Un rol admin no debería convertirse automáticamente en bypass universal. Define:
alcance de tenant
acciones break-glass
motivo obligatorio
auditoría
expiración temporal
exclusiones sensibles
Las herramientas de soporte necesitan más trazabilidad, no menos.
TypeScript
Copiar async function cancelOrder ( command: CancelOrderCommand) {
const order = await orders. findVisibleById ( {
orderId: command. orderId,
tenantId: command. actor. tenantId,
} ) ;
if ( ! order) throw new OrderNotFoundError ( ) ;
policies. assertCanCancel ( { actor: command. actor, order } ) ;
return transactions. run ( ( tx) => orders. cancel ( order. id, tx) ) ;
} El resource real se resuelve antes de la policy. La transición se protege dentro de la operación.
Autorizar y ejecutar no siempre son atómicos:
Texto
Copiar policy permite
→ otro proceso cambia el recurso
→ operación ejecuta sobre estado distintoRevalida precondiciones dentro de la transacción o usa update condicional:
SQL
Copiar UPDATE orders
SET status = 'cancelled'
WHERE id = $1
AND status = 'confirmed' ; Cachear memberships reduce latencia, pero revocaciones tardan en reflejarse. Para acciones sensibles:
TTL corto
invalidación
consulta actual
versionado de policy
No mezcles cache entre tenants.
Ocultar un botón mejora experiencia, pero el servidor debe denegar igualmente. El frontend es un consumidor no confiable.
Ownership simple falla. Modela memberships o grants.
Tokens o caches pueden quedar obsoletos.
Decide si utiliza permisos del momento de creación o revalida al ejecutar.
Autoriza cada recurso o limita la query al scope permitido; un permiso global no implica acceso a todos los IDs enviados.
Necesita expiración y auditoría.
Roles enviados por el cliente.
Middleware como única autorización.
Query sin tenant scope.
Admin global sin auditoría.
Policy con efectos secundarios.
Deny por defecto ausente.
Cache de permisos sin invalidación.
Confundir no encontrar con fallo de dependencia.
Actor Recurso Acción Esperado owner propio leer allow owner ajeno leer deny manager misma branch cancelar allow manager otra branch cancelar deny admin tenant A tenant B leer deny
Incluye concurrencia y cambios de permisos.
RBAC es simple, ABAC flexible y ReBAC expresivo, pero cada nivel aumenta complejidad. Empieza con políticas explícitas y evoluciona cuando el modelo real lo exige.
Authorization es contextual.
Authenticated no significa authorized.
Router, caso de uso, query y base pueden colaborar.
Tenant y ownership deben derivarse de estado confiable.
La decisión puede cambiar entre check y write; protege la operación atómicamente.
¿Qué diferencia existe entre escalación horizontal y vertical?
¿Por qué una policy debe recibir el recurso real?
¿Cuándo 404 puede ser mejor que 403?
¿Qué riesgo tiene cachear permisos?
¿Por qué ocultar un botón no protege?
Ver respuestas
Horizontal cruza recursos de igual nivel; vertical obtiene privilegios superiores.
Porque ownership, tenant y estado dependen del recurso actual.
Cuando revelar existencia facilitaría enumeración.
Revocaciones y cambios pueden tardar en aplicarse.
El cliente puede llamar directamente al endpoint.
Multi-tenancy y aislamiento de requests aplica autorización y scope a todos los recursos compartidos de una plataforma SaaS.