Proteja sus datos de Firestore con las reglas de seguridad de Firebase

1. Antes de comenzar

Cloud Firestore, Cloud Storage for Firebase y Realtime Database dependen de los archivos de configuración que usted 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 sencillo 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, utilice estas instrucciones ; para verificar su versión, ejecute java -version )

que haras

En este codelab, protegerá una plataforma de blog simple construida en Firestore. Utilizará el emulador de Firestore para ejecutar pruebas unitarias según las reglas de seguridad y asegurarse de que las reglas permitan o prohíban el acceso que espera.

Aprenderás cómo:

  • Conceder permisos granulares
  • Aplicar validaciones de datos y tipos
  • Implementar control de acceso basado en atributos
  • Conceder acceso según el método de autenticación
  • Crear funciones personalizadas
  • Cree reglas de seguridad basadas en el tiempo
  • Implementar una lista de denegación y eliminaciones temporales
  • Comprender 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:

Borradores de publicaciones de blog:

  • Los usuarios pueden crear borradores de publicaciones de blog, que se encuentran en la colección drafts .
  • El autor puede continuar 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.

Publicaciones 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 de 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 negadores 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 los moderadores.

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

Todo sucederá localmente, utilizando Firebase Emulator Suite.

Obtener el código fuente

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

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

Luego, vaya al directorio de estado inicial, donde trabajará durante el resto de este codelab:

$ cd codelab-rules/initial-state

Ahora, instala las dependencias para que puedas 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 -

Obtenga la CLI de Firebase

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

Iniciar los emuladores

La aplicación con la que trabajará tiene tres colecciones principales de Firestore: drafts contienen publicaciones de blog que están en progreso, 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 del usuario y otras condiciones necesarias para que un usuario cree, lea, actualice y elimine documentos en drafts , colecciones published y comments . Escribirás 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 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 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

...

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

4. Cree borradores de publicaciones de blog.

Debido a que el acceso a los borradores de las publicaciones de blog es muy diferente del acceso a las publicaciones de blog publicadas, esta aplicación de blogs almacena los borradores de las publicaciones de blog en una colección separada, /drafts . Solo el autor o un moderador pueden acceder a los borradores y tienen 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=**} , utiliza la sintaxis ** para aplicarse de forma recursiva a todos los documentos de 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 intentando 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 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 aplicación, solo la persona que figura como autor puede crear borradores. Verifique que el UID de la persona que realiza la solicitud sea 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 obliga a la aplicación a 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 pueda tener más de 50 caracteres:

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

Dado que todas estas condiciones deben ser verdaderas, concatenelas con el operador AND lógico, && . 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.

A continuación, a medida que los autores refinen los borradores de sus 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, sólo 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 por último, el título debe tener 50 caracteres o menos:

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

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

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

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

Las reglas completas ahora son:

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 son tan diferentes, esta aplicación desnormaliza las publicaciones en colecciones separadas draft y published . Por ejemplo, cualquier persona puede leer las publicaciones publicadas, pero no se pueden eliminar de forma definitiva, mientras que los borradores se pueden eliminar, pero solo el autor y los moderadores pueden leerlos. En esta aplicación, cuando un usuario quiere publicar un borrador de 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 de escribir son que cualquier persona puede leer las publicaciones publicadas y nadie puede crearlas ni eliminarlas. Añade 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;
}

Al agregarlas a las reglas existentes, el archivo de reglas completo 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:

  • sólo puede ser realizado por el autor o moderador, y
  • debe contener todos los campos obligatorios.

Dado que ya ha escrito las condiciones para ser autor o moderador, puede copiar y pegar las condiciones, pero con el tiempo esto podría resultar 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á 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á tanto para borradores como para 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 configurar 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;
}

Llame 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 puedes escribir una condición para actualizar las publicaciones publicadas que también utilice 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"
])

Crea una función personalizada por tu cuenta

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

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, deberías tener 5 pruebas aprobadas y 4 reprobadas.

9. Comentarios: subcolecciones y permisos de proveedores 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} ). Por defecto, las reglas de una colección no se aplican a las subcolecciones. No desea que se apliquen a los comentarios las mismas reglas que se aplican al documento principal de la publicación publicada; crearás otros 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

Leyendo comentarios: No puede ser anónimo.

Para esta aplicación, sólo los usuarios que hayan 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 se encuentra 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.

Crear comentarios: comprobar una lista de denegados

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

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

10. Actualización de comentarios: reglas basadas en 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 fue 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. Eliminar comentarios: verificar 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.

Primero, debido a que la función auxiliar que agregó anteriormente busca un campo authorUID que podría existir en una publicación o en un comentario, puede reutilizar la función auxiliar para verificar si el usuario es el autor o moderador:

isAuthorOrModerator(resource.data, request.auth)

Para verificar si el usuario es el autor de la publicación del blog, use get para buscar la publicación en Firestore:

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

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

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

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

Y el archivo de reglas completo es:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

12. Próximos pasos

¡Felicidades! ¡Has escrito las Reglas de Seguridad que hicieron pasar todas las pruebas y aseguraron la aplicación!

Aquí hay algunos temas relacionados en los que profundizar a continuación:

  • Publicación de blog : Cómo codificar la revisión de las reglas de seguridad
  • Codelab : recorrido por el primer desarrollo local con los emuladores
  • Vídeo : Cómo utilizar la configuración de CI para pruebas basadas en emulador usando GitHub Actions