Controlar el acceso a campos específicos

Esta página se basa en los conceptos de Estructuración de reglas de seguridad y Escritura de condiciones para reglas de seguridad para explicar cómo puedes usar las reglas de seguridad de Cloud Firestore para crear reglas que permitan a los clientes realizar operaciones en algunos campos de un documento pero no en otros.

Puede haber ocasiones en las que desee controlar los cambios en un documento no a nivel de documento sino a nivel de campo.

Por ejemplo, es posible que desee permitir que un cliente cree o cambie un documento, pero no permitirle editar ciertos campos en ese documento. O tal vez desee imponer que cualquier documento que un cliente cree siempre contenga un determinado conjunto de campos. Esta guía cubre cómo puede realizar algunas de estas tareas utilizando las reglas de seguridad de Cloud Firestore.

Permitir acceso de lectura solo para campos específicos

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

Si hay ciertos campos dentro de un documento que desea mantener ocultos para algunos usuarios, la mejor manera sería colocarlos en un documento separado. Por ejemplo, podrías considerar crear un documento en una subcolección private como esta:

/empleados/{emp_id}

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

/empleados/{emp_id}/privado/finanzas

    salary: 80000,
    bonus_mult: 1.25,
    perf_review: 4.2

Luego puede agregar reglas de seguridad que tengan diferentes niveles de acceso para las dos colecciones. En este ejemplo, utilizamos notificaciones de autenticación personalizadas para indicar que solo los usuarios con la role de notificación de autenticación personalizada equivalente 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 en la creación de documentos

Cloud Firestore no tiene esquemas, lo que significa que no hay restricciones a nivel de base de datos sobre los campos que contiene un documento. Si bien esta flexibilidad puede facilitar el desarrollo, habrá ocasiones en las que querrá asegurarse de que los clientes solo puedan crear documentos que contengan campos específicos o que no contengan otros campos.

Puede crear estas reglas examinando 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. Al combinar este conjunto de campos con funciones como hasOnly() o hasAny() , puedes agregar una lógica que restrinja los tipos de documentos que un usuario puede agregar a Cloud Firestore.

Requerir campos específicos en documentos nuevos

Supongamos que desea asegurarse de que todos los documentos creados en la colección de un restaurant contengan al menos un campo name , location y city . Puede hacerlo llamando 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']);
    }
  }
}

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

Prohibir campos específicos en documentos nuevos

De manera similar, puede evitar que los clientes creen documentos que contengan campos específicos usando hasAny() en una lista de campos prohibidos. Este método se evalúa como verdadero si un documento contiene alguno de estos campos, por lo que probablemente desees negar el resultado para prohibir ciertos 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 agregarán mediante una llamada al servidor en un momento posterior.

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 campos permitidos para nuevos documentos

En lugar de prohibir ciertos campos en documentos nuevos, es posible que desee crear una lista de solo aquellos campos que están permitidos explícitamente en documentos nuevos. Luego puede usar la función hasOnly() para asegurarse de que cualquier documento nuevo creado contenga solo estos campos (o un subconjunto de estos campos) 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']));
    }
  }
}

Combinando campos obligatorios y opcionales

Puede combinar las operaciones hasAll y hasOnly en sus reglas de seguridad para requerir algunos campos y permitir otros. Por ejemplo, este ejemplo requiere que todos los documentos nuevos contengan los campos name , location y city y, opcionalmente, permite 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 un escenario del mundo real, es posible que desees mover esta lógica a una función auxiliar para evitar duplicar tu código y combinar más fácilmente los campos opcionales y obligatorios en una sola lista, así:

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 al actualizar

Una práctica de seguridad común es permitir que los clientes solo editen algunos campos y no otros. No puede lograr esto únicamente mirando 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 incluyó. cambiar.

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

Al llamar al método affectedKeys() en este mapDiff, puede generar un conjunto de campos que se cambiaron en una edición. Luego puede usar funciones como hasOnly() o hasAny() para asegurarse de que este conjunto contenga (o no) ciertos elementos.

Evitar que se cambien algunos campos

Al utilizar el método hasAny() en el conjunto generado por affectedKeys() y luego negar el resultado, puedes rechazar cualquier solicitud de cliente que intente cambiar campos que no deseas cambiar.

Por ejemplo, es posible que desee permitir que los clientes actualicen la información sobre un restaurante pero no cambien su puntuación promedio 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 ciertos campos

En lugar de especificar campos que no desea cambiar, también puede usar la función hasOnly() para especificar una lista de campos que sí desea cambiar. Esto generalmente se considera más seguro porque las escrituras en cualquier campo de documento nuevo no están permitidas de forma predeterminada hasta que las permita explícitamente en sus reglas de seguridad.

Por ejemplo, en lugar de no permitir los campos average_score y rating_count , podría crear reglas de seguridad que permitan a los clientes cambiar solo los campos de 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 versión futura de su aplicación, los documentos del restaurante incluyen un campo telephone , los intentos de editar ese campo fallarán hasta que regrese y agregue ese campo a la lista hasOnly() en sus reglas de seguridad.

Hacer cumplir los tipos de campos

Otro efecto de que Cloud Firestore no tenga esquemas es que no hay ninguna aplicación a nivel de base de datos sobre qué tipos de datos se pueden almacenar en campos específicos. Sin embargo, esto es algo que puede aplicar en las reglas de seguridad con el operador is .

Por ejemplo, la siguiente regla de seguridad exige que el campo score de una reseña sea un número entero, los campos headline , content y author_name sean cadenas, y 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 dado que estos son generados por el lenguaje de reglas de seguridad en sí y no por 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. En otras palabras, puede utilizar reglas de seguridad para exigir que un determinado campo contenga una lista o un mapa, pero no puede exigir que un campo contenga una lista de todos los números enteros o todas las cadenas.

De manera similar, puede usar reglas de seguridad para imponer valores de tipo para entradas específicas en una lista o un mapa (usando notación entre paréntesis o nombres de clave respectivamente), pero no existe ningún atajo para imponer los tipos de datos de todos los miembros en un mapa o una lista en una vez.

Por ejemplo, las siguientes reglas garantizan que un campo tags en un documento contenga una lista y que la primera entrada sea una cadena. También garantiza 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, es posible que desee considerar la creación de una función auxiliar a la que pueda llamar en las secciones de creación y actualización de sus 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 para campos opcionales

Es importante recordar que llamar request.resource.data.foo en un documento donde foo no existe genera un error y, por lo tanto, cualquier regla de seguridad que realice esa llamada denegará la solicitud. Puede manejar esta situación utilizando el método get en request.resource.data . El método get le permite proporcionar un argumento predeterminado para el campo que está recuperando 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 desea verificar que sean cadenas y listas respectivamente, puede lograrlo reescribiendo la función reviewFieldsAreValidTypes en algo como lo siguiente:

  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;
  }

Esto rechaza los documentos donde existen tags , pero que no son una lista, y al mismo tiempo permite documentos que no contienen un campo tags (o photo_url ).

Nunca se permiten escrituras parciales.

Una nota final sobre las reglas de seguridad de Cloud Firestore es que permiten al cliente realizar un cambio en un documento o rechazan la edición completa. No puede crear reglas de seguridad que acepten escrituras en algunos campos de su documento y rechacen otros en la misma operación.