Controlar el acceso a campos específicos

En esta página se desarrollan los conceptos de Estructurar reglas de seguridad y Escribir condiciones para reglas de seguridad para explicar cómo puedes usar las reglas de seguridad de Firestore para crear reglas que permitan a los clientes realizar operaciones en algunos campos de un documento, pero no en otros.

Puede que haya ocasiones en las que quieras controlar los cambios de un documento no a nivel de documento, sino a nivel de campo.

Por ejemplo, puede permitir que un cliente cree o modifique un documento, pero no que edite determinados campos de ese documento. También puede querer que cualquier documento que cree un cliente contenga siempre un determinado conjunto de campos. En esta guía se explica cómo puedes llevar a cabo algunas de estas tareas con las reglas de seguridad de Firestore.

Permitir el acceso de lectura solo a campos específicos

Las lecturas en Firestore se realizan a nivel de documento. O bien recuperas el documento completo o no recuperas nada. No hay forma de recuperar un documento parcial. Es imposible evitar que los usuarios lean campos específicos de un documento solo con reglas de seguridad.

Si hay determinados campos de un documento que quieres ocultar a algunos usuarios, lo mejor es que los incluyas en un documento aparte. Por ejemplo, puedes crear un documento en una privatesubcolección de la siguiente manera:

/employees/{emp_id}

  name: "Alice Hamilton",
  department: 461,
  start_date: <timestamp>

/employees/{emp_id}/private/finances

    salary: 80000,
    bonus_mult: 1.25,
    perf_review: 4.2

Después, puedes añadir reglas de seguridad que tengan diferentes niveles de acceso para las dos colecciones. En este ejemplo, usamos reivindicaciones de autenticación personalizadas para indicar que solo los usuarios con la reivindicación de autenticación personalizada role igual a Finance pueden ver la información financiera de un empleado.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow any logged in user to view the public employee data
    match /employees/{emp_id} {
      allow read: if request.resource.auth != null
      // Allow only users with the custom auth claim of "Finance" to view
      // the employee's financial data
      match /private/finances {
        allow read: if request.resource.auth &&
          request.resource.auth.token.role == 'Finance'
      }
    }
  }
}

Restringir campos al crear documentos

Firestore no utiliza esquemas, lo que significa que no hay restricciones a nivel de base de datos sobre los campos que puede contener un documento. Aunque esta flexibilidad puede facilitar el desarrollo, habrá ocasiones en las que quieras asegurarte de que los clientes solo puedan crear documentos que contengan campos específicos o que no contengan otros campos.

Para crear estas reglas, examine el método keys del objeto request.resource.data. Esta es una lista de todos los campos que el cliente intenta escribir en este nuevo documento. Si combinas este conjunto de campos con funciones como hasOnly() o hasAny(), puedes añadir lógica que restrinja los tipos de documentos que un usuario puede añadir a Firestore.

Requerir campos específicos en documentos nuevos

Supongamos que quieres asegurarte de que todos los documentos creados en una colección restaurant contengan al menos los campos name, location y city. Para ello, puedes llamar a hasAll() en la lista de claves del nuevo documento.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document contains a name
    // location, and city field
    match /restaurant/{restId} {
      allow create: if request.resource.data.keys().hasAll(['name', 'location', 'city']);
    }
  }
}

De esta forma, los restaurantes se pueden crear con otros campos, pero se asegura de que todos los documentos creados por un cliente contengan al menos estos tres campos.

Prohibir campos específicos en documentos nuevos

Del mismo modo, puedes impedir que los clientes creen documentos que contengan campos específicos mediante hasAny() en una lista de campos prohibidos. Este método devuelve el valor "true" si un documento contiene alguno de estos campos, por lo que probablemente quieras negar el resultado para prohibir determinados campos.

Por ejemplo, en el siguiente ejemplo, los clientes no pueden crear un documento que contenga un campo average_score o rating_count, ya que estos campos se añadirán mediante una llamada al servidor más adelante.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document does *not*
    // contain an average_score or rating_count field.
    match /restaurant/{restId} {
      allow create: if (!request.resource.data.keys().hasAny(
        ['average_score', 'rating_count']));
    }
  }
}

Crear una lista de permitidos de campos para documentos nuevos

En lugar de prohibir determinados campos en los documentos nuevos, puede crear una lista con los campos que se permiten explícitamente en los documentos nuevos. Después, puedes usar la función hasOnly() para asegurarte de que los nuevos documentos que crees solo contengan estos campos (o un subconjunto de ellos) y ningún otro.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document doesn't contain
    // any fields besides the ones listed below.
    match /restaurant/{restId} {
      allow create: if (request.resource.data.keys().hasOnly(
        ['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

Combinar campos obligatorios y opcionales

Puedes combinar las operaciones hasAll y hasOnly en tus reglas de seguridad para requerir algunos campos y permitir otros. Por ejemplo, en este ejemplo se requiere que todos los documentos nuevos contengan los campos name, location y city y, de forma opcional, los campos address, hours y cuisine.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document has a name,
    // location, and city field, and optionally address, hours, or cuisine field
    match /restaurant/{restId} {
      allow create: if (request.resource.data.keys().hasAll(['name', 'location', 'city'])) &&
       (request.resource.data.keys().hasOnly(
           ['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

En una situación real, puede que quieras mover esta lógica a una función auxiliar para evitar duplicar el código y combinar más fácilmente los campos opcionales y obligatorios en una sola lista, como se muestra a continuación:

service cloud.firestore {
  match /databases/{database}/documents {
    function verifyFields(required, optional) {
      let allAllowedFields = required.concat(optional);
      return request.resource.data.keys().hasAll(required) &&
        request.resource.data.keys().hasOnly(allAllowedFields);
    }
    match /restaurant/{restId} {
      allow create: if verifyFields(['name', 'location', 'city'],
        ['address', 'hours', 'cuisine']);
    }
  }
}

Restringir campos en la actualización

Una práctica de seguridad habitual es permitir que los clientes editen algunos campos, pero no otros. No puedes hacerlo solo consultando la lista request.resource.data.keys()descrita en la sección anterior, ya que esta lista representa el documento completo tal como se vería después de la actualización y, por lo tanto, incluiría campos que el cliente no ha cambiado.

Sin embargo, si usaras la función diff(), podrías comparar request.resource.data con el objeto resource.data, que representa el documento en la base de datos antes de la actualización. De esta forma, se crea un objeto mapDiff, que contiene todos los cambios entre dos mapas diferentes.

Si llamas al método affectedKeys() en este mapDiff, puedes obtener un conjunto de campos que se hayan modificado en una edición. Después, puedes usar funciones como hasOnly() o hasAny() para asegurarte de que este conjunto contiene (o no) determinados elementos.

Impedir que se modifiquen algunos campos

Si usas el método hasAny() en el conjunto generado por affectedKeys() y, a continuación, inviertes el resultado, puedes rechazar cualquier solicitud de cliente que intente cambiar campos que no quieras que se modifiquen.

Por ejemplo, puede que quieras permitir que los clientes actualicen la información sobre un restaurante, pero no que cambien su puntuación media ni el número de reseñas.

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
      // Allow the client to update a document only if that document doesn't
      // change the average_score or rating_count fields
      allow update: if (!request.resource.data.diff(resource.data).affectedKeys()
        .hasAny(['average_score', 'rating_count']));
    }
  }
}

Permitir que solo se cambien determinados campos

En lugar de especificar los campos que no quieres que se modifiquen, también puedes usar la función hasOnly() para especificar una lista de campos que sí quieres que se modifiquen. Por lo general, se considera que es más seguro, ya que las escrituras en los campos de los documentos nuevos no se permiten de forma predeterminada hasta que las permitas explícitamente en tus reglas de seguridad.

Por ejemplo, en lugar de no permitir los campos average_score y rating_count, puedes crear reglas de seguridad que permitan a los clientes cambiar solo los campos name, location, city, address, hours y cuisine.

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
    // Allow a client to update only these 6 fields in a document
      allow update: if (request.resource.data.diff(resource.data).affectedKeys()
        .hasOnly(['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

Esto significa que, si en alguna iteración futura de tu aplicación, los documentos de restaurante incluyen un campo telephone, los intentos de editar ese campo fallarán hasta que vuelvas y añadas ese campo a la lista hasOnly() en tus reglas de seguridad.

Aplicar tipos de campo

Otro efecto de que Firestore no tenga esquema es que no se aplica ningún tipo de restricción a nivel de base de datos sobre los tipos de datos que se pueden almacenar en campos específicos. Sin embargo, puedes aplicar esta medida en las reglas de seguridad con el operador is.

Por ejemplo, la siguiente regla de seguridad obliga a que el campo score de una reseña sea un número entero, que los campos headline, content y author_name sean cadenas y que review_date sea una marca de tiempo.

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
      // Restaurant rules go here...
      match /review/{reviewId} {
        allow create: if (request.resource.data.score is int &&
          request.resource.data.headline is string &&
          request.resource.data.content is string &&
          request.resource.data.author_name is string &&
          request.resource.data.review_date is timestamp
        );
      }
    }
  }
}

Los tipos de datos válidos para el operador is son bool, bytes, float, int, list, latlng, number, path, map, string y timestamp. El operador is también admite los tipos de datos constraint, duration, set y map_diff, pero, como los genera el propio lenguaje de las reglas de seguridad y no los clientes, rara vez se usan en la mayoría de las aplicaciones prácticas.

Los tipos de datos list y map no admiten genéricos ni argumentos de tipo. Es decir, puedes usar reglas de seguridad para obligar a que un campo determinado contenga una lista o un mapa, pero no puedes obligar a que un campo contenga una lista de todos los números enteros o todas las cadenas.

Del mismo modo, puedes usar reglas de seguridad para aplicar valores de tipo a entradas específicas de una lista o un mapa (mediante la notación de corchetes o los nombres de las claves, respectivamente), pero no hay ningún método abreviado para aplicar los tipos de datos de todos los miembros de un mapa o una lista a la vez.

Por ejemplo, las siguientes reglas aseguran que un campo tags de un documento contenga una lista y que la primera entrada sea una cadena. También se asegura de que el campo product contenga un mapa que, a su vez, contenga un nombre de producto que sea una cadena y una cantidad que sea un número entero.

service cloud.firestore {
  match /databases/{database}/documents {
  match /orders/{orderId} {
    allow create: if request.resource.data.tags is list &&
      request.resource.data.tags[0] is string &&
      request.resource.data.product is map &&
      request.resource.data.product.name is string &&
      request.resource.data.product.quantity is int
      }
    }
  }
}

Los tipos de campos deben aplicarse al crear y actualizar un documento. Por lo tanto, te recomendamos que crees una función auxiliar que puedas llamar en las secciones de creación y actualización de tus reglas de seguridad.

service cloud.firestore {
  match /databases/{database}/documents {

  function reviewFieldsAreValidTypes(docData) {
     return docData.score is int &&
          docData.headline is string &&
          docData.content is string &&
          docData.author_name is string &&
          docData.review_date is timestamp;
  }

   match /restaurant/{restId} {
      // Restaurant rules go here...
      match /review/{reviewId} {
        allow create: if reviewFieldsAreValidTypes(request.resource.data) &&
          // Other rules may go here
        allow update: if reviewFieldsAreValidTypes(request.resource.data) &&
          // Other rules may go here
      }
    }
  }
}

Aplicar tipos a campos opcionales

Es importante recordar que, si llamas a request.resource.data.foo en un documento en el que no existe foo, se producirá un error. Por lo tanto, cualquier regla de seguridad que haga esa llamada denegará la solicitud. Puedes gestionar esta situación con el método get en request.resource.data. El método get te permite proporcionar un argumento predeterminado para el campo que estás obteniendo de un mapa si ese campo no existe.

Por ejemplo, si los documentos de revisión también contienen un campo photo_url opcional y un campo tags opcional que quieres verificar que sean cadenas y listas respectivamente, puedes hacerlo reescribiendo la función reviewFieldsAreValidTypes de la siguiente manera:

  function reviewFieldsAreValidTypes(docData) {
     return docData.score is int &&
          docData.headline is string &&
          docData.content is string &&
          docData.author_name is string &&
          docData.review_date is timestamp &&
          docData.get('photo_url', '') is string &&
          docData.get('tags', []) is list;
  }

De esta forma, se rechazan los documentos en los que existe tags, pero no es una lista, y se permiten los documentos que no contienen el campo tags (ni photo_url).

Nunca se permiten las escrituras parciales

Una última nota sobre las reglas de seguridad de Firestore es que permiten que el cliente haga un cambio en un documento o rechazan toda la edición. No puedes crear reglas de seguridad que acepten escrituras en algunos campos de tu documento y rechacen otros en la misma operación.