Protege los 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 aplicación. En este codelab, te explicaremos cómo hacerlo.

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 verificar tu versión, ejecuta node --version)
  • Java 7 o superior (para instalar Java, sigue estas instrucciones; para comprobar tu versión, ejecuta java -version)

Actividades

En este codelab, protegerás una plataforma de blog simple compilada en Firestore. Usarás el emulador de Firestore para ejecutar pruebas de unidades con las reglas de seguridad y asegurarte de que las reglas permitan y deshabiliten 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
  • Cómo crear funciones personalizadas
  • Crea reglas de seguridad basadas en el tiempo
  • Implementa una lista de denegación y eliminaciones de forma no definitiva
  • Comprender cuándo desnormalizar los datos para cumplir con varios patrones de acceso

2. Configurar

Esta es una aplicación de blogs. Este es un resumen de alto nivel de las funciones de la aplicación:

Borradores de entradas de blog:

  • Los usuarios pueden crear borradores de entradas de blog, que se encuentran en la colección drafts.
  • El autor puede seguir actualizando un borrador hasta que esté listo para su publicación.
  • Cuando está lista 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, solo a través de una función.
  • Solo se pueden borrar de forma no definitiva, lo que actualiza un atributo visible a falso.

Comentarios

  • Las entradas publicadas permiten realizar comentarios, que son una subcolección de cada entrada publicada.
  • Para reducir los abusos, los usuarios deben tener una dirección de correo electrónico verificada y no tener una entidad de bloqueo para dejar un comentario.
  • Los comentarios solo se pueden actualizar en el plazo de una hora después de su publicación.
  • El autor, el autor de la publicación original o los moderadores pueden borrar los comentarios.

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

Todo sucederá 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 imitarás las reglas, 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 lenta, es posible que este proceso demore unos minutos:

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

Cómo obtener Firebase CLI

El conjunto de herramientas 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 forma 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 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 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 un usuario 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, se aprobarán las pruebas. Para ver las pruebas, abre functions/test.js en tu 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 errores. A medida que creas el archivo de reglas, puedes medir el progreso viendo más pruebas aprobadas.

4. Crear borradores para entradas de blog

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

Si abres el archivo firestore.rules, encontrarás un archivo de reglas predeterminado:

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 aplicarla de manera recursiva a todos los documentos de las subcolecciones. Y, como se encuentra en el nivel superior, en este momento se aplica la misma regla general a todas las solicitudes, sin importar quién la realice o qué datos intente leer o escribir.

Primero, quita la declaración de coincidencia más interna y reemplázala por match /drafts/{draftID}. (Los comentarios sobre 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 escribas para los borradores controlará quiénes pueden crear los documentos. En esta aplicación, solo la persona que figura como autor puede crear los 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 pueden crear si incluyen los tres campos obligatorios: authorUID, createdAt y title. (El usuario no proporciona el campo createdAt; esto implica que la app deba agregarlo antes de intentar crear un documento). Dado que 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, concatenelas con el operador lógico AND, &&. La primera regla 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;
    }
  }
}

En la terminal, vuelve a ejecutar las pruebas y confirma que se haya aprobado la primera prueba.

5. Actualizar los borradores de las entradas de blog

A continuación, a medida que los autores definen mejor sus borradores de entradas de blog, editarán los documentos en borrador. Cree una regla para las condiciones en las que se puede actualizar una publicación. En primer lugar, solo el autor puede actualizar sus borradores. Ten en cuenta que aquí puedes 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 todas estas condiciones deben cumplirse, concatenelas 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 tus pruebas y confirma que se haya aprobado otra.

6. Borrar y leer borradores: control de acceso basado en atributos

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

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

Además, los autores que tengan 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 pueda agregar 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 tus pruebas y confirma que ahora se apruebe otra prueba.

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

Debido a que los patrones de acceso para las entradas publicadas y las entradas en borrador son tan diferentes, esta app desnormaliza las publicaciones en colecciones de draft y published independientes. Por ejemplo, cualquier persona puede leer las entradas publicadas, pero no se pueden borrar de forma definitiva, mientras que los borradores se pueden borrar, pero solo el autor y los moderadores pueden leerlos. 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 entradas 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;
}

Si se agregan a las reglas existentes, todo el archivo de reglas tendrá el siguiente aspecto:

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.

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

Las condiciones para actualizar una publicación son las siguientes:

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

Dado que ya escribiste condiciones para ser autor o moderador, puedes copiarlas y pegarlas, pero con el tiempo esto podría volverse difícil 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 desde varias condiciones.

Cómo crear una función personalizada

Arriba de la declaración de coincidencia para borradores, crea una nueva función llamada isAuthorOrModerator que tome como argumento un documento de entrada (esto funcionará para borradores o publicaciones 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 configurar las variables isAuthor y isModerator. Todas las funciones deben terminar con una sentencia return, y las nuestras muestran un valor booleano que indica 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 para que los borradores llamen a esa función y asegúrate 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 publicaciones que también use la nueva función:

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

Agregar validaciones

Algunos de los campos de una entrada publicada no se deben cambiar. Específicamente, 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 aplicar estos requisitos en relación con 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"
])

Cómo crear una función personalizada por tu cuenta

Por último, agrega la condición de que el título tenga menos de 50 caracteres. Debido a que esta es una lógica reutilizada, puedes crear una función nueva, titleIsUnder50Chars. Con la nueva función, la condición para actualizar una entrada publicada pasa a 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 con errores.

9. Comentarios: Subcolecciones y permisos del proveedor de acceso

Las entradas publicadas permiten comentarios, que se almacenan en una subcolección de la entrada publicada (/published/{postID}/comments/{commentID}). De forma predeterminada, no se aplican las reglas de una colección 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; deberás crear diferentes.

Para escribir reglas de acceso a los comentarios, comienza con la sentencia match:

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

Lectura de comentarios: No puede ser anónimo

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 una más.

Crea comentarios: revisa una lista de rechazo

Hay tres condiciones para crear un comentario:

  • un usuario debe tener un correo electrónico verificado
  • El comentario debe tener menos de 500 caracteres.
  • No pueden estar en una lista de usuarios bloqueados, que está almacenada en Firestore en la colección bannedUsers. Realice 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));

Ahora, todo el archivo de reglas tiene el siguiente aspecto:

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 aprueben una o más.

10. Actualización de los comentarios: Reglas basadas en el tiempo

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

Primero, para establecer que el usuario es el autor:

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

Luego, verifica que el comentario se haya creado en la última hora:

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

Si la combinas con el operador lógico AND, la regla para actualizar comentarios se convierte en 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 aprueben una o más.

11. Eliminación de comentarios: verificando la propiedad parental

El autor, el moderador o el autor de la entrada de blog pueden borrarlos.

En primer lugar, debido a que la función auxiliar que agregaste antes busca 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 el 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

Debido a 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 aprueben una o más.

Y el archivo de reglas completo 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 fueran exitosas y protegieran la aplicación.

Estos son algunos temas relacionados para profundizar 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 CI para pruebas basadas en emuladores con Acciones de GitHub