Controla el acceso a campos específicos

En esta página, se amplían los conceptos mencionados en los artículos sobre cómo estructurar reglas de seguridad y cómo escribir condiciones para las reglas de seguridad. Se explica cómo puedes usar las reglas de seguridad de Firestore con el fin de crear reglas que les permitan a los clientes realizar operaciones en algunos campos de un documento, pero no en otros.

Puede haber ocasiones en las que quieras controlar los cambios que se realizan en un documento, pero solo a nivel de los campos.

Por ejemplo, es posible que desees permitir que un cliente cree o cambie un documento, pero no quieras permitirle editar ciertos campos del documento. También es posible que apliques que cualquier documento que un cliente cree siempre contenga un conjunto determinado de campos. En esta guía, se explica cómo realizar algunas de estas tareas con las reglas de seguridad de Firestore.

Permite el acceso de lectura solo para campos específicos

Las operaciones de lectura de Firestore se realizan a nivel de los documentos. Puedes recuperar el documento completo o no recuperas nada. No hay forma de recuperar un documento parcial. Es imposible usar solo las reglas de seguridad para evitar que los usuarios lean campos específicos de un documento.

Si hay ciertos campos de un documento que quieres mantener ocultos a algunos usuarios, la mejor manera de hacerlo es colocarlos en otro documento. Por ejemplo, podrías crear un documento en una subcolección private 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

Luego, puedes agregar reglas de seguridad que tengan diferentes niveles de acceso para ambas colecciones. En este ejemplo, usamos reclamaciones de autenticación personalizadas para indicar que solo los usuarios que tengan una reclamación de autenticación personalizada role igual a Finance puedan visualizar 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'
      }
    }
  }
}

Restringe campos durante la creación de documentos

Firestore no usa esquemas, por lo que no existen restricciones a nivel de la base de datos para los campos que contiene un documento. Si bien 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 no contengan otros campos.

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

Exige campos específicos en documentos nuevos

Supongamos que quieres asegurarte de que todos los documentos creados en una colección restaurant contengan los campos name, location y city como mínimo. Para ello, llama a hasAll() en la lista de claves del documento nuevo.

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']);
    }
  }
}

Esto también permite crear restaurantes con otros campos, pero garantiza que todos los documentos que cree un cliente contengan al menos estos tres.

Prohíbe campos específicos en documentos nuevos

Del mismo modo, puedes evitar que los clientes creen documentos que contengan campos específicos. Para ello, usa hasAny() en una lista de campos prohibidos. Este método se evalúa como verdadero si un documento contiene cualquiera de estos campos, por lo que te recomendamos que niegues el resultado a fin de prohibir ciertos campos.

En el siguiente ejemplo, los clientes no pueden crear un documento que contenga los campos average_score o rating_count, ya que una llamada al servidor los agregará 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']));
    }
  }
}

Crea una lista de campos permitidos para los documentos nuevos

En vez de prohibir determinados campos en los documentos nuevos, te recomendamos que crees una lista de campos permitidos explícitamente. Luego, puedes usar la función hasOnly() para asegurarte de que cualquier documento nuevo que se cree contenga solo 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']));
    }
  }
}

Combina campos obligatorios y opcionales

Puedes combinar las operaciones hasAll y hasOnly en tus reglas de seguridad para exigir algunos campos y permitir otros. En este ejemplo, se exige que todos los documentos nuevos contengan los campos name, location y city, y, de forma opcional, se permiten 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, te recomendamos que traslades esta lógica a una función auxiliar para evitar la duplicación de código y combinar con mayor facilidad los campos opcionales y obligatorios en una sola lista, de la siguiente manera:

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']);
    }
  }
}

Restringe campos durante la actualización

Una práctica de seguridad común es permitir que los clientes editen solo algunos campos y no otros. No puedes hacerlo si solamente observas la lista request.resource.data.keys() descrita en la sección anterior, ya que esta representa el estado que tendrá el documento completo después de la actualización y, por lo tanto, incluirá campos que el cliente no modificó.

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

Con una llamada al método affectedKeys() en este mapDiff, puedes crear un conjunto de campos que se modificaron en una edición. Luego, puedes usar funciones como hasOnly() o hasAny() para asegurarte de que este conjunto contenga (o no) determinados elementos.

Evita que se modifiquen algunos campos

Si usas el método hasAny() en el conjunto generado por affectedKeys() y, luego, niegas el resultado, puedes rechazar cualquier solicitud del cliente que intente cambiar los campos que no quieras que se modifiquen.

Por ejemplo, tal vez quieras permitir que los clientes actualicen la información sobre un restaurante, pero no cambiar su puntuación promedio o la cantidad de opiniones.

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']));
    }
  }
}

Permite que solo se modifiquen algunos campos

En lugar de especificar los campos que no deseas cambiar, puedes usar la función hasOnly() para especificar una lista de los campos que sí quieres modificar. En general, esta práctica se considera más segura porque las operaciones de escritura realizadas en campos de documentos nuevos se rechazan de forma predeterminada hasta que se permitan explícitamente en tus reglas de seguridad.

Por ejemplo, en lugar de inhabilitar los campos average_score y rating_count, puedes crear reglas de seguridad que les permitan a los clientes solo cambiar 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 una iteración futura de tu app los documentos del restaurante incluyen un campo telephone, los intentos para editarlo fallarán hasta que regreses y agregues ese campo a la lista hasOnly() de tus reglas de seguridad.

Aplica tipos de campos

Otra consecuencia de que Firestore no utilice esquemas es que, a nivel de la base de datos, no se aplican los tipos de datos que se pueden almacenar en campos específicos. Sin embargo, puedes hacerlo en las reglas de seguridad con el operador is.

Por ejemplo, la siguiente regla de seguridad exige que el campo score de una opinión sea un número entero, que los campos headline, content y author_name sean strings 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 tipos de datos constraint, duration, set y map_diff. Sin embargo, como esos datos los genera el lenguaje de las reglas de seguridad y no los clientes, rara vez se utilizan en aplicaciones prácticas.

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

Del mismo modo, puedes usar las reglas de seguridad a fin de aplicar valores de tipo para entradas específicas de una lista o un mapa (mediante la notación con corchetes o los nombres de clave, respectivamente), pero no existe un atajo para aplicar los tipos de datos de todos los miembros de un mapa o una lista a la vez.

Por ejemplo, las siguientes reglas garantizan que un campo tags de un documento contenga una lista y que la primera entrada sea una string. También se garantiza que el campo product contenga un mapa que, a su vez, contenga un nombre de producto que sea una string 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 campo se deben aplicar cuando se crea y se actualiza un documento. Por lo tanto, es recomendable que crees una función auxiliar a la que puedas llamar en las secciones de creación y actualización de las 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
      }
    }
  }
}

Aplica tipos para los campos opcionales

Es importante recordar que, si llamas a request.resource.data.foo en un documento en el que no existe foo, se generará un error. Por lo tanto, se rechazará la solicitud de cualquier regla de seguridad que realice tal llamada. Para controlar esta situación, usa el método get en request.resource.data. El método get te permite proporcionar un argumento predeterminado para el campo que se recupera de un mapa si ese campo no existe.

Por ejemplo, si los documentos de opiniones también contienen los campos opcionales photo_url y tags, y quieres verificar que sean strings y listas, respectivamente, puedes reescribir 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;
  }

Con esto se rechazarán los documentos en los que exista tags, pero no sea una lista, y se seguirán permitiendo los documentos que no contengan un campo tags (o photo_url).

Las escrituras parciales nunca se permiten

Para finalizar, te recordamos que las reglas de seguridad de Firestore permiten que el cliente realice un cambio en un documento, o bien rechazan toda la edición. No puedes crear reglas de seguridad que acepten operaciones de escritura en algunos campos de tu documento y, al mismo tiempo, rechacen otras en la misma operación.