Permisos

Autorización Security Notes Jan 6, 2025 JAVASCRIPT

Definition

Cuando inicias sesión en Gmail, puedes leer y enviar emails. Pero no puedes eliminar los emails de otra persona ni cambiar los servidores de correo de Google. Esas fronteras - lo que puedes y no puedes hacer - son permisos. Son los derechos de acceso específicos que definen exactamente qué acciones puede realizar un usuario sobre qué recursos. “Puede leer sus propios emails” es un permiso. “Puede modificar todas las cuentas de usuario” es otro permiso. Son los átomos de la autorización, las unidades más pequeñas de “permitido hacer X.”

Los permisos típicamente se expresan como pares acción-recurso: “read:documents,” “write:users,” “delete:comments.” Este formato los hace precisos y componibles. Un editor de contenido podría tener “read:articles,” “write:articles,” y “publish:articles.” Un visor solo tiene “read:articles.” Un admin podría tener “admin:*” - un comodín que otorga todo. Combinando estos permisos atómicos, construyes exactamente los niveles de acceso que tu sistema necesita.

El poder de los permisos es su granularidad. En lugar de solo “admin” o “no admin,” puedes expresar acceso matizado como “puede editar documentos que creó,” “puede ver informes de su departamento,” o “puede aprobar gastos menores de 1000€.” Los sistemas modernos a menudo añaden permisos condicionales: “puede eliminar comentarios que escribió dentro de 24 horas.” Esta granularidad te permite implementar el principio de menor privilegio - dar a los usuarios exactamente los permisos que necesitan y nada más. Es la diferencia entre dar a alguien una llave maestra de tu edificio versus darles una llave específica para la única habitación que realmente necesitan entrar.

Example

Los permisos moldean el control de acceso en cada sistema que usas:

Acceso a Repositorios de GitHub: Cuando te añaden a un repo de GitHub, te asignan permisos como “read” (clonar, ver código), “triage” (gestionar issues), “write” (push código), “maintain” (gestionar configuración), o “admin” (todo incluyendo eliminar el repo). Cada nivel de permiso es realmente un paquete de pares acción-recurso específicos.

Compartir en Google Workspace: Cuando compartes un Google Doc, eliges “Visor” (lectura), “Comentador” (lectura + comentar), o “Editor” (lectura + escritura). Pero detrás de escena, Google tiene permisos granulares: puede copiar, puede descargar, puede imprimir, puede compartir con otros. Los admins empresariales pueden modificar qué permisos vienen con cada rol.

Políticas AWS IAM: Los permisos de AWS son increíblemente granulares: “s3:GetObject” te permite leer de S3, “s3:PutObject” te permite escribir, “s3:DeleteObject” te permite eliminar. Puedes combinarlos con condiciones: solo permitir “s3:GetObject” en archivos de un bucket específico, solo durante horas laborales, solo desde direcciones IP específicas.

Permisos de Apps Móviles: Cuando una app pide permiso de “Cámara” en tu teléfono, está solicitando “capture:photos” y “capture:video.” El permiso de “Ubicación” incluye “access:fine-location” y “access:coarse-location.” Cada permiso es una capacidad específica que la app tiene o no tiene.

Acceso a Base de Datos: Las sentencias GRANT de PostgreSQL son permisos: “GRANT SELECT ON users TO app_user” da permiso de lectura. “GRANT INSERT, UPDATE ON orders TO order_service” da permiso de escritura. “GRANT ALL ON schema TO admin” es el comodín.

Analogía

El Sistema de Tarjeta Llave del Hotel: Las llaves de hotel modernas no son solo llaves de habitación - son tarjetas de permiso. Tu tarjeta podría desbloquear tu habitación (read:room123), el área de piscina (access:pool), y el gimnasio (access:gym), pero no las áreas de personal ni las habitaciones de otros huéspedes. Recepción programa exactamente qué permisos tiene tu tarjeta.

El Llavero Lleno de Llaves: Piensa en los permisos como llaves individuales en un llavero. Una llave abre la puerta principal (access:building), otra abre tu oficina (access:office-42), una tercera abre el armario de suministros (read:supplies). Un conserje tiene llaves diferentes a las tuyas. El administrador del edificio tiene una llave maestra que abre todo.

El Acceso a la Cocina del Restaurante: En un restaurante, los camareros pueden acceder al comedor y sistema de TPV. Los cocineros pueden acceder a la cocina pero no a la caja fuerte. Los gerentes pueden acceder a la oficina y caja fuerte. El dueño tiene acceso a todo. Cada rol lleva permisos específicos sobre qué áreas y sistemas pueden usar.

La Entrada del Cine: Tu entrada es un permiso. Otorga “enter:theater-7” y “occupy:seat-J12” para una sesión específica. No otorga acceso a otras salas, la cabina de proyección, o la sesión de mañana. Es un permiso muy específico, delimitado, y de tiempo limitado.

Code Example


// Permission system implementation
class PermissionManager {
  constructor() {
    // Permission format: "action:resource:condition"
    this.permissions = new Map()
  }

  // Check if user has permission
  hasPermission(user, requiredPermission) {
    const userPermissions = this.getUserPermissions(user)

    // Direct match
    if (userPermissions.has(requiredPermission)) {
      return true
    }

    // Wildcard match
    const [action, resource, condition] = requiredPermission.split(':')

    // Check wildcards: admin:*, *:documents, read:*
    if (
      userPermissions.has('*:*') ||
      userPermissions.has(`${action}:*`) ||
      userPermissions.has(`*:${resource}`)
    ) {
      return true
    }

    return false
  }

  getUserPermissions(user) {
    const permissions = new Set()

    // Add direct user permissions
    if (user.permissions) {
      user.permissions.forEach(p => permissions.add(p))
    }

    // Add role-based permissions
    if (user.roles) {
      user.roles.forEach(role => {
        const rolePerms = this.getRolePermissions(role)
        rolePerms.forEach(p => permissions.add(p))
      })
    }

    return permissions
  }

  getRolePermissions(role) {
    const rolePermissionMap = {
      'admin': ['*:*'],
      'editor': [
        'read:documents',
        'write:documents',
        'read:comments',
        'write:comments'
      ],
      'viewer': [
        'read:documents',
        'read:comments'
      ],
      'moderator': [
        'read:*',
        'delete:comments',
        'update:comments'
      ]
    }

    return rolePermissionMap[role] || []
  }

  // Check resource-specific permission with ownership
  hasResourcePermission(user, action, resource) {
    const permission = `${action}:${resource.type}`

    // Check basic permission
    if (!this.hasPermission(user, permission)) {
      // Check if has "own" variant
      const ownPermission = `${action}:own-${resource.type}`
      if (this.hasPermission(user, ownPermission)) {
        // Verify ownership
        return resource.ownerId === user.id
      }
      return false
    }

    return true
  }
}

// Permission-based middleware
const permissionManager = new PermissionManager()

function requirePermission(permission) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Unauthorized' })
    }

    if (!permissionManager.hasPermission(req.user, permission)) {
      return res.status(403).json({
        error: 'Forbidden',
        message: `Missing required permission: ${permission}`
      })
    }

    next()
  }
}

// Usage in routes
app.get('/api/documents',
  requirePermission('read:documents'),
  async (req, res) => {
    const documents = await db.documents.findAll()
    res.json(documents)
  }
)

app.delete('/api/documents/:id',
  async (req, res) => {
    const document = await db.documents.findById(req.params.id)

    if (!document) {
      return res.status(404).json({ error: 'Not found' })
    }

    // Check resource-specific permission
    if (!permissionManager.hasResourcePermission(
      req.user,
      'delete',
      document
    )) {
      return res.status(403).json({
        error: 'Forbidden',
        message: 'Cannot delete this document'
      })
    }

    await db.documents.delete(req.params.id)
    res.status(204).send()
  }
)

// Permission hierarchy
class PermissionHierarchy {
  constructor() {
    this.hierarchy = {
      'admin': ['editor', 'moderator', 'viewer'],
      'editor': ['viewer'],
      'moderator': ['viewer']
    }
  }

  // Check if role A implies role B
  implies(roleA, roleB) {
    if (roleA === roleB) return true

    const implied = this.hierarchy[roleA] || []
    if (implied.includes(roleB)) return true

    // Recursive check
    return implied.some(role => this.implies(role, roleB))
  }

  hasPermissionViaHierarchy(user, requiredPermission) {
    const permManager = new PermissionManager()

    // Check direct permissions
    if (permManager.hasPermission(user, requiredPermission)) {
      return true
    }

    // Check via role hierarchy
    for (const userRole of user.roles || []) {
      for (const [role, perms] of Object.entries(permManager.rolePermissionMap)) {
        if (this.implies(userRole, role)) {
          if (perms.includes(requiredPermission)) {
            return true
          }
        }
      }
    }

    return false
  }
}

// Conditional permissions
class ConditionalPermission {
  constructor(permission, condition) {
    this.permission = permission
    this.condition = condition
  }

  evaluate(user, resource, context) {
    return this.condition(user, resource, context)
  }
}

const conditionalPerms = [
  new ConditionalPermission(
    'delete:documents',
    (user, doc, ctx) => {
      // Can delete if owner OR admin OR created less than 1 hour ago
      return (
        doc.ownerId === user.id ||
        user.roles.includes('admin') ||
        (Date.now() - doc.createdAt) < 3600000
      )
    }
  )
]

Notas de Seguridad

SECURITY NOTES

CRÍTICO: Los sistemas de permisos son centrales para la seguridad. Vulnerabilidades: (1) Bypass de permisos - …

Configuración:

  • verificar permisos en algunos lugares pero no en otros, (2) Abuso de comodines - comodines excesivamente permisivos como “:” otorgados demasiado libremente, (3) Escalada de permisos - usuarios capaces de otorgarse permisos a sí mismos, (4) Permisos obsoletos - no revocados cuando el rol cambia, (5) Permisos implícitos - permisos por defecto peligrosos, (6) Problemas de sensibilidad a mayúsculas - “READ:docs” vs “read:docs” tratados de manera diferente.
  • Mejores prácticas: Usar denegar por defecto (sin permisos a menos que se otorguen explícitamente), validar permisos en cada solicitud (nunca solo en login), implementar menor privilegio (permisos mínimos necesarios), evitar permisos negativos (son confusos y propensos a errores), usar namespace claro para permisos (org:resource:action), auditar cambios de permisos, implementar expiración de permisos para acceso temporal, nunca confiar en verificaciones de permisos del lado del cliente, validar cadenas de permisos (prevenir inyección), cachear permisos cuidadosamente (invalidar en cambio de rol), registrar todas las denegaciones de permisos para monitoreo de seguridad, probar límites de permisos exhaustivamente, implementar herencia de permisos cuidadosamente, separar propiedad de recursos de permisos.

Mejores Prácticas:

    • verificar permisos en algunos lugares pero no en otros, (2) Abuso de comodines - comodines excesivamente permisivos como “:” otorgados demasiado libremente, (3) Escalada de permisos - usuarios capaces de otorgarse permisos a sí mismos, (4) Permisos obsoletos - no revocados cuando el rol cambia, (5) Permisos implícitos - permisos por defecto peligrosos, (6) Problemas de sensibilidad a mayúsculas - “READ:docs” vs “read:docs” tratados de manera diferente.
    • Mejores prácticas: Usar denegar por defecto (sin permisos a menos que se otorguen explícitamente), validar permisos en cada solicitud (nunca solo en login), implementar menor privilegio (permisos mínimos necesarios), evitar permisos negativos (son confusos y propensos a errores), usar namespace claro para permisos (org:resource:action), auditar cambios de permisos, implementar expiración de permisos para acceso temporal, nunca confiar en verificaciones de permisos del lado del cliente, validar cadenas de permisos (prevenir inyección), cachear permisos cuidadosamente (invalidar en cambio de rol), registrar todas las denegaciones de permisos para monitoreo de seguridad, probar límites de permisos exhaustivamente, implementar herencia de permisos cuidadosamente, separar propiedad de recursos de permisos.