Consulta datos de forma segura

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 la forma en la que interactúan las consultas con las reglas de seguridad de Cloud Firestore. Además, se examina con más detalle la manera en que las reglas de seguridad afectan a las consultas que puedes escribir y se describe cómo garantizar que las consultas usen las mismas restricciones que tus reglas de seguridad. En esta página, también se explica cómo escribir reglas de seguridad para permitir o rechazar consultas según las propiedades de una consulta, como limit y orderBy.

Las reglas no son filtros

Cuando escribas consultas para recuperar documentos, ten en cuenta que las reglas de seguridad no son filtros: las consultas son todo o nada. Para ahorrar tiempo y recursos, Cloud Firestore evalúa una consulta en comparación con su conjunto de resultados potenciales en lugar de con los valores de campo reales para todos tus documentos. Si una consulta llegara a mostrar documentos que el cliente no tiene permiso para leer, fallará toda la solicitud.

Consultas y reglas de seguridad

Tal como se muestra en los siguientes ejemplos, debes escribir las consultas de modo que se adapten a las restricciones de las reglas de seguridad.

Protege y consulta documentos en función del auth.uid

En el siguiente ejemplo, se muestra cómo escribir una consulta para recuperar documentos protegidos por una regla de seguridad. Supongamos que tienes una base de datos que contiene una colección de documentos story:

/stories/{storyid}

{
  title: "A Great Story",
  content: "Once upon a time...",
  author: "some_auth_id",
  published: false
}

Además de los campos title y content, cada documento almacena los campos author y published para usarlos a fin de controlar el acceso. En estos ejemplos, se supone que la app usa Firebase Authentication para configurar el campo author con el UID del usuario que creó el documento. Firebase Authentication también propaga la variable request.auth de las reglas de seguridad.

En la siguiente regla de seguridad, se usan las variables request.auth y resource.data para restringir el acceso de lectura y escritura de cada story, de modo que solo el autor pueda leer y escribir los documentos:

service cloud.firestore {
  match /databases/{database}/documents {
    match /stories/{storyid} {
      // Only the authenticated user who authored the document can read or write
      allow read, write: if request.auth != null && request.auth.uid == resource.data.author;
    }
  }
}

Supongamos que la app incluye una página que muestra a los usuarios una lista de documentos story que ellos mismos crearon. Es de esperar que puedas usar la siguiente consulta para propagar el contenido de la página. Sin embargo, esta consulta fallará porque no incluye las mismas restricciones que tus reglas de seguridad:

No válida: Las restricciones de la consulta no coinciden con las restricciones de las reglas de seguridad.

// This query will fail
db.collection("stories").get()

No se logra realizar la consulta incluso si el usuario actual es el autor de cada documento story. Este comportamiento se debe a que, cuando Cloud Firestore aplica las reglas de seguridad, evalúa la consulta según su conjunto de resultados posibles, no según las propiedades reales de los documentos de la base de datos. Si una consulta llegara a incluir documentos que infringen las reglas de seguridad, esta fallará.

Por el contrario, la siguiente consulta se ejecuta de forma correcta porque incluye la misma restricción del campo author que las reglas de seguridad:

Válida: Las restricciones de la consulta coinciden con las restricciones de las reglas de seguridad.

var user = firebase.auth().currentUser;

db.collection("stories").where("author", "==", user.uid).get()

Protege y consulta documentos según el valor de un campo

Para demostrar en más profundidad la interacción entre consultas y reglas, las siguientes reglas de seguridad amplían el acceso de lectura de la colección stories para que cualquier usuario pueda leer los documentos story cuyo campo published esté configurado como true.

service cloud.firestore {
  match /databases/{database}/documents {
    match /stories/{storyid} {
      // Anyone can read a published story; only story authors can read unpublished stories
      allow read: if resource.data.published == true || (request.auth != null && request.auth.uid == resource.data.author);
      // Only story authors can write
      allow write: if request.auth != null && request.auth.uid == resource.data.author;
    }
  }
}

La consulta para las páginas publicadas debe incluir las mismas restricciones que las reglas de seguridad:

db.collection("stories").where("published", "==", true).get()

La restricción de consulta .where("published", "==", true) garantiza que resource.data.published sea true para cualquier resultado. Por lo tanto, esta consulta cumple con las reglas de seguridad y tiene permitido leer los datos.

consultas OR

Al evaluar una consulta lógica OR (or, in o array-contains-any) según un conjunto de reglas, Cloud Firestore evalúa cada valor de comparación por separado. Cada valor de comparación debe cumplir con las restricciones de las reglas de seguridad. Veamos la siguiente regla como ejemplo:

match /mydocuments/{doc} {
  allow read: if resource.data.x > 5;
}

No válida: La consulta no garantiza que x > 5 en todos los documentos posibles.

// These queries will fail
query(db.collection("mydocuments"),
      or(where("x", "==", 1),
         where("x", "==", 6)
      )
    )

query(db.collection("mydocuments"),
      where("x", "in", [1, 3, 6, 42, 99])
    )

Válida: La consulta garantiza que x > 5 en todos los documentos posibles.

query(db.collection("mydocuments"),
      or(where("x", "==", 6),
         where("x", "==", 42)
      )
    )

query(db.collection("mydocuments"),
      where("x", "in", [6, 42, 99, 105, 200])
    )

Evalúa restricciones en las consultas

Las reglas de seguridad también pueden aceptar o rechazar consultas según sus restricciones. La variable request.query contiene las propiedades limit, offset y orderBy de una consulta. Por ejemplo, las reglas de seguridad pueden rechazar cualquier consulta que no limite la cantidad máxima de documentos recuperados a un intervalo determinado:

allow list: if request.query.limit <= 10;

En el siguiente conjunto de reglas, se muestra cómo escribir reglas de seguridad que evalúen las restricciones impuestas a las consultas. En este ejemplo, se amplía el conjunto de reglas stories con los siguientes cambios:

  • El conjunto de reglas desglosa la regla read en reglas get y list.
  • La regla get restringe la recuperación de documentos individuales a documentos públicos o documentos que haya creado el usuario.
  • La regla list aplica las mismas restricciones que get, pero en las consultas. También verifica el límite de consultas y, luego, rechaza cualquier consulta sin límite o con uno superior a 10.
  • El conjunto de reglas define una función authorOrPublished() para evitar la duplicación de código.
service cloud.firestore {

  match /databases/{database}/documents {

    match /stories/{storyid} {

      // Returns `true` if the requested story is 'published'
      // or the user authored the story
      function authorOrPublished() {
        return resource.data.published == true || request.auth.uid == resource.data.author;
      }

      // Deny any query not limited to 10 or fewer documents
      // Anyone can query published stories
      // Authors can query their unpublished stories
      allow list: if request.query.limit <= 10 &&
                     authorOrPublished();

      // Anyone can retrieve a published story
      // Only a story's author can retrieve an unpublished story
      allow get: if authorOrPublished();

      // Only a story's author can write to a story
      allow write: if request.auth.uid == resource.data.author;
    }

  }
}

Consultas de grupos de colecciones y reglas de seguridad

De forma predeterminada, las consultas se enfocan a una sola colección y solo obtienen resultados de ella. Con las consultas de grupos de colecciones, puedes obtener resultados de un grupo de colecciones que contenga todas las colecciones que tengan el mismo ID. En esta sección, se describe cómo proteger las consultas de grupos de colecciones con reglas de seguridad.

Protege y consulta documentos según grupos de colecciones

En las reglas de seguridad, debes permitir explícitamente las consultas de grupos de colecciones. Para ello, escribe una regla específica para estos grupos:

  1. Asegúrate de que rules_version = '2'; sea la primera línea del conjunto de reglas. Las consultas de grupos de colecciones requieren el nuevo comodín recursivo {name=**} correspondiente a la versión 2 de las reglas de seguridad.
  2. Escribe una regla para el grupo de colecciones con match /{path=**}/[COLLECTION_ID]/{doc}.

Por ejemplo, considera un foro organizado en documentos forum que contienen subcolecciones de posts:

/forums/{forumid}/posts/{postid}

{
  author: "some_auth_id",
  authorname: "some_username",
  content: "I just read a great story.",
}

En esta aplicación, permitimos que los propietarios puedan editar sus publicaciones y que los usuarios autenticados puedan leerlas:

service cloud.firestore {
  match /databases/{database}/documents {
    match /forums/{forumid}/posts/{post} {
      // Only authenticated users can read
      allow read: if request.auth != null;
      // Only the post author can write
      allow write: if request.auth != null && request.auth.uid == resource.data.author;
    }
  }
}

Cualquier usuario autenticado puede recuperar las publicaciones de cualquier foro:

db.collection("forums/technology/posts").get()

¿Pero qué sucede si deseas mostrar al usuario actual las publicaciones que realizó en todos los foros? Puedes usar una consulta de grupo de colecciones para obtener resultados de todas las colecciones de posts:

var user = firebase.auth().currentUser;

db.collectionGroup("posts").where("author", "==", user.uid).get()

En las reglas de seguridad, debes permitir esta consulta con una regla read o list para el grupo de colecciones posts:

rules_version = '2';
service cloud.firestore {

  match /databases/{database}/documents {
    // Authenticated users can query the posts collection group
    // Applies to collection queries, collection group queries, and
    // single document retrievals
    match /{path=**}/posts/{post} {
      allow read: if request.auth != null;
    }
    match /forums/{forumid}/posts/{postid} {
      // Only a post's author can write to a post
      allow write: if request.auth != null && request.auth.uid == resource.data.author;

    }
  }
}

Sin embargo, ten en cuenta que estas reglas se aplicarán a todas las colecciones que tengan el ID posts, independientemente de la jerarquía. Por ejemplo, estas reglas se aplican a todas estas colecciones de posts:

  • /posts/{postid}
  • /forums/{forumid}/posts/{postid}
  • /forums/{forumid}/subforum/{subforumid}/posts/{postid}

Protege consultas de grupos de colecciones según un campo

Al igual que las consultas de una sola colección, las consultas de grupos de colecciones también deben cumplir con las restricciones establecidas en las reglas de seguridad. Por ejemplo, podemos agregar un campo published a cada publicación del foro, como hicimos en el ejemplo anterior de stories:

/forums/{forumid}/posts/{postid}

{
  author: "some_auth_id",
  authorname: "some_username",
  content: "I just read a great story.",
  published: false
}

Luego, podemos escribir reglas para el grupo de colecciones de posts según el estado published y el author de la publicación:

rules_version = '2';
service cloud.firestore {

  match /databases/{database}/documents {

    // Returns `true` if the requested post is 'published'
    // or the user authored the post
    function authorOrPublished() {
      return resource.data.published == true || request.auth.uid == resource.data.author;
    }

    match /{path=**}/posts/{post} {

      // Anyone can query published posts
      // Authors can query their unpublished posts
      allow list: if authorOrPublished();

      // Anyone can retrieve a published post
      // Authors can retrieve an unpublished post
      allow get: if authorOrPublished();
    }

    match /forums/{forumid}/posts/{postid} {
      // Only a post's author can write to a post
      allow write: if request.auth.uid == resource.data.author;
    }
  }
}

Con estas reglas, los clientes web, de Apple y Android pueden realizar las siguientes consultas:

  • Cualquiera puede obtener las publicaciones de un foro:

    db.collection("forums/technology/posts").where('published', '==', true).get()
    
  • Cualquier usuario puede recuperar las publicaciones que realizó un autor en todos los foros:

    db.collectionGroup("posts").where("author", "==", "some_auth_id").where('published', '==', true).get()
    
  • Los autores pueden obtener todo lo que publicaron, o no, en todos los foros:

    var user = firebase.auth().currentUser;
    
    db.collectionGroup("posts").where("author", "==", user.uid).get()
    

Protege y consulta documentos en función de un grupo de colecciones y la ruta del documento

En algunos casos, es posible que desees restringir las consultas de grupos de colecciones en función de la ruta del documento. Para crear estas restricciones, puedes usar las mismas técnicas para proteger y consultar documentos en función de un campo.

Imagina una aplicación que realiza un seguimiento de las transacciones de cada usuario entre varios intercambios de acciones y criptomonedas:

/users/{userid}/exchange/{exchangeid}/transactions/{transaction}

{
  amount: 100,
  exchange: 'some_exchange_name',
  timestamp: April 1, 2019 at 12:00:00 PM UTC-7,
  user: "some_auth_id",
}

Observa el campo user. Si bien conocemos al propietario del documento transaction gracias a su ruta, duplicamos esta información en cada documento transaction porque nos permite realizar estas dos acciones:

  • Escribir consultas de grupos de colecciones que estén restringidas a documentos que incluyan un /users/{userid} específico en su ruta: Por ejemplo:

    var user = firebase.auth().currentUser;
    // Return current user's last five transactions across all exchanges
    db.collectionGroup("transactions").where("user", "==", user).orderBy('timestamp').limit(5)
    
  • Aplicar esta restricción a todas las consultas del grupo de colecciones de transactions para que un usuario no pueda recuperar los documentos de transaction de otro usuario

Aplicamos esta restricción a nuestras reglas de seguridad y, además, incluimos la validación de datos para el campo user:

rules_version = '2';
service cloud.firestore {

  match /databases/{database}/documents {

    match /{path=**}/transactions/{transaction} {
      // Authenticated users can retrieve only their own transactions
      allow read: if resource.data.user == request.auth.uid;
    }

    match /users/{userid}/exchange/{exchangeid}/transactions/{transaction} {
      // Authenticated users can write to their own transactions subcollections
      // Writes must populate the user field with the correct auth id
      allow write: if userid == request.auth.uid && request.data.user == request.auth.uid
    }
  }
}

Pasos siguientes