1. Avant de commencer
Cloud Firestore, Cloud Storage for Firebase et Realtime Database s'appuient sur des fichiers de configuration que vous écrivez pour accorder l'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'un des aspects les plus importants du développement de votre application. Cet atelier de programmation vous guidera tout au long du processus.
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 ; pour vérifier votre version, exécutez
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 refusent l'accès comme prévu.
Vous allez apprendre à effectuer les opérations suivantes :
- Accorder des autorisations précises
- Appliquer des validations de données et de types
- 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é basées sur le temps
- Implémenter une liste de refus et des suppressions réversibles
- Comprendre quand dénormaliser les données pour répondre à plusieurs modèles d'accès
2. Configurer
Il s'agit d'une application de blogging. Voici un résumé général des fonctionnalités de l'application :
Rédiger des articles de blog :
- Les utilisateurs peuvent créer des brouillons d'articles de blog, qui sont stockés dans la collection
drafts
. - L'auteur peut continuer à modifier 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 pour créer 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, uniquement via une fonction.
- Ils ne peuvent être supprimés de façon réversible, ce qui met à jour un attribut
visible
sur "false".
Commentaires
- Les posts publiés autorisent les commentaires, qui sont une sous-collection de chaque post publié.
- Pour limiter les utilisations abusives, les utilisateurs doivent disposer d'une adresse e-mail validée et ne pas figurer sur une liste de refus pour pouvoir laisser un commentaire.
- Vous ne pouvez modifier un commentaire qu'une heure après sa 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 les champs obligatoires et les validations de données.
Tout se passera en local, à l'aide de la suite d'émulateurs Firebase.
Obtenir le code source
Dans cet atelier de programmation, vous allez commencer par tester les règles de sécurité, mais avec des règles de sécurité minimales. 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 travaillerez 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 votre connexion Internet est lente, cette 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 (interface de ligne de commande) Firebase, qui peut être installée sur votre machine à l'aide de la commande suivante :
$ npm install -g firebase-tools
Ensuite, vérifiez 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 l'Emulator Suite.
Démarrer les émulateurs
L'application avec laquelle vous allez travailler comporte trois collections Firestore principales : drafts
contient les articles de blog en cours, la collection published
contient les articles de blog publiés et comments
est une sous-collection des 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 allez écrire les règles de sécurité pour que ces tests réussissent.
Pour commencer, votre base de données est verrouillée : les accès en lecture et en écriture sont refusés à tous les utilisateurs, et tous les tests échouent. À mesure que 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 :
$ 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 ...
Pour le moment, il y a neuf échecs. À mesure que vous créez le fichier de règles, vous pouvez mesurer votre progression en observant le nombre de tests réussis.
4. créer 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 blog stocke les articles de blog en brouillon dans une collection distincte, /drafts
. Seuls l'auteur ou un modérateur peuvent accéder aux brouillons, qui sont soumis à 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 de correspondance match /{document=**}
utilise la syntaxe **
pour s'appliquer de manière récursive à tous les documents des sous-collections. Comme il s'agit d'une règle de premier niveau, la même règle générale s'applique actuellement à toutes les requêtes, quels que soient l'auteur de la requête et les données qu'il tente de lire ou d'écrire.
Commencez par supprimer l'instruction de correspondance la plus intérieure 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 allez écrire 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 qui envoie la demande est identique à celui indiqué dans le document.
La première condition de la création sera la suivante :
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 permet de s'assurer que l'application doit l'ajouter avant de tenter de créer un document.) Comme vous n'avez besoin que de vérifier que les attributs sont créés, vous pouvez vérifier que request.resource
possède toutes ces clés :
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
])
Enfin, le titre de l'article de blog ne doit pas comporter plus de 50 caractères :
request.resource.data.title.size() < 50
Comme toutes ces conditions doivent être vraies, concaténez-les avec 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 des brouillons d'articles de blog
Ensuite, les auteurs affinent leurs brouillons d'articles de blog en les modifiant. Créez une règle pour les conditions dans lesquelles un post peut être modifié. Tout d'abord, seul l'auteur peut modifier ses brouillons. Notez que vous devez vérifier l'UID déjà écrit,resource.data.authorUID
:
resource.data.authorUID == request.auth.uid
La deuxième exigence 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"
]);
Enfin, le titre ne doit pas comporter plus de 50 caractères :
request.resource.data.title.size() < 50;
Comme toutes ces conditions doivent ê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 vérifiez qu'un autre test réussit.
6. Supprimer et lire les brouillons : contrôle des accès basé sur les attributs
Tout comme les auteurs peuvent créer et modifier des brouillons, ils peuvent également les supprimer.
resource.data.authorUID == request.auth.uid
De plus, les auteurs dont le jeton d'authentification comporte un attribut isModerator
sont autorisés à supprimer des brouillons :
request.auth.token.isModerator == true
Étant donné que l'une ou l'autre de ces conditions suffit pour une suppression, concaténez-les avec un opérateur logique OR, ||
:
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
Les règles complètes sont désormais les suivantes :
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 vérifiez qu'un autre test réussit désormais.
7. Lecture, création et suppression de posts publiés : dénormalisation pour différents modèles d'accès
Étant donné que les modèles d'accès pour les posts publiés et les brouillons sont très différents, cette application dénormalise les posts dans des collections draft
et published
distinctes. Par exemple, les posts publiés peuvent être lus par tous les utilisateurs, mais ne peuvent pas être supprimés définitivement. Les brouillons, quant à eux, 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 pour créer le nouvel article publié.
Vous allez ensuite écrire les règles pour les posts publiés. Les règles les plus simples à écrire sont que les posts publiés peuvent être lus par n'importe qui, mais ne peuvent être ni créés ni supprimés par n'importe qui. 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 à celles existantes, le fichier de règles complet 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 vérifiez qu'un autre test réussit.
8. Mettre à jour les posts publiés : fonctions personnalisées et variables locales
Voici les conditions à remplir pour modifier un post publié :
- seule l'auteur ou le modérateur peuvent le faire ;
- il doit contenir tous les champs obligatoires.
Comme vous avez déjà écrit des conditions pour être auteur ou modérateur, vous pouvez les copier et les coller. Toutefois, au fil du temps, cela peut devenir difficile à lire et à gérer. Au lieu de cela, vous allez créer une fonction personnalisée qui encapsule la logique pour être un auteur ou un modérateur. Vous l'appellerez ensuite à 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 prend comme arguments un document de post (cela fonctionnera pour les brouillons ou les posts publiés) 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 de retour. La nôtre renverra une valeur booléenne indiquant si l'une des variables est définie sur "true" :
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 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 désormais écrire une condition pour mettre à jour les posts 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. Plus précisément, les champs url
, authorUID
et publishedAt
sont immuables. Les deux autres champs, title
et content
, ainsi que visible
doivent toujours être présents après une mise à jour. Ajoutez des conditions pour appliquer ces exigences aux modifications apportées aux 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 vous-même une fonction personnalisée
Enfin, ajoutez une condition pour que le titre comporte 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 la nouvelle fonction, la condition pour mettre à jour un post 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);
}
}
}
Relancez les tests. À ce stade, vous devriez avoir cinq tests réussis et quatre tests échoués.
9. Commentaires : sous-collections et autorisations du fournisseur de connexion
Les posts publiés autorisent les commentaires, qui sont stockés dans une sous-collection du post 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 s'appliquent aux commentaires qu'au document parent du post publié. Vous allez donc en créer d'autres.
Pour écrire des règles d'accès 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 : impossible d'être anonyme
Pour cette application, seuls les utilisateurs qui ont 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 vérifiez qu'un test supplémentaire réussit.
Créer des commentaires : vérifier une liste de refus
Pour pouvoir 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 doivent pas figurer sur une liste d'utilisateurs bannis, stockée dans Firestore dans la collection
bannedUsers
. Examinons 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 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));
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));
}
}
}
Réexécutez les tests et assurez-vous qu'un test supplémentaire réussit.
10. Modifier les commentaires : règles basées sur le temps
La logique métier des commentaires est qu'ils peuvent être modifiés par leur auteur pendant une heure après leur création. Pour ce faire, utilisez le code temporel 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 combinant ces éléments avec l'opérateur logique AND, 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 test supplémentaire réussit.
11. Suppression de commentaires : vérification de la propriété parentale
Les commentaires peuvent être supprimés par leur auteur, par un modérateur ou par l'auteur de l'article de blog.
Tout d'abord, étant donné que la fonction d'assistance que vous avez ajoutée précédemment recherche un champ authorUID
qui peut exister sur un post 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 un get
pour rechercher l'article dans Firestore :
request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID
Comme l'une ou l'autre de ces conditions suffit, utilisez un opérateur logique OR 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 test supplémentaire 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 connexes à explorer :
- Article de blog : "Comment réviser le code des règles de sécurité"
- Atelier de programmation : découvrez le développement "local first" avec les émulateurs
- Vidéo : Configurer l'intégration continue pour les tests basés sur l'émulateur à l'aide de GitHub Actions