1. Avant de commencer
Cloud Firestore, Cloud Storage for Firebase et la base de données en temps réel s'appuient sur des 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 schéma pour votre application. Il s'agit de l'une des parties les plus importantes du développement de votre application. Cet atelier de programmation vous guidera tout au long de cette procédure.
Conditions préalables
- Un éditeur simple tel que Visual Studio Code, Atom ou Sublime Text
- Node.js 8.6.0 ou version ultérieure (pour installer Node.js, utilisez nvm ; pour vérifier votre version, exécutez
node --version
) - Java 7 ou version ultérieure (pour installer Java, suivez ces instructions ; vérifiez votre version en exécutant
java -version
)
Objectifs de l'atelier
Dans cet atelier de programmation, vous allez sécuriser une plate-forme de blog simple basée sur Firestore. Vous utiliserez l'émulateur Firestore pour exécuter des tests unitaires sur les règles de sécurité, et vous assurer qu'elles autorisent et interdisent l'accès que vous attendez.
Vous allez apprendre à effectuer les opérations suivantes :
- Accorder des autorisations précises
- Appliquer des validations de données et de type
- Implémenter le contrôle des 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éer des règles de sécurité temporelles
- Implémenter une liste de refus et des suppressions réversibles
- Savoir quand dénormaliser les données pour répondre à plusieurs modèles d'accès
2. Configurer
Il s'agit d'une application de blog. Voici un récapitulatif général des fonctionnalités de l'application:
Brouillons 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 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 utilisateurs ne peuvent pas créer de posts publiés, mais uniquement via une fonction.
- Ils ne peuvent être supprimés que de façon réversible, ce qui met à jour un attribut
visible
sur "false".
Commentaires
- Les posts publiés autorisent les commentaires, qui constituent une sous-collection de chaque article publié.
- Afin de limiter les abus, les utilisateurs doivent disposer d'une adresse e-mail validée et ne pas être bloqués pour pouvoir laisser un commentaire.
- Les commentaires ne peuvent être modifiés que dans l'heure qui suit leur publication.
- Les commentaires peuvent être supprimés par leur auteur, par l'auteur du post d'origine ou par les modérateurs.
En plus des règles d'accès, vous allez créer des règles de sécurité qui appliquent des champs obligatoires et des validations de données.
Tout se passe localement, à l'aide de la suite d'émulateurs Firebase.
Obtenir le code source
Dans cet atelier de programmation, vous commencerez par des tests pour les règles de sécurité, mais avec des règles de sécurité semblables. 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 "initial-state", dans lequel vous allez travailler pour le reste de cet atelier de programmation :
$ cd codelab-rules/initial-state
Installez maintenant les dépendances pour pouvoir exécuter les tests. Si vous utilisez une connexion Internet lente, l'opération peut prendre une ou deux minutes:
# Move into the functions directory, install dependencies, jump out. $ cd functions && npm install && cd -
Obtenir la CLI Firebase
La suite d'émulateurs que vous utiliserez pour exécuter les tests fait partie de la CLI Firebase (interface de ligne de commande), que vous pouvez installer sur votre machine à l'aide de la commande suivante :
$ npm install -g firebase-tools
Vérifiez ensuite 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écuter les tests
Dans cette section, vous allez exécuter les tests en local. Il est donc temps de démarrer la suite d'émulateurs.
Démarrer les émulateurs
L'application avec laquelle vous allez travailler comporte trois collections Firestore principales: drafts
contient des articles de blog en cours, la collection published
contient les articles de blog qui ont été publiés et comments
est une sous-collection sur les articles publiés. Le dépôt est fourni avec des tests unitaires pour les règles de sécurité qui définissent les attributs utilisateur et les autres conditions requises pour qu'un utilisateur puisse créer, lire, mettre à jour et supprimer des documents dans les 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 les écritures dans la base de données sont universellement refusées, et tous les tests échouent. Lorsque vous écrivez des règles de sécurité, les tests réussissent. Pour afficher les tests, ouvrez functions/test.js
dans votre éditeur.
Sur la ligne de commande, démarrez les émulateurs à l'aide de emulators:exec
et exécutez les tests :
$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test"
Faites défiler la page jusqu'en 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 neuf échecs. Lorsque vous créez le fichier de règles, vous pouvez mesurer la progression en observant davantage de tests réussis.
4. Créez des brouillons d'articles de blog.
Étant donné que l'accès aux articles de blog en brouillon est très différent de celui aux articles de blog publiés, cette application de bloggage stocke les articles de blog en brouillon dans une collection distincte, /drafts
. Seuls l'auteur ou un modérateur peuvent accéder aux brouillons, et les champs obligatoires et immuables sont validés.
Si vous ouvrez 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 de correspondance, match /{document=**}
, utilise la syntaxe **
pour s'appliquer de manière récursive à tous les documents des sous-collections. Et comme il se trouve au niveau supérieur, la même règle s'applique actuellement à toutes les requêtes, quel que soit l'utilisateur qui les envoie ou les données qu'il tente de lire ou d'écrire.
Commencez par supprimer l'instruction de correspondance la plus interne et remplacez-la par match /drafts/{draftID}
. (Les commentaires de 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 les utilisateurs autorisés à créer des 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 à l'origine de 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
. L'application doit donc l'ajouter avant de tenter de créer un document.) Comme vous n'avez besoin de vérifier que les attributs sont créés, vous pouvez vérifier que request.resource
dispose de toutes ces clés :
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
])
La dernière exigence pour créer un article de blog est que le titre ne doit pas comporter plus de 50 caractères :
request.resource.data.title.size() < 50
Étant donné que toutes ces conditions doivent être vraies, concaténez-les à l'aide de l'opérateur logique AND, &&
. 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 vérifiez que le premier test réussit.
5. modifier les brouillons d'articles de blog ;
Ensuite, à mesure que les auteurs affinent leurs brouillons d'articles de blog, ils modifient les brouillons de documents. Créez une règle définissant les conditions dans lesquelles un post peut être mis à jour. Tout d'abord, seul l'auteur peut mettre à jour ses brouillons. Notez que vous vérifiez ici l'UID déjà écrit,resource.data.authorUID
:
resource.data.authorUID == request.auth.uid
La deuxième condition pour une mise à jour est que les deux attributs authorUID
et createdAt
ne doivent pas changer:
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]);
Enfin, le titre ne doit pas dépasser 50 caractères:
request.resource.data.title.size() < 50;
Étant donné que toutes ces conditions doivent être remplies, concatenatez-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;
}
}
}
Relancez vos tests et vérifiez qu'un autre test réussit.
6. Supprimer et lire des brouillons : contrôle des accès basé sur les attributs
Tout comme les auteurs peuvent créer et mettre à jour des brouillons, ils peuvent également en supprimer.
resource.data.authorUID == request.auth.uid
De plus, les auteurs disposant d'un attribut isModerator
sur leur jeton d'authentification sont autorisés à supprimer les brouillons :
request.auth.token.isModerator == true
Étant donné que 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. Vous pouvez donc ajouter cette autorisation à la règle :
allow read, delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true
Le règlement complet est désormais le suivant:
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;
}
}
}
Relancez vos tests et vérifiez qu'un autre test réussit maintenant.
7. Lecture, création et suppression des posts publiés : dénormalisation pour différents schémas d'accès
Les modèles d'accès aux articles publiés et aux brouillons étant très différents, cette application dénormalise les articles en collections draft
et published
distinctes. Par exemple, les posts publiés peuvent être lus par tout le monde, mais ne peuvent pas être supprimés définitivement. Les brouillons, quant à eux, peuvent être supprimés, mais seuls l'auteur et les modérateurs peuvent les lire. Dans cette application, lorsqu'un utilisateur souhaite publier un brouillon d'article de blog, une fonction est déclenchée pour créer l'article publié.
Vous allez ensuite rédiger les règles concernant les posts publiés. Les règles les plus simples à rédiger sont les suivantes : les posts publiés peuvent être lus par n'importe qui, et ne peuvent être créés ni supprimés par quiconque. Ajoutez les règles suivantes :
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 ajoutant ces règles aux règles existantes, l'intégralité 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;
}
}
}
Exécutez à nouveau les tests et vérifiez qu'un autre test réussit.
8. Mettre à jour des posts publiés: fonctions personnalisées et variables locales
Les conditions pour mettre à jour un article publié sont les suivantes:
- seul l'auteur ou le modérateur peut le faire.
- il doit contenir tous les champs obligatoires.
Étant donné que vous avez déjà rédigé les conditions pour être auteur ou modérateur, vous pouvez les copier et les coller, mais avec le temps, cela peut devenir difficile à lire et à gérer. Vous allez plutôt créer une fonction personnalisée qui encapsule la logique d'un auteur ou d'un modérateur. Ensuite, vous l'appellerez à partir de plusieurs conditions.
Créer une fonction personnalisée
Au-dessus de l'instruction de correspondance pour les brouillons, créez une fonction appelée isAuthorOrModerator
qui utilise comme arguments un document de publication (fonctionne aussi bien pour les brouillons que pour les articles publiés) et l'objet auth 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 de retour. La nôtre renverra une valeur booléenne indiquant si l'une 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 modifier la règle pour les brouillons afin d'appeler cette fonction, en veillant à 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 maintenant écrire 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 post 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 afin d'appliquer les conditions suivantes pour la mise à jour des posts publiés:
// 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éer une fonction personnalisée par vous-même
Enfin, ajoutez une condition selon laquelle le titre doit comporter moins de 50 caractères. Comme il s'agit d'une logique réutilisée, vous pouvez le faire en créant une fonction titleIsUnder50Chars
. Avec cette nouvelle fonction, la condition de mise à jour d'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);
Le fichier de règles complet est le suivant :
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);
}
}
}
Réexécutez les tests. À ce stade, vous devriez avoir cinq tests réussis et quatre échecs.
9. Commentaires : Autorisations des sous-collections et du fournisseur de connexion
Les commentaires publiés autorisent les commentaires, qui sont stockés dans une sous-collection de l'article publié (/published/{postID}/comments/{commentID}
). Par défaut, les règles d'une collection ne s'appliquent pas aux sous-collections. Vous ne souhaitez pas que les mêmes règles que celles appliquées au document parent du post publié s'appliquent aux commentaires. Vous allez en créer d'autres.
Pour définir des règles permettant d'accéder aux commentaires, commencez par l'instruction de correspondance:
match /published/{postID}/comments/{commentID} {
// `authorUID`: string, required
// `comment`: string, < 500 characters, required
// `createdAt`: timestamp, required
// `editedAt`: timestamp, optional
Lecture des commentaires: anonyme
Pour cette application, seuls les utilisateurs ayant créé un compte permanent (et non les utilisateurs anonymes) peuvent lire les commentaires. Pour appliquer cette règle, recherchez l'attribut sign_in_provider
figurant sur chaque objet auth.token
:
allow read: if request.auth.token.firebase.sign_in_provider != "anonymous";
Réexécutez vos tests et vérifiez qu'un autre test réussit.
Créer des commentaires : vérifier une liste de blocage
Pour créer un commentaire, vous devez remplir trois conditions :
- un utilisateur doit disposer d'une adresse e-mail validée
- le commentaire doit comporter moins de 500 caractères ;
- ils ne peuvent pas figurer sur la liste des utilisateurs bannis, qui est stockée dans Firestore dans la collection
bannedUsers
. Prenez ces conditions une par une:
request.auth.token.email_verified == true
request.resource.data.comment.size() < 500
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
La dernière règle 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));
Le fichier de règles complet est désormais le suivant:
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));
}
}
}
Exécutez à nouveau les tests et assurez-vous qu'un autre test réussit.
10. Mettre à jour les commentaires : règles basées sur le temps
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 ce faire, utilisez le code temporel createdAt
.
Tout d'abord, pour déterminer que l'utilisateur est l'auteur:
request.auth.uid == resource.data.authorUID
Ensuite, vérifiez que le commentaire a été créé au cours de la dernière heure :
(request.time - resource.data.createdAt) < duration.value(1, 'h');
Si l'on les combine avec l'opérateur logique AND, la règle de mise à jour des commentaires devient la suivante:
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');
Exécutez à nouveau les tests et assurez-vous qu'un autre test réussit.
11. Suppression de commentaires: vérification de la propriété des parents
Les commentaires peuvent être supprimés par leur auteur, un modérateur ou l'auteur de l'article de blog.
Tout d'abord, comme la fonction d'assistance que vous avez ajoutée précédemment recherche un champ authorUID
qui peut exister dans un post ou un commentaire, vous pouvez la réutiliser 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, recherchez l'article dans Firestore à l'aide d'un get
:
request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID
Chacune de ces conditions étant 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.
L'intégralité du fichier de règles est la suivante :
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. Étapes suivantes
Félicitations ! Vous avez écrit les règles de sécurité qui ont permis de réussir tous les tests et de sécuriser l'application.
Voici quelques sujets associés à étudier:
- Article de blog : Comment réviser le code des règles de sécurité
- Atelier de programmation : découvrir le développement local first avec les émulateurs
- Vidéo : Configurer une CI pour les tests basés sur un émulateur à l'aide de GitHub Actions