Protégez vos données Firestore avec les règles de sécurité Firebase

1. Avant de commencer

Cloud Firestore, Cloud Storage pour Firebase et Realtime Database s'appuient sur les fichiers de configuration que vous écrivez pour accorder un accès en lecture et en écriture. Cette configuration, appelée Règles de sécurité, peut également servir de sorte de schéma pour votre application. C'est l'une des parties les plus importantes du développement de votre application. Et cet atelier de programmation vous guidera à travers tout cela.

Conditions préalables

  • Un éditeur simple tel que Visual Studio Code, Atom ou Sublime Text
  • Node.js 8.6.0 ou supérieur (pour installer Node.js, utilisez nvm ; pour vérifier votre version, exécutez node --version )
  • Java 7 ou supérieur (pour installer Java, utilisez ces instructions ; pour vérifier votre version, exécutez java -version )

Ce que tu feras

Dans cet atelier de programmation, vous sécuriserez une plate-forme de blog simple construite sur Firestore. Vous utiliserez l'émulateur Firestore pour exécuter des tests unitaires par rapport aux règles de sécurité et vous assurerez que les règles autorisent et interdisent l'accès que vous attendez.

Vous apprendrez à :

  • Accorder des autorisations granulaires
  • Appliquer les validations de données et de types
  • Implémenter un contrôle d'accès basé sur les attributs
  • Accorder l'accès en fonction de la méthode d'authentification
  • Créer des fonctions personnalisées
  • Créez des règles de sécurité basées sur le temps
  • Implémenter une liste de refus et des suppressions logicielles
  • Comprendre quand dénormaliser les données pour répondre à plusieurs modèles d'accès

2. Configurer

Ceci est une application de blog. Voici un résumé de haut niveau des fonctionnalités de l'application :

Projets d'articles de blog :

  • Les utilisateurs peuvent créer des brouillons d'articles de blog, qui se trouvent dans la collection drafts .
  • L'auteur peut continuer à mettre à jour un brouillon jusqu'à ce qu'il soit prêt à être publié.
  • Lorsqu'il est prêt à être publié, une fonction Firebase est déclenchée et crée un nouveau document dans la collection published .
  • Les brouillons peuvent être supprimés par l'auteur ou par les modérateurs du site

Articles de blog publiés :

  • Les publications publiées ne peuvent pas être créées par les utilisateurs, uniquement via une fonction.
  • Ils ne peuvent être supprimés que de manière réversible, ce qui met à jour un attribut visible sur false.

commentaires

  • Les publications publiées autorisent les commentaires, qui constituent une sous-collection de chaque publication publiée.
  • Pour réduire les abus, les utilisateurs doivent avoir une adresse e-mail vérifiée et ne pas être sur un refus pour pouvoir laisser un commentaire.
  • Les commentaires ne peuvent être mis à jour que dans l’heure suivant leur publication.
  • Les commentaires peuvent être supprimés par l'auteur du commentaire, l'auteur du message original ou par les modérateurs.

En plus des règles d'accès, vous créerez des règles de sécurité qui appliquent les champs obligatoires et les validations de données.

Tout se passera localement, à l'aide de Firebase Emulator Suite.

Obtenez le code source

Dans cet atelier de programmation, vous commencerez par tester les règles de sécurité, mais les règles de sécurité elles-mêmes seront minimes. La première chose à faire est donc de cloner la source pour exécuter les tests :

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

Accédez ensuite au répertoire de l'état initial, où vous travaillerez pour le reste de cet atelier de programmation :

$ cd codelab-rules/initial-state

Maintenant, installez les dépendances pour pouvoir exécuter les tests. Si votre connexion Internet est plus lente, cela peut prendre une minute ou deux :

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

Obtenez la CLI Firebase

La suite Emulator que vous utiliserez pour exécuter les tests fait partie de la Firebase CLI (interface de ligne de commande) qui peut être installée sur votre machine avec la commande suivante :

$ npm install -g firebase-tools

Ensuite, confirmez que vous disposez de la dernière version de la CLI. Cet atelier de programmation devrait fonctionner avec la version 8.4.0 ou ultérieure, mais les versions ultérieures incluent davantage de corrections de bugs.

$ firebase --version
9.10.2

3. Exécutez les tests

Dans cette section, vous exécuterez les tests localement. Cela signifie qu'il est temps de démarrer Emulator Suite.

Démarrez les émulateurs

L'application avec laquelle vous travaillerez comporte trois collections Firestore principales : drafts contiennent des articles de blog en cours, la collection published contient les articles de blog qui ont été publiés et comments sont une sous-collection d'articles publiés. Le référentiel est livré avec des tests unitaires pour les règles de sécurité qui définissent les attributs utilisateur et d'autres conditions requises pour qu'un utilisateur puisse créer, lire, mettre à jour et supprimer des documents dans des collections drafts , published et comments . Vous rédigerez les règles de sécurité pour que ces tests réussissent.

Pour commencer, votre base de données est verrouillée : les lectures et écritures dans la base de données sont universellement refusées et tous les tests échouent. Au fur et à mesure que vous écrivez les règles de sécurité, les tests réussiront. Pour voir les tests, ouvrez functions/test.js dans votre éditeur.

Sur la ligne de commande, démarrez les émulateurs à l'aide emulators:exec et exécutez les tests :

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

Faites défiler vers le haut de la sortie :

$ 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

...

Il y a actuellement 9 échecs. Au fur et à mesure que vous créez le fichier de règles, vous pouvez mesurer les progrès en regardant davantage de tests réussir.

4. Créez des brouillons d’articles de blog.

Étant donné que l'accès aux brouillons d'articles de blog est très différent de l'accès aux articles de blog publiés, cette application de blog stocke les brouillons d'articles de blog dans une collection distincte, /drafts . Les brouillons ne sont accessibles que par l'auteur ou un modérateur et comportent des validations pour les champs obligatoires et immuables.

En ouvrant le fichier firestore.rules , vous trouverez un fichier de règles par défaut :

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }
  }
}

L'instruction match, match /{document=**} , utilise la syntaxe ** pour s'appliquer de manière récursive à tous les documents des sous-collections. Et comme il s'agit du niveau le plus élevé, la même règle générale s'applique actuellement à toutes les demandes, peu importe qui fait la demande ou quelles données il essaie de lire ou d'écrire.

Commencez par supprimer l'instruction match la plus interne et remplacez-la par match /drafts/{draftID} . (Les commentaires sur la structure des documents peuvent être utiles dans les règles et seront inclus dans cet atelier de programmation ; ils sont toujours facultatifs.)

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 première règle que vous écrirez pour les brouillons contrôlera qui peut créer les documents. Dans cette application, les brouillons ne peuvent être créés que par la personne indiquée comme auteur. Vérifiez que l’UID de la personne faisant la demande est le même que celui indiqué dans le document.

La première condition pour la création sera :

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

Ensuite, les documents ne peuvent être créés que s'ils incluent les trois champs obligatoires, authorUID , createdAt et title . (L'utilisateur ne fournit pas le champ createdAt ; cela oblige l'application à l'ajouter avant d'essayer de créer un document.) Puisque vous devez uniquement vérifier que les attributs sont en cours de création, vous pouvez vérifier que request.resource a tous ces clés :

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

La dernière condition pour créer un article de blog est que le titre ne puisse pas contenir plus de 50 caractères :

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

Puisque toutes ces conditions doivent être vraies, concaténez-les avec l’opérateur logique ET, && . La première règle devient :

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

Dans le terminal, réexécutez les tests et confirmez que le premier test réussit.

5. Mettez à jour les brouillons des articles de blog.

Ensuite, à mesure que les auteurs peaufinent leurs brouillons d'articles de blog, ils modifieront les brouillons de documents. Créez une règle pour les conditions dans lesquelles une publication peut être mise à jour. Premièrement, seul l’auteur peut mettre à jour ses brouillons. Notez qu'ici vous vérifiez l'UID déjà écrit, resource.data.authorUID :

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

La deuxième condition requise pour une mise à jour est que deux attributs, authorUID et createdAt ne doivent pas changer :

request.resource.data.diff(resource.data).unchangedKeys().hasAll([
    "authorUID",
    "createdAt"
]);

Et enfin, le titre doit comporter 50 caractères ou moins :

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

Puisque ces conditions doivent toutes être remplies, concaténez-les avec && :

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;

Les règles complètes deviennent :

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

Réexécutez vos tests et confirmez qu'un autre test réussit.

6. Supprimer et lire les brouillons : contrôle d'accès basé sur les attributs

Tout comme les auteurs peuvent créer et mettre à jour des brouillons, ils peuvent également supprimer des brouillons.

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

De plus, les auteurs avec un attribut isModerator sur leur jeton d'authentification sont autorisés à supprimer les brouillons :

request.auth.token.isModerator == true

Puisque l'une ou l'autre de ces conditions est suffisante pour une suppression, concaténez-les avec un opérateur logique OU, || :

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

Les mêmes conditions s'appliquent aux lectures, afin que l'autorisation puisse être ajoutée à la règle :

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

Les règles complètes sont maintenant :

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

Réexécutez vos tests et confirmez qu'un autre test réussit désormais.

7. Lit, crée et supprime les publications publiées : dénormalisation pour différents modèles d'accès

Étant donné que les modèles d'accès aux publications publiées et aux brouillons sont si différents, cette application dénormalise les publications en collections distinctes draft et de publications published . Par exemple, les articles publiés peuvent être lus par n'importe qui mais ne peuvent pas être supprimés de manière définitive, tandis que les brouillons peuvent être supprimés mais ne peuvent être lus que par l'auteur et les modérateurs. Dans cette application, lorsqu'un utilisateur souhaite publier un brouillon d'article de blog, une fonction est déclenchée qui créera le nouvel article publié.

Ensuite, vous rédigerez les règles pour les publications publiées. Les règles les plus simples à écrire sont que les publications publiées peuvent être lues par n'importe qui et ne peuvent être créées ou supprimées par personne. Ajoutez ces règles :

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

En les ajoutant aux règles existantes, l'ensemble du fichier de règles devient :

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

Réexécutez les tests et confirmez qu'un autre test réussit.

8. Mise à jour des articles publiés : fonctions personnalisées et variables locales

Les conditions pour mettre à jour un article publié sont :

  • cela ne peut être fait que par l'auteur ou le modérateur, et
  • il doit contenir tous les champs obligatoires.

Puisque vous avez déjà rédigé des conditions pour être auteur ou modérateur, vous pouvez copier et coller les conditions, mais avec le temps, cela pourrait devenir difficile à lire et à maintenir. Au lieu de cela, vous allez créer une fonction personnalisée qui encapsule la logique permettant d’être auteur ou modérateur. Ensuite, vous l’appellerez à partir de plusieurs conditions.

Créer une fonction personnalisée

Au-dessus de l'instruction match pour les brouillons, créez une nouvelle fonction appelée isAuthorOrModerator qui prend comme arguments un document de publication (cela fonctionnera pour les brouillons ou les publications publiées) et l'objet d'authentification de l'utilisateur :

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: ...
    }
  }
}

Utiliser des variables locales

Dans la fonction, utilisez le mot-clé let pour définir les variables isAuthor et isModerator . Toutes les fonctions doivent se terminer par une instruction return, et la nôtre renverra un booléen indiquant si l'une ou l'autre des variables est vraie :

function isAuthorOrModerator(post, auth) {
  let isAuthor = auth.uid == post.authorUID;
  let isModerator = auth.token.isModerator == true;
  return isAuthor || isModerator;
}

Appeler la fonction

Vous allez maintenant mettre à jour la règle permettant aux brouillons d'appeler cette fonction, en prenant soin de transmettre resource.data comme premier argument :

  // Draft blog posts
  match /drafts/{draftID} {
    ...
    // Can be deleted by author or moderator
    allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
  }

Vous pouvez désormais rédiger une condition de mise à jour des articles publiés qui utilise également la nouvelle fonction :

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

Ajouter des validations

Certains champs d'un article publié ne doivent pas être modifiés, en particulier les champs url , authorUID et publishedAt sont immuables. Les deux autres champs, title et content , et visible doivent toujours être présents après une mise à jour. Ajoutez des conditions pour appliquer ces exigences pour les mises à jour des publications publiées :

// 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"
])

Créez vous-même une fonction personnalisée

Et enfin, ajoutez une condition selon laquelle le titre doit contenir moins de 50 caractères. Comme il s'agit d'une logique réutilisée, vous pouvez le faire en créant une nouvelle fonction, titleIsUnder50Chars . Avec la nouvelle fonction, la condition pour mettre à jour un article publié devient :

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

Et le fichier de règles complet est :

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

Refaites les tests. À ce stade, vous devriez avoir 5 tests réussis et 4 échecs.

9. Commentaires : sous-collections et autorisations du fournisseur de connexion

Les publications publiées autorisent les commentaires et les commentaires sont stockés dans une sous-collection de la publication publiée ( /published/{postID}/comments/{commentID} ). Par défaut, les règles d'une collection ne s'appliquent pas aux sous-collections. Vous ne voulez pas que les mêmes règles qui s'appliquent au document parent de la publication publiée s'appliquent aux commentaires ; vous en fabriquerez différents.

Pour rédiger des règles d'accès aux commentaires, commencez par l'instruction match :

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

Lecture des commentaires : ne peut pas être anonyme

Pour cette application, seuls les utilisateurs ayant créé un compte permanent, et non un compte anonyme, peuvent lire les commentaires. Pour appliquer cette règle, recherchez l'attribut sign_in_provider qui se trouve sur chaque objet auth.token :

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

Réexécutez vos tests et confirmez qu'un autre test réussit.

Créer des commentaires : vérifier une liste de refus

Il y a trois conditions pour créer un commentaire :

  • un utilisateur doit avoir une adresse e-mail vérifiée
  • le commentaire doit comporter moins de 500 caractères, et
  • ils ne peuvent pas figurer sur une liste d'utilisateurs bannis, qui est stockée dans Firestore dans la collection bannedUsers . En prenant ces conditions une à la fois :
request.auth.token.email_verified == true
request.resource.data.comment.size() < 500
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));

La règle finale pour créer des commentaires est la suivante :

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

L'intégralité du fichier de règles est désormais :

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

Réexécutez les tests et assurez-vous qu’un autre test réussit.

10. Mise à jour des commentaires : règles temporelles

La logique métier des commentaires est qu'ils peuvent être modifiés par l'auteur du commentaire pendant une heure après sa création. Pour implémenter cela, utilisez l'horodatage createdAt .

Tout d’abord, pour établir que l’utilisateur est l’auteur :

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

Ensuite, que le commentaire a été créé au cours de la dernière heure :

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

En les combinant avec l'opérateur logique ET, la règle de mise à jour des commentaires devient :

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

Réexécutez les tests et assurez-vous qu’un autre test réussit.

11. Suppression de commentaires : vérification de la propriété parentale

Les commentaires peuvent être supprimés par l'auteur du commentaire, un modérateur ou l'auteur du billet de blog.

Premièrement, étant donné que la fonction d'assistance que vous avez ajoutée précédemment recherche un champ authorUID qui pourrait exister dans une publication ou un commentaire, vous pouvez réutiliser la fonction d'assistance pour vérifier si l'utilisateur est l'auteur ou le modérateur :

isAuthorOrModerator(resource.data, request.auth)

Pour vérifier si l'utilisateur est l'auteur de l'article de blog, utilisez get pour rechercher l'article dans Firestore :

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

Étant donné que chacune de ces conditions est suffisante, utilisez un opérateur logique OU entre elles :

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;

Réexécutez les tests et assurez-vous qu’un autre test réussit.

Et le fichier de règles complet est :

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. Prochaines étapes

Toutes nos félicitations! Vous avez rédigé les règles de sécurité qui ont fait passer tous les tests et sécurisé l'application !

Voici quelques sujets connexes à aborder ensuite :

  • Article de blog : Comment coder les règles de sécurité de révision
  • Codelab : découvrir les premiers développements locaux avec les émulateurs
  • Vidéo : Comment utiliser la configuration de CI pour les tests basés sur un émulateur à l'aide des actions GitHub