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

Actividades

En este codelab, protegerás una plataforma de blogs simple compilada en Firestore. Usarás el emulador de Firestore para ejecutar pruebas de unidades en las reglas de seguridad y asegurarte de que las reglas permitan y rechacen el acceso que esperas.

Aprenderás a hacer lo siguiente:

  • Cómo otorgar permisos detallados
  • Aplicar validaciones de datos y tipos
  • Implementa el control de acceso basado en atributos
  • Otorga acceso según el método de autenticación
  • Crea funciones personalizadas
  • Crea reglas de seguridad basadas en el tiempo
  • Implementa una lista de entidades denegadas y borrados no definitivos
  • Comprende cuándo desnormalizar los datos para satisfacer varios patrones de acceso

2. Configurar

Esta es una aplicación de blogs. A continuación, se incluye un resumen general 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á lista para publicarse, se activa una función de Firebase que crea un documento nuevo en la colección published.
  • Los borradores pueden ser borrados por el autor o por los moderadores del sitio.

Entradas de blog publicadas:

  • Los usuarios no pueden crear publicaciones publicadas, solo se pueden crear 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 publicaciones permiten comentarios, que son una subcolección en cada publicación.
  • Para reducir el abuso, los usuarios deben tener una dirección de correo electrónico verificada y no estar en una lista de bloqueo para dejar un comentario.
  • Los comentarios solo se pueden actualizar dentro de la hora posterior a su publicación.
  • Los comentarios pueden borrarlos el autor del comentario, el autor de la publicación original o los moderadores.

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

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 con reglas de seguridad mínimas. Por lo tanto, lo primero que debes hacer es clonar la fuente para ejecutar las pruebas:

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

Luego, muévete 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 paquete 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 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 hora 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 que se publicaron y comments es una subcolección de las entradas publicadas. El repo incluye pruebas unitarias 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 esas pruebas pasen.

Para comenzar, tu base de datos está bloqueada: se rechazan universalmente las lecturas y escrituras en la base de datos, y todas las pruebas fallan. A medida que escribas reglas de seguridad, las pruebas se aprobará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 cómo se aprueban más pruebas.

4. Crea borradores de entradas de blog.

Dado que el acceso a las entradas de blog en borrador es muy diferente del acceso a 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 pueden acceder a los borradores, que tienen validaciones para los campos obligatorios e inmutables.

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 declaración 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 ni qué datos intente leer o escribir.

Comienza por quitar la sentencia 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, pero 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 aplicación, 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 se indica en el documento.

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

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

Luego, los documentos solo se pueden crear si incluyen los tres campos obligatorios: authorUID,createdAt y title. (El usuario no proporciona el campo createdAt; esto exige que la app lo agregue antes de intentar crear un documento). Como solo necesitas verificar que se estén creando los atributos, puedes comprobar que request.resource tenga todas esas claves:

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

El requisito final 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, concaténalas 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 se apruebe.

5. Actualiza los borradores de las entradas de blog.

A continuación, a medida que los autores mejoren 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 no deben cambiar dos atributos, authorUID y createdAt:

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, concaténalas 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 serán 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 apruebe otra prueba.

6. Borra y lee borradores: Control de acceso basado en atributos

Del mismo modo que 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 una eliminación, concaténalas con un operador OR lógico, ||:

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

Las mismas condiciones se aplican a las lecturas, por lo que se puede agregar ese permiso a la regla:

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

Las reglas completas ahora 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 ahora se aprueba otra prueba.

7. Lecturas, creaciones y eliminaciones de publicaciones: desnormalización para diferentes patrones de acceso

Debido a que los patrones de acceso para las publicaciones publicadas y las publicaciones en borrador 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 se pueden borrar, pero solo los pueden leer el autor y los moderadores. En esta app, cuando un usuario quiere publicar una entrada de blog en borrador, 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 que se pueden 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 verá de la siguiente manera:

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 apruebe otra prueba.

8. Actualización de publicaciones: Funciones personalizadas y variables locales

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

  • Solo el autor o el moderador pueden hacerlo.
  • Debe contener todos los campos obligatorios.

Como ya escribiste las condiciones para ser autor o moderador, podrías copiarlas y pegarlas, pero, con el tiempo, eso podría ser 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, la llamarás desde varias condiciones.

Cómo crear una función personalizada

Sobre la instrucción de coincidencia para borradores, crea una nueva función llamada isAuthorOrModerator que tome como argumentos un documento de publicación (esto funcionará para borradores o publicaciones) y el objeto de autenticación 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 finalizar con una sentencia de retorno, y la nuestra devolverá 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 borradores para llamar 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 campos de una entrada publicada no deben cambiarse, específicamente 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 aplicar estos requisitos a las actualizaciones de las publicaciones:

// 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 lógica reutilizada, podrías crear una función nueva, titleIsUnder50Chars. Con la nueva función, la condición para actualizar una publicación publicada se convierte en 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 reprobadas.

9. Comentarios: Subcolecciones y permisos del proveedor 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 se apliquen a los comentarios las mismas reglas que se aplican al documento principal de la publicación, por lo que crearás reglas diferentes.

Para escribir reglas de acceso a los comentarios, comienza con la instrucción de coincidencia:

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

Leer comentarios: No se puede hacer de forma anónima

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 las pruebas y confirma que se aprueba una prueba más.

Creación de comentarios: Verificación de una lista de bloqueo

Existen tres condiciones para crear un comentario:

  • El usuario debe 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. Analicemos estas condiciones una por una:
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));

El archivo de reglas completo ahora es el siguiente:

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

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

La lógica empresarial de los comentarios indica 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, haz lo siguiente:

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 combinamos estos elementos 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 apruebe una prueba más.

11. Cómo borrar comentarios: Se verifica la propiedad parental

Los comentarios pueden borrarlos el autor del comentario, un moderador o el autor de la entrada de blog.

Primero, como la función auxiliar que agregaste antes verifica si hay un campo authorUID que podría existir en una publicación o en un comentario, puedes reutilizar 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

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

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 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 pasaran y protegiste 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: Recorrido por el desarrollo local primero con los emuladores
  • Video: Cómo configurar la CI para pruebas basadas en emuladores con Acciones de GitHub