Protege tus datos de Firestore con las reglas de seguridad de Firebase

1. Antes de comenzar

Cloud Firestore, Cloud Storage para Firebase y Realtime Database dependen de los archivos de configuración que escribes para otorgar acceso de lectura y escritura. Esa configuración, llamada Reglas de seguridad, también puede actuar como un tipo de esquema para tu app. Es una de las partes más importantes del desarrollo de tu app. En este codelab, te guiaremos a través de él.

Requisitos previos

  • Un editor simple, como Visual Studio Code, Atom o Sublime Text
  • Node.js 8.6.0 o una versión posterior (para instalar Node.js, usa nvm; para comprobar tu versión, ejecuta node --version)
  • Java 7 o superior (para instalar Java, usa estas instrucciones; para comprobar tu versión, ejecuta java -version)

Actividades

En este codelab, protegerás una plataforma de blog sencilla compilada en Firestore. Usarás el emulador de Firestore para ejecutar pruebas de unidades con las reglas de seguridad y asegurarte de que estas permitan y no permitan el acceso que esperas.

Aprenderás a hacer lo siguiente:

  • Otorga permisos detallados
  • Aplica validaciones de datos y tipos
  • Implementa el control de acceso basado en atributos
  • Otorgar acceso según el método de autenticación
  • Crea funciones personalizadas
  • Crea reglas de seguridad basadas en el tiempo
  • Cómo implementar una lista de bloqueo y eliminaciones no definitivas
  • Comprender cuándo desnormalizar los datos para cumplir con múltiples patrones de acceso

2. Configurar

Esta es una aplicación de blog. Este es un resumen de alto nivel de la funcionalidad de la aplicación:

Borradores de entradas de blog:

  • Los usuarios pueden crear borradores de entradas de blog, las cuales se encuentran en la colección drafts.
  • El autor puede seguir actualizando un borrador hasta que esté listo para publicarse.
  • Cuando está listo para publicarse, se activa una función de Firebase que crea un documento nuevo en la colección published.
  • El autor o los moderadores del sitio pueden borrar los borradores.

Entradas de blog publicadas:

  • Los usuarios no pueden crear entradas publicadas. Los usuarios solo pueden crearlas a través de una función.
  • Solo se pueden borrar de forma no definitiva, lo que actualiza un atributo visible a falso.

Comentarios

  • En las entradas publicadas, se pueden realizar comentarios, que son una subcolección de cada publicación publicada.
  • Para reducir los abusos, los usuarios deben tener una dirección de correo electrónico verificada y no ser un denegador para dejar un comentario.
  • Los comentarios solo se pueden actualizar dentro de la hora posterior a su publicación.
  • Tanto el autor de la publicación, el autor de la publicación original o los moderadores pueden borrarlos.

Además de las reglas de acceso, crearás reglas de seguridad que apliquen validaciones de datos y campos obligatorios.

Todo ocurrirá de forma local con Firebase Emulator Suite.

Obtén el código fuente

En este codelab, comenzarás con pruebas para las reglas de seguridad, pero las reglas de seguridad mínimas en sí, por lo que lo primero que debes hacer es clonar la fuente para ejecutar las pruebas:

$ git clone https://github.com/FirebaseExtended/codelab-rules.git

Luego, ve al directorio de estado inicial, en el que trabajarás durante el resto de este codelab:

$ cd codelab-rules/initial-state

Ahora, instala las dependencias para poder ejecutar las pruebas. Si tienes una conexión a Internet más lenta, el proceso podría tardar uno o dos minutos:

# Move into the functions directory, install dependencies, jump out.
$ cd functions && npm install && cd -

Obtén Firebase CLI

El conjunto de herramientas del emulador que usarás para ejecutar las pruebas forma parte de Firebase CLI (interfaz de línea de comandos) que se puede instalar en tu máquina con el siguiente comando:

$ npm install -g firebase-tools

A continuación, confirma que tienes la versión más reciente de la CLI. Este codelab debería funcionar con la versión 8.4.0 o una posterior, pero las versiones posteriores incluyen más correcciones de errores.

$ firebase --version
9.10.2

3. Ejecuta las pruebas

En esta sección, ejecutarás las pruebas de manera local. Esto significa que es momento de iniciar Emulator Suite.

Cómo iniciar los emuladores

La aplicación con la que trabajarás tiene tres colecciones principales de Firestore: drafts contiene las entradas de blog que están en curso, la colección published contiene las entradas de blog que se publicaron y comments es una subcolección de las entradas publicadas. El repositorio incluye pruebas de unidades para las reglas de seguridad que definen los atributos del usuario y otras condiciones necesarias para que este cree, lea, actualice y borre documentos en las colecciones drafts, published y comments. Escribirás las reglas de seguridad para que se aprueben esas pruebas.

Para comenzar, tu base de datos está bloqueada: las operaciones de lectura y escritura en la base de datos se rechazan universalmente y todas las pruebas fallan. A medida que escribas las reglas de seguridad, las pruebas se superarán. Para ver las pruebas, abre functions/test.js en el editor.

En la línea de comandos, inicia los emuladores con emulators:exec y ejecuta las pruebas:

$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test"

Desplázate hasta la parte superior del resultado:

$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test"
i  emulators: Starting emulators: functions, firestore, hosting
⚠  functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: auth, database, pubsub
⚠  functions: Unable to fetch project Admin SDK configuration, Admin SDK behavior in Cloud Functions emulator may be incorrect.
i  firestore: Importing data from /Users/user/src/firebase/rules-codelab/initial-state/.seed/firestore_export/firestore_export.overall_export_metadata
i  firestore: Firestore Emulator logging to firestore-debug.log
⚠  hosting: Authentication error when trying to fetch your current web app configuration, have you run firebase login?
⚠  hosting: Could not fetch web app configuration and there is no cached configuration on this machine. Check your internet connection and make sure you are authenticated. To continue, you must call firebase.initializeApp({...}) in your code before using Firebase.
i  hosting: Serving hosting files from: public
✔  hosting: Local server: http://localhost:5000
i  functions: Watching "/Users/user/src/firebase/rules-codelab/initial-state/functions" for Cloud Functions...
✔  functions[publishPost]: http function initialized (http://localhost:5001/codelab/us-central1/publishPost).
✔  functions[softDelete]: http function initialized (http://localhost:5001/codelab/us-central1/softDelete).
i  Running script: pushd functions; npm test
~/src/firebase/rules-codelab/initial-state/functions ~/src/firebase/rules-codelab/initial-state

> functions@ test /Users/user/src/firebase/rules-codelab/initial-state/functions
> mocha

(node:76619) ExperimentalWarning: Conditional exports is an experimental feature. This feature could change at any time


  Draft blog posts
    1) can be created with required fields by the author
    2) can be updated by author if immutable fields are unchanged
    3) can be read by the author and moderator

  Published blog posts
    4) can be read by everyone; created or deleted by no one
    5) can be updated by author or moderator

  Comments on published blog posts
    6) can be read by anyone with a permanent account
    7) can be created if email is verfied and not blocked
    8) can be updated by author for 1 hour after creation
    9) can be deleted by an author or moderator


  0 passing (848ms)
  9 failing

...

En este momento, hay 9 fallas. A medida que compilas el archivo de reglas, puedes medir el progreso mirando más pruebas aprobadas.

4. Crear borradores de entradas de blog

Como el acceso a los borradores de entradas de blog es muy diferente al acceso a las entradas de blog publicadas, esta app de blog almacena las entradas de blog en borrador en una colección separada, /drafts. Solo el autor o un moderador puede acceder a los borradores, y tienen validaciones para los campos inmutables y obligatorios.

Cuando abras el archivo firestore.rules, encontrarás un archivo de reglas predeterminadas:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }
  }
}

La sentencia de coincidencia, match /{document=**}, usa la sintaxis ** para aplicar de manera recursiva todos los documentos de las subcolecciones. Y como está en el nivel superior, en este momento se aplica la misma regla general a todas las solicitudes, independientemente de quién las realice o qué datos intenten leer o escribir.

Comienza quitando la declaración de coincidencia más interna y reemplázala por match /drafts/{draftID}. (Los comentarios de la estructura de los documentos pueden ser útiles en las reglas y se incluirán en este codelab; siempre son opcionales).

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional
    }
  }
}

La primera regla que escribirás para los borradores controlará quiénes pueden crear los documentos. En esta app, solo la persona que figura como autor puede crear borradores. Verifica que el UID de la persona que realiza la solicitud sea el mismo que aparece en el documento.

La primera condición para la creación será la siguiente:

request.resource.data.authorUID == request.auth.uid

A continuación, los documentos solo se podrán crear si incluyen los tres campos obligatorios, authorUID, createdAt y title. (El usuario no proporciona el campo createdAt; esto hace que la app deba agregarlo antes de intentar crear un documento). Como solo necesitas verificar que se creen los atributos, puedes comprobar que request.resource tenga todas esas claves:

request.resource.data.keys().hasAll([
  "authorUID",
  "createdAt",
  "title"
])

El último requisito para crear una entrada de blog es que el título no puede tener más de 50 caracteres:

request.resource.data.title.size() < 50

Dado que todas estas condiciones deben ser verdaderas, concatenalas junto con el operador lógico AND, &&. La primera regla pasa a ser la siguiente:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;
    }
  }
}

En la terminal, vuelve a ejecutar las pruebas y confirma que la primera sea exitosa.

5. Actualiza los borradores de las entradas de blog.

A continuación, a medida que los autores definan mejor sus borradores de entradas de blog, editarán los borradores de documentos. Crea una regla para las condiciones en las que se puede actualizar una publicación. Primero, solo el autor puede actualizar sus borradores. Ten en cuenta que aquí debes verificar el UID que ya está escrito,resource.data.authorUID:

resource.data.authorUID == request.auth.uid

El segundo requisito para una actualización es que dos atributos, authorUID y createdAt, no deben cambiar:

request.resource.data.diff(resource.data).unchangedKeys().hasAll([
    "authorUID",
    "createdAt"
]);

Por último, el título debe tener 50 caracteres o menos:

request.resource.data.title.size() < 50;

Dado que se deben cumplir todas estas condiciones, puedes concatenarlas junto con &&:

allow update: if
  // User is the author, and
  resource.data.authorUID == request.auth.uid &&
  // `authorUID` and `createdAt` are unchanged
  request.resource.data.diff(resource.data).unchangedKeys().hasAll([
    "authorUID",
    "createdAt"
  ]) &&
  // Title must be < 50 characters long
  request.resource.data.title.size() < 50;

Las reglas completas se convierten en las siguientes:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow update: if
        // User is the author, and
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;
    }
  }
}

Vuelve a ejecutar las pruebas y confirma que se haya aprobado otra prueba.

6. Cómo borrar y leer borradores: Control de acceso basado en atributos

Así como los autores pueden crear y actualizar borradores, también pueden borrarlos.

resource.data.authorUID == request.auth.uid

Además, los autores con un atributo isModerator en su token de autenticación pueden borrar borradores:

request.auth.token.isModerator == true

Dado que cualquiera de estas condiciones es suficiente para borrar, concatenalas con un operador lógico OR, ||:

allow delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true

Se aplican las mismas condiciones a las lecturas, de modo que se puede agregar ese permiso a la regla:

allow read, delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true

Ahora las reglas completas son las siguientes:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow update: if
        // User is the author, and
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow read, delete: if
        // User is draft author
        resource.data.authorUID == request.auth.uid ||
        // User is a moderator
        request.auth.token.isModerator == true;
    }
  }
}

Vuelve a ejecutar las pruebas y confirma que se haya aprobado otra prueba.

7. Lee, crea y borra entradas publicadas: desnormalización para diferentes patrones de acceso

Debido a que los patrones de acceso para las entradas publicadas y las entradas de borrador son muy diferentes, esta app desnormaliza las entradas en las colecciones draft y published separadas. Por ejemplo, cualquier persona puede leer las publicaciones publicadas, pero no se pueden borrar de forma definitiva, mientras que los borradores pueden borrarse, pero solo pueden leerlas el autor y los moderadores. En esta app, cuando un usuario quiere publicar el borrador de una entrada de blog, se activa una función que creará la nueva entrada publicada.

A continuación, escribirás las reglas para las entradas publicadas. Las reglas más simples de escribir son que cualquier persona puede leer las publicaciones publicadas, pero nadie puede crearlas ni borrarlas. Agrega estas reglas:

match /published/{postID} {
  // `authorUID`: string, required
  // `content`: string, required
  // `publishedAt`: timestamp, required
  // `title`: string, < 50 characters, required
  // `url`: string, required
  // `visible`: boolean, required

  // Can be read by everyone
  allow read: if true;

  // Published posts are created only via functions, never by users
  // No hard deletes; soft deletes update `visible` field.
  allow create, delete: if false;
}

Cuando se agregan estas reglas a las reglas existentes, todo el archivo de reglas se convierte en lo siguiente:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow update: if
        // User is the author, and
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow read, delete: if
        // User is draft author
        resource.data.authorUID == request.auth.uid ||
        // User is a moderator
        request.auth.token.isModerator == true;
    }

    match /published/{postID} {
      // `authorUID`: string, required
      // `content`: string, required
      // `publishedAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, required
      // `visible`: boolean, required

      // Can be read by everyone
      allow read: if true;

      // Published posts are created only via functions, never by users
      // No hard deletes; soft deletes update `visible` field.
      allow create, delete: if false;
    }
  }
}

Vuelve a ejecutar las pruebas y confirma que se haya aprobado otra prueba.

8. Actualiza las entradas publicadas: funciones personalizadas y variables locales

Las condiciones para actualizar una entrada publicada son las siguientes:

  • solo lo puede hacer el autor o el moderador
  • debe contener todos los campos obligatorios.

Como ya escribiste condiciones para ser autor o moderador, podrías copiarlas y pegarlas, pero, con el tiempo, podrían volverse difíciles de leer y mantener. En su lugar, crearás una función personalizada que encapsule la lógica para ser autor o moderador. Luego, lo llamarás a partir de varias condiciones.

Crea una función personalizada

Arriba de la declaración de coincidencia de borradores, crea una nueva función llamada isAuthorOrModerator que tome como argumentos un documento de entrada (esto funcionará para borradores o entradas publicadas) y el objeto auth del usuario:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Returns true if user is post author or a moderator
    function isAuthorOrModerator(post, auth) {

    }

    match /drafts/{postID} {
      allow create: ...
      allow update: ...
      ...
    }

    match /published/{postID} {
      allow read: ...
      allow create, delete: ...
    }
  }
}

Usa variables locales

Dentro de la función, usa la palabra clave let para establecer las variables isAuthor y isModerator. Todas las funciones deben terminar con una sentencia return, y la nuestra devolverá un valor booleano que indicará si alguna de las variables es verdadera:

function isAuthorOrModerator(post, auth) {
  let isAuthor = auth.uid == post.authorUID;
  let isModerator = auth.token.isModerator == true;
  return isAuthor || isModerator;
}

Llama a la función

Ahora, actualizarás la regla de borradores para llamar a esa función y ten cuidado de pasar resource.data como primer argumento:

  // Draft blog posts
  match /drafts/{draftID} {
    ...
    // Can be deleted by author or moderator
    allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
  }

Ahora puedes escribir una condición para actualizar las entradas publicadas que también usa la nueva función:

allow update: if isAuthorOrModerator(resource.data, request.auth);

Agrega validaciones

Algunos de los campos de una publicación publicada no se deben cambiar. En particular, los campos url, authorUID y publishedAt son inmutables. Los otros dos campos, title, content y visible, deben seguir presentes después de una actualización. Agrega condiciones para hacer cumplir estos requisitos en las actualizaciones de las entradas publicadas:

// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
  "authorUID",
  "publishedAt",
  "url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
  "content",
  "title",
  "visible"
])

Crea una función personalizada por tu cuenta

Y, por último, agrega una condición para que el título tenga menos de 50 caracteres. Debido a que esta es lógica de reutilización, puedes hacerlo creando una nueva función, titleIsUnder50Chars. Con la función nueva, la condición para actualizar una entrada publicada será la siguiente:

allow update: if
  isAuthorOrModerator(resource.data, request.auth) &&
  // Immutable fields are unchanged
  request.resource.data.diff(resource.data).unchangedKeys().hasAll([
    "authorUID",
    "publishedAt",
    "url"
  ]) &&
  // Required fields are present
  request.resource.data.keys().hasAll([
    "content",
    "title",
    "visible"
  ]) &&
  titleIsUnder50Chars(request.resource.data);

Y el archivo de reglas completo es el siguiente:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Returns true if user is post author or a moderator
    function isAuthorOrModerator(post, auth) {
      let isAuthor = auth.uid == post.authorUID;
      let isModerator = auth.token.isModerator == true;
      return isAuthor || isModerator;
    }

    function titleIsUnder50Chars(post) {
      return post.title.size() < 50;
    }

    // Draft blog posts
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        titleIsUnder50Chars(request.resource.data);

      allow update: if
        // User is the author, and
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
          ]) &&
        titleIsUnder50Chars(request.resource.data);

      // Can be read or deleted by author or moderator
      allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
    }

    // Published blog posts are denormalized from drafts
    match /published/{postID} {
      // `authorUID`: string, required
      // `content`: string, required
      // `publishedAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, required
      // `visible`: boolean, required

      // Can be read by everyone
      allow read: if true;

      // Published posts are created only via functions, never by users
      // No hard deletes; soft deletes update `visible` field.
      allow create, delete: if false;

      allow update: if
        isAuthorOrModerator(resource.data, request.auth) &&
        // Immutable fields are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "publishedAt",
          "url"
        ]) &&
        // Required fields are present
        request.resource.data.keys().hasAll([
          "content",
          "title",
          "visible"
        ]) &&
        titleIsUnder50Chars(request.resource.data);
    }
  }
}

Vuelve a ejecutar las pruebas. En este punto, deberías tener 5 pruebas aprobadas y 4 pruebas fallidas.

9. Comentarios: Subcolecciones y permisos del proveedor de acceso

Las entradas publicadas permiten realizar comentarios, y estos se almacenan en una subcolección de la publicación publicada (/published/{postID}/comments/{commentID}). De forma predeterminada, las reglas de una colección no se aplican a las subcolecciones. No querrás que las mismas reglas que se aplican al documento principal de la entrada publicada se apliquen a los comentarios, sino que crearás otras diferentes.

Para escribir reglas para acceder a los comentarios, comienza con la declaración de coincidencia:

match /published/{postID}/comments/{commentID} {
  // `authorUID`: string, required
  // `comment`: string, < 500 characters, required
  // `createdAt`: timestamp, required
  // `editedAt`: timestamp, optional

Los comentarios de lectura no pueden ser anónimos

En esta app, solo los usuarios que hayan creado una cuenta permanente (no una cuenta anónima) pueden leer los comentarios. Para aplicar esa regla, busca el atributo sign_in_provider que se encuentra en cada objeto auth.token:

allow read: if request.auth.token.firebase.sign_in_provider != "anonymous";

Vuelve a ejecutar tus pruebas y confirma que se haya aprobado otra prueba.

Creación de comentarios: Verificar una lista de bloqueo

Existen tres condiciones para crear un comentario:

  • los usuarios deben tener un correo electrónico verificado
  • El comentario debe tener menos de 500 caracteres.
  • No puede estar en una lista de usuarios bloqueados, que se almacena en Firestore, en la colección bannedUsers. Cómo interpretar estas afecciones de a una a la vez:
request.auth.token.email_verified == true
request.resource.data.comment.size() < 500
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));

La regla final para crear comentarios es la siguiente:

allow create: if
  // User has verified email
  (request.auth.token.email_verified == true) &&
  // UID is not on bannedUsers list
  !(exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));

Todo el archivo de reglas ahora es:

For bottom of step 9
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Returns true if user is post author or a moderator
    function isAuthorOrModerator(post, auth) {
      let isAuthor = auth.uid == post.authorUID;
      let isModerator = auth.token.isModerator == true;
      return isAuthor || isModerator;
    }

    function titleIsUnder50Chars(post) {
      return post.title.size() < 50;
    }

    // Draft blog posts
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User is author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and createdAt fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        titleIsUnder50Chars(request.resource.data);

      allow update: if
        // User is author
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
          ]) &&
        titleIsUnder50Chars(request.resource.data);

      // Can be read or deleted by author or moderator
      allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
    }

    // Published blog posts are denormalized from drafts
    match /published/{postID} {
      // `authorUID`: string, required
      // `content`: string, required
      // `publishedAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, required
      // `visible`: boolean, required

      // Can be read by everyone
      allow read: if true;

      // Published posts are created only via functions, never by users
      // No hard deletes; soft deletes update `visible` field.
      allow create, delete: if false;

      allow update: if
        isAuthorOrModerator(resource.data, request.auth) &&
        // Immutable fields are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "publishedAt",
          "url"
        ]) &&
        // Required fields are present
        request.resource.data.keys().hasAll([
          "content",
          "title",
          "visible"
        ]) &&
        titleIsUnder50Chars(request.resource.data);
    }

    match /published/{postID}/comments/{commentID} {
      // `authorUID`: string, required
      // `createdAt`: timestamp, required
      // `editedAt`: timestamp, optional
      // `comment`: string, < 500 characters, required

      // Must have permanent account to read comments
      allow read: if !(request.auth.token.firebase.sign_in_provider == "anonymous");

      allow create: if
        // User has verified email
        request.auth.token.email_verified == true &&
        // Comment is under 500 characters
        request.resource.data.comment.size() < 500 &&
        // UID is not on the block list
        !exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
    }
  }
}

Vuelve a ejecutar las pruebas y asegúrate de que se haya aprobado otra prueba.

10. Actualizando comentarios: reglas basadas en el tiempo

La lógica empresarial para los comentarios es que el autor puede editarlos durante una hora después de su creación. Para implementar esto, usa la marca de tiempo createdAt.

Primero, establece que el usuario es el autor:

request.auth.uid == resource.data.authorUID

Luego, verifica que el comentario se creó en la última hora:

(request.time - resource.data.createdAt) < duration.value(1, 'h');

Si los combinas con el operador lógico AND, la regla para actualizar comentarios será la siguiente:

allow update: if
  // is author
  request.auth.uid == resource.data.authorUID &&
  // within an hour of comment creation
  (request.time - resource.data.createdAt) < duration.value(1, 'h');

Vuelve a ejecutar las pruebas y asegúrate de que se haya aprobado otra prueba.

11. Eliminación de comentarios: verificación de la propiedad de los elementos superiores

Tanto el autor, el moderador o el autor de la entrada de blog pueden borrar los comentarios.

Primero, debido a que la función auxiliar que agregaste antes verifica un campo authorUID que podría existir en una publicación o un comentario, puedes volver a usar la función auxiliar para verificar si el usuario es el autor o moderador:

isAuthorOrModerator(resource.data, request.auth)

Para verificar si el usuario es el autor de la entrada de blog, usa un get para buscar la entrada en Firestore:

request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID

Dado que cualquiera de estas condiciones es suficiente, usa un operador lógico OR entre ellas:

allow delete: if
  // is comment author or moderator
  isAuthorOrModerator(resource.data, request.auth) ||
  // is blog post author
  request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID;

Vuelve a ejecutar las pruebas y asegúrate de que se haya aprobado otra prueba.

Y todo el archivo de reglas es:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Returns true if user is post author or a moderator
    function isAuthorOrModerator(post, auth) {
      let isAuthor = auth.uid == post.authorUID;
      let isModerator = auth.token.isModerator == true;
      return isAuthor || isModerator;
    }

    function titleIsUnder50Chars(post) {
      return post.title.size() < 50;
    }

    // Draft blog posts
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User is author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and createdAt fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        titleIsUnder50Chars(request.resource.data);

      allow update: if
        // User is author
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
          ]) &&
        titleIsUnder50Chars(request.resource.data);

      // Can be read or deleted by author or moderator
      allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
    }

    // Published blog posts are denormalized from drafts
    match /published/{postID} {
      // `authorUID`: string, required
      // `content`: string, required
      // `publishedAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, required
      // `visible`: boolean, required

      // Can be read by everyone
      allow read: if true;

      // Published posts are created only via functions, never by users
      // No hard deletes; soft deletes update `visible` field.
      allow create, delete: if false;

      allow update: if
        isAuthorOrModerator(resource.data, request.auth) &&
        // Immutable fields are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "publishedAt",
          "url"
        ]) &&
        // Required fields are present
        request.resource.data.keys().hasAll([
          "content",
          "title",
          "visible"
        ]) &&
        titleIsUnder50Chars(request.resource.data);
    }

    match /published/{postID}/comments/{commentID} {
      // `authorUID`: string, required
      // `createdAt`: timestamp, required
      // `editedAt`: timestamp, optional
      // `comment`: string, < 500 characters, required

      // Must have permanent account to read comments
      allow read: if !(request.auth.token.firebase.sign_in_provider == "anonymous");

      allow create: if
        // User has verified email
        request.auth.token.email_verified == true &&
        // Comment is under 500 characters
        request.resource.data.comment.size() < 500 &&
        // UID is not on the block list
        !exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));

      allow update: if
        // is author
        request.auth.uid == resource.data.authorUID &&
        // within an hour of comment creation
        (request.time - resource.data.createdAt) < duration.value(1, 'h');

      allow delete: if
        // is comment author or moderator
        isAuthorOrModerator(resource.data, request.auth) ||
        // is blog post author
        request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID;
    }
  }
}

12. Próximos pasos

¡Felicitaciones! Escribiste las reglas de seguridad que hicieron que todas las pruebas se aprobaran y protegieron la aplicación.

Estos son algunos temas relacionados para analizar a continuación:

  • Entrada de blog: Cómo revisar el código de las reglas de seguridad
  • Codelab: Explicación del primer desarrollo local con los emuladores
  • Video: Cómo usar la configuración de la CI para pruebas basadas en emuladores con acciones de GitHub