Proteja sus 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 escribe para otorgar acceso de lectura y escritura. Esa configuración, llamada Reglas de seguridad, también puede actuar como una especie de esquema para su aplicación. Es una de las partes más importantes del desarrollo de su aplicación. Y este codelab lo guiará a través de él.

requisitos previos

  • Un editor simple como Visual Studio Code, Atom o Sublime Text
  • Node.js 8.6.0 o superior (para instalar Node.js, use nvm ; para verificar su versión, ejecute node --version )
  • Java 7 o superior (para instalar Java, use estas instrucciones ; para verificar su versión, ejecute java -version )

que vas a hacer

En este laboratorio de programación, protegerá una plataforma de blog simple creada en Firestore. Utilizará el emulador de Firestore para ejecutar pruebas unitarias contra las Reglas de seguridad y asegurarse de que las reglas permitan y no permitan el acceso que espera.

Aprenderá a:

  • Otorgar permisos granulares
  • Hacer cumplir las validaciones de datos y tipos
  • Implementar control de acceso basado en atributos
  • Otorgar acceso basado en el método de autenticación
  • Crear funciones personalizadas
  • Crear reglas de seguridad basadas en el tiempo
  • Implementar una lista de denegación y eliminaciones temporales
  • Comprenda cuándo desnormalizar los datos para cumplir con múltiples patrones de acceso

2. Configurar

Esta es una aplicación de blogs. Aquí hay un resumen de alto nivel de la funcionalidad de la aplicación:

Borrador de entradas de blog:

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

Entradas de blog publicadas:

  • Los usuarios no pueden crear publicaciones publicadas, solo a través de una función.
  • Solo se pueden eliminar temporalmente, lo que actualiza un atributo visible a falso.

Comentarios

  • Las publicaciones publicadas permiten comentarios, que son una subcolección en cada publicación publicada.
  • Para reducir el abuso, los usuarios deben tener una dirección de correo electrónico verificada y no estar en una lista de denegación para poder dejar un comentario.
  • Los comentarios solo se pueden actualizar dentro de una hora después de su publicación.
  • Los comentarios pueden ser eliminados por el autor del comentario, el autor de la publicación original o por los moderadores.

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

Todo sucederá localmente, utilizando Firebase Emulator Suite.

Obtener el código fuente

En este laboratorio de código, comenzará con pruebas para las reglas de seguridad, pero las reglas de seguridad serán mínimas, por lo que lo primero que debe hacer es clonar la fuente para ejecutar las pruebas:

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

Luego muévase al directorio de estado inicial, donde trabajará durante el resto de este laboratorio de código:

$ cd codelab-rules/initial-state

Ahora, instala las dependencias para que puedas ejecutar las pruebas. Si tiene una conexión a Internet más lenta, esto puede demorar uno o dos minutos:

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

Obtenga la CLI de Firebase

Emulator Suite que utilizará para ejecutar las pruebas es parte de Firebase CLI (interfaz de línea de comandos) que se puede instalar en su máquina con el siguiente comando:

$ npm install -g firebase-tools

A continuación, confirme que tiene la última versión de la CLI. Este codelab debería funcionar con la versión 8.4.0 o superior, pero las versiones posteriores incluyen más correcciones de errores.

$ firebase --version
9.10.2

3. Ejecute las pruebas

En esta sección, ejecutará las pruebas localmente. Esto significa que es hora de iniciar Emulator Suite.

Inicie los emuladores

La aplicación con la que trabajará tiene tres colecciones principales de Firestore: drafts contienen publicaciones de blog que están en curso, la colección published contiene las publicaciones de blog que se han publicado y comments son una subcolección de publicaciones publicadas. El repositorio viene con pruebas unitarias para las reglas de seguridad que definen los atributos de usuario y otras condiciones requeridas para que un usuario cree, lea, actualice y elimine documentos en drafts , published y colecciones comments . Escribirá las reglas de seguridad para que esas pruebas pasen.

Para empezar, su base de datos está bloqueada: las lecturas y escrituras en la base de datos se niegan universalmente y todas las pruebas fallan. A medida que escriba las reglas de seguridad, las pruebas pasarán. Para ver las pruebas, abra functions/test.js en su editor.

En la línea de comando, inicie los emuladores usando emulators:exec y ejecute las pruebas:

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

Desplácese hasta la parte superior de la salida:

$ 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

...

Ahora mismo hay 9 fallos. A medida que crea el archivo de reglas, puede medir el progreso observando cómo pasan más pruebas.

4. Crea borradores de publicaciones de blog.

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

Al abrir el archivo firestore.rules , encontrará 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 aplicar recursivamente a todos los documentos en las subcolecciones. Y debido a que está en el nivel superior, en este momento se aplica la misma regla general a todas las solicitudes, sin importar quién realiza la solicitud o qué datos están tratando de leer o escribir.

Comience eliminando la declaración de coincidencia más interna y reemplazándola con match /drafts/{draftID} . (Los comentarios sobre la estructura de los documentos pueden ser útiles en las reglas y se incluirán en este laboratorio de código; 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á para los borradores controlará quién puede crear los documentos. En esta aplicación, los borradores solo pueden ser creados por la persona que figura como autor. Compruebe que el UID de la persona que realiza la solicitud es el mismo UID que figura en el documento.

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

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 impone que la aplicación debe agregarlo antes de intentar crear un documento). Dado que solo necesita verificar que se están creando los atributos, puede verificar que request.resource tenga todos esas llaves:

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

El requisito final para crear una publicación 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 junto con el operador lógico AND, && . La primera regla se convierte en:

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, vuelva a ejecutar las pruebas y confirme que pasa la primera prueba.

5. Actualice los borradores de las publicaciones del blog.

Luego, a medida que los autores refinan sus borradores de publicaciones de blog, editarán los borradores de los documentos. Cree una regla para las condiciones en las que se puede actualizar una publicación. Primero, solo el autor puede actualizar sus borradores. Tenga en cuenta que aquí verifica 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"
]);

Y finalmente, el título debe tener 50 caracteres o menos:

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

Dado que todas estas condiciones deben cumplirse, concatenelas 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:

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;
    }
  }
}

Vuelva a ejecutar sus pruebas y confirme que pasa otra prueba.

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

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

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

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

request.auth.token.isModerator == true

Dado que cualquiera de estas condiciones es suficiente para una eliminación, concatenelas con un operador lógico OR, || :

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 permiso a la regla:

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

Las reglas completas son ahora:

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;
    }
  }
}

Vuelva a ejecutar sus pruebas y confirme que ahora pasa otra prueba.

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

Debido a que los patrones de acceso para las publicaciones publicadas y los borradores de publicaciones son tan diferentes, esta aplicación desnormaliza las publicaciones en draft y colecciones published separadas. Por ejemplo, cualquiera puede leer las publicaciones publicadas, pero no se pueden eliminar de forma permanente, mientras que los borradores se pueden eliminar, pero solo el autor y los moderadores pueden leerlos. En esta aplicación, cuando un usuario desea publicar un borrador de una publicación de blog, se activa una función que creará la nueva publicación publicada.

A continuación, escribirá las reglas para las publicaciones publicadas. Las reglas más simples para escribir son que cualquier persona puede leer las publicaciones publicadas y nadie puede crearlas ni eliminarlas. 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;
}

Agregándolos a las reglas existentes, todo el archivo de reglas se convierte en:

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;
    }
  }
}

Vuelva a ejecutar las pruebas y confirme que pasa otra prueba.

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

Las condiciones para actualizar un post publicado son:

  • solo puede ser realizado por el autor o moderador, y
  • debe contener todos los campos requeridos.

Dado que ya ha escrito las condiciones para ser autor o moderador, puede copiar y pegar las condiciones, pero con el tiempo podría volverse difícil de leer y mantener. En su lugar, creará una función personalizada que encapsule la lógica para ser autor o moderador. Luego, lo llamarás desde múltiples condiciones.

Crear una función personalizada

Encima de la declaración de coincidencia para borradores, cree una nueva función llamada isAuthorOrModerator que tome como argumentos un documento de publicación (esto funcionará para borradores o publicaciones publicadas) 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: ...
    }
  }
}

Usar variables locales

Dentro de la función, use la palabra clave let para establecer las variables isAuthor e isModerator . Todas las funciones deben terminar con una declaración 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;
}

Llamar a la función

Ahora actualizará 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 puede escribir una condición para actualizar publicaciones publicadas que también use la nueva función:

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

Agregar validaciones

Algunos de los campos de una publicación publicada no deben cambiarse, específicamente los campos url , authorUID y publishedAt son inmutables. Los otros dos campos, title y content , y visible aún deben estar presentes después de una actualización. Agregue condiciones para hacer cumplir estos requisitos para las actualizaciones de las publicaciones 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"
])

Cree una función personalizada por su cuenta

Y finalmente, agregue una condición de que el título tenga menos de 50 caracteres. Debido a que esta es una lógica reutilizada, puede hacerlo creando una nueva función, titleIsUnder50Chars . Con la nueva función, la condición para actualizar una publicación se convierte en:

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:

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);
    }
  }
}

Vuelva a ejecutar las pruebas. En este punto, debe tener 5 exámenes aprobados y 4 reprobados.

9. Comentarios: subcolecciones y permisos del proveedor de inicio de sesión

Las publicaciones publicadas permiten comentarios, y los comentarios 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 desea que las mismas reglas que se aplican al documento principal de la publicación publicada se apliquen a los comentarios; crearás diferentes.

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

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

Lectura de comentarios: no puede ser anónimo

Para esta aplicación, solo los usuarios que han creado una cuenta permanente, no una cuenta anónima, pueden leer los comentarios. Para hacer cumplir esa regla, busque el atributo sign_in_provider que está en cada objeto auth.token :

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

Vuelva a ejecutar sus pruebas y confirme que pasa una prueba más.

Creación de comentarios: Comprobación de una lista de denegación

Hay tres condiciones para crear un comentario:

  • un usuario debe tener un correo electrónico verificado
  • el comentario debe tener menos de 500 caracteres, y
  • no pueden estar en una lista de usuarios prohibidos, que se almacena en firestore en la colección bannedUsers . Tomando estas condiciones 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:

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 es ahora:

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 charachters
        request.resource.data.comment.size() < 500 &&
        // UID is not on the block list
        !exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
    }
  }
}

Vuelva a ejecutar las pruebas y asegúrese de que pase una prueba más.

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

La lógica empresarial de los comentarios es que el autor del comentario puede editarlos durante una hora después de su creación. Para implementar esto, use 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 haya creado en la última hora:

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

Combinando estos con el operador lógico AND, la regla para actualizar comentarios se convierte en:

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');

Vuelva a ejecutar las pruebas y asegúrese de que pase una prueba más.

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

Los comentarios pueden ser eliminados por el autor del comentario, un moderador o el autor de la publicación del blog.

En primer lugar, debido a que la función de ayuda que agregó anteriormente busca un campo authorUID que podría existir en una publicación o en un comentario, puede reutilizar la función de ayuda 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 publicación del blog, use un get para buscar la publicación en Firestore:

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

Como cualquiera de estas condiciones es suficiente, use 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;

Vuelva a ejecutar las pruebas y asegúrese de que pase una prueba 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 charachters
        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

¡Felicidades! ¡Ha escrito las reglas de seguridad que hicieron que todas las pruebas pasaran y aseguraron la aplicación!

Aquí hay algunos temas relacionados para sumergirse a continuación:

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