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 se basan en 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 una especie de esquema para tu app. Es una de las partes más importantes del desarrollo de tu aplicación. Y este codelab te guiará en el proceso.

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 simple 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 blogs. 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 publicaciones de blog, que se almacenan en la colección drafts.
  • El autor puede seguir actualizando un borrador hasta que esté listo para publicarse.
  • Cuando esté todo listo para publicarlo, se activará una función de Firebase que creará 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 publicaciones 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

  • En las entradas publicadas, se admiten 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 estar en una lista de usuarios rechazados para poder dejar un comentario.
  • Los comentarios solo se pueden actualizar dentro de una hora después de su publicación.
  • El autor del comentario, 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 que apliquen campos obligatorios y validaciones de datos.

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 initial-state, 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, esto puede tardar uno o dos minutos:

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

Obtén Firebase CLI

El Emulator Suite 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.

Inicia los emuladores

La aplicación con la que trabajarás tiene tres colecciones principales de Firestore: drafts contiene las entradas de blog en curso, la colección published contiene las entradas de blog publicadas y comments es una subcolección de las entradas publicadas. El repo 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, las pruebas se superarán. 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 fallas. A medida que compilas el archivo de reglas, puedes medir el progreso observando que se aprueben más pruebas.

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, que tienen validaciones para campos obligatorios e inmutables.

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 aplicarse de forma recursiva a 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, sin importar quién las realice o qué datos intente leer o escribir.

Comienza quitando 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 escribirás para los borradores controlará quién puede 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 pueden 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 con el operador lógico AND, &&. La primera regla se convierte en 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 publicaciones de blog.

A continuación, a medida que los autores definan mejor sus borradores de entradas de blog, editarán los documentos de borrador. 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í verificas 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, concatenarlas con un operador lógico O, ||:

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

Las mismas condiciones se aplican a las operaciones de lectura, de modo que se puede agregar el 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 publicaciones publicadas: desnormaliza para diferentes patrones de acceso

Debido a que los patrones de acceso de las publicaciones publicadas y de los borradores son muy diferentes, esta app desnormaliza las publicaciones en 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 un borrador de entrada de blog, se activa una función que creará la nueva entrada publicada.

A continuación, escribirás las reglas para las publicaciones publicadas. Las reglas más simples para escribir son que cualquier persona puede leer las publicaciones publicadas y que 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 agregas estas reglas a las existentes, el archivo de reglas completo 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 publicación 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, puedes copiar y pegar las condiciones, pero con el tiempo podría ser difícil leerlas y mantenerlas. 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 de retorno, y la nuestra mostrará 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 para que los borradores llamen a esa función, teniendo 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 publicaciones publicadas que también use la nueva función:

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

Agrega validaciones

Algunos de los campos de una publicación publicada no deben cambiarse, en particular, los campos url, authorUID y publishedAt son inmutables. Los otros dos campos, title y 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

Por último, agrega una condición para que el título tenga menos de 50 caracteres. Como se trata de una lógica reutilizada, puedes hacerlo creando una función nueva: 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: Permisos de subcolecciones y proveedores de acceso

Las publicaciones publicadas permiten 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 quieres que las mismas reglas que se aplican al documento principal de la entrada publicada se apliquen a los comentarios. crearás diferentes.

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

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

Lectura de comentarios: No se puede ser anónimo

En esta app, solo los usuarios que crearon una cuenta permanente, no una 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 pueden estar en una lista de usuarios prohibidos, 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 apruebe una más.

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

La lógica empresarial de 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, para establecer que el usuario es el autor:

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

A continuación, 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

El autor del comentario, un moderador o el autor de la entrada de blog pueden borrar los comentarios.

Primero, como 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 usarla 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

Dado que cualquiera de estas condiciones es suficiente, usa un operador OR lógico 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 apruebe una más.

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 que puedes explorar 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