Autorización de Grano Fino

Autorización Security Notes Jan 6, 2025 JAVASCRIPT

Definition

Modelo de autorización que toma decisiones de acceso a nivel detallado y granular - controlando acceso no solo a recursos, sino a campos específicos, operaciones o datos basados en condiciones complejas. Va más allá del “puede el usuario acceder a esta API” de grano grueso a “puede este usuario leer este campo específico en este registro específico ahora mismo”.

Example

Usuario puede ver registros de clientes pero solo ver números de tarjeta de crédito enmascarados a menos que tengan rol “pci-compliance” Y la solicitud sea desde red interna Y el log de auditoría esté habilitado. Nivel de campo: mostrar campo “salario” solo a departamento de RRHH o al propio registro del empleado.

Analogía

Como un hospital donde doctores pueden ver todos los registros de pacientes, enfermeras ven la mayoría pero no notas psiquiátricas, personal de facturación solo ve información de seguro, y el paciente puede ver todo sobre sí mismo pero nada sobre otros. El acceso se adapta precisamente al rol, contexto y el ítem de datos específico.

Code Example


// Fine-grained authorization example
class FinegrainedAuthz {
  constructor() {
    this.policies = new Map()
  }

  // Field-level authorization
  canReadField(user, resource, fieldName) {
    const field = resource[fieldName]

    // Email: visible to owner or admin
    if (fieldName === 'email') {
      return user.id === resource.userId || user.roles.includes('admin')
    }

    // SSN: only for compliance officers from internal network
    if (fieldName === 'ssn') {
      return (
        user.roles.includes('compliance') &&
        user.network === 'internal' &&
        user.mfaVerified === true
      )
    }

    // Salary: HR or own record
    if (fieldName === 'salary') {
      return (
        user.department === 'hr' ||
        user.id === resource.userId
      )
    }

    // Default: public fields
    return true
  }

  // Operation-level authorization
  canPerformOperation(user, resource, operation) {
    // Delete: only owner or admin, not on locked resources
    if (operation === 'delete') {
      return (
        (user.id === resource.userId || user.roles.includes('admin')) &&
        !resource.isLocked
      )
    }

    // Approve: only managers of same department
    if (operation === 'approve') {
      return (
        user.roles.includes('manager') &&
        user.department === resource.department
      )
    }

    return false
  }

  // Filter response based on permissions
  filterFields(user, resource) {
    const filtered = {}

    for (const [key, value] of Object.entries(resource)) {
      if (this.canReadField(user, resource, key)) {
        filtered[key] = value
      } else {
        // Return masked or null for unauthorized fields
        filtered[key] = this.getMaskedValue(key, value)
      }
    }

    return filtered
  }

  getMaskedValue(fieldName, value) {
    if (fieldName === 'ssn') return '***-**-****'
    if (fieldName === 'email') return '***@***.***'
    if (fieldName === 'salary') return null
    return '[REDACTED]'
  }
}

// API endpoint with fine-grained authorization
app.get('/api/employees/:id', async (req, res) => {
  const authz = new FinegrainedAuthz()
  const user = req.user
  const employee = await db.employees.findById(req.params.id)

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

  // Apply fine-grained filtering
  const filtered = authz.filterFields(user, employee)

  // Log what was filtered for audit
  const redactedFields = Object.keys(employee).filter(
    key => !(key in filtered) || filtered[key] === null
  )

  if (redactedFields.length > 0) {
    await auditLog.log({
      action: 'field_redaction',
      user: user.id,
      resource: employee.id,
      redactedFields
    })
  }

  res.json(filtered)
})

// Conditional field exposure in GraphQL
const resolvers = {
  Employee: {
    salary: (parent, args, context) => {
      const user = context.user
      const authz = new FinegrainedAuthz()

      if (authz.canReadField(user, parent, 'salary')) {
        return parent.salary
      }

      throw new ForbiddenError('Not authorized to view salary')
    }
  }
}

Notas de Seguridad

SECURITY NOTES

CRÍTICO: La autorización de grano fino es compleja y propensa a errores. Vulnerabilidades comunes: (1) Bypass de autorización vía agregación - …

Configuración:

  • usuario no puede ver salarios individuales pero puede ver promedio departamental que los revela, (2) Aplicación inconsistente - verificar en lectura pero olvidar en actualización/eliminación, (3) Filtrado del lado del cliente - nunca filtrar campos sensibles en frontend, siempre del lado del servidor, (4) Fugas de canal lateral - mensajes de error o diferencias de tiempo revelan datos no autorizados, (5) Envenenamiento de caché - respuestas en caché servidas a usuarios no autorizados, (6) Ataques por lotes - combinar múltiples solicitudes para inferir datos protegidos, (7) Degradación de rendimiento - verificaciones complejas en cada campo pueden matar el rendimiento.
  • Mejores prácticas: Centralizar lógica de autorización (no dispersar verificaciones por todas partes), probar exhaustivamente con matrices de permisos, implementar a nivel de consulta de base de datos cuando sea posible (seguridad a nivel de fila), auditar todas las decisiones de autorización, usar denegar por defecto (permiso explícito requerido), cachear decisiones de autorización cuidadosamente (TTL corto, específico por usuario), monitorear patrones de acceso inusuales, implementar limitación de tasa, fallar cerrado (denegar acceso en error), nunca exponer existencia de recursos no autorizados.

Mejores Prácticas:

    • usuario no puede ver salarios individuales pero puede ver promedio departamental que los revela, (2) Aplicación inconsistente - verificar en lectura pero olvidar en actualización/eliminación, (3) Filtrado del lado del cliente - nunca filtrar campos sensibles en frontend, siempre del lado del servidor, (4) Fugas de canal lateral - mensajes de error o diferencias de tiempo revelan datos no autorizados, (5) Envenenamiento de caché - respuestas en caché servidas a usuarios no autorizados, (6) Ataques por lotes - combinar múltiples solicitudes para inferir datos protegidos, (7) Degradación de rendimiento - verificaciones complejas en cada campo pueden matar el rendimiento.
    • Mejores prácticas: Centralizar lógica de autorización (no dispersar verificaciones por todas partes), probar exhaustivamente con matrices de permisos, implementar a nivel de consulta de base de datos cuando sea posible (seguridad a nivel de fila), auditar todas las decisiones de autorización, usar denegar por defecto (permiso explícito requerido), cachear decisiones de autorización cuidadosamente (TTL corto, específico por usuario), monitorear patrones de acceso inusuales, implementar limitación de tasa, fallar cerrado (denegar acceso en error), nunca exponer existencia de recursos no autorizados.