Pair-Roulette

moon indicating dark mode
sun indicating light mode

Protégez vos données avec assurance dans Firestore

October 07, 2019

Firestore en quelques mots

Firestore est une base de données NoSQL évolutive pour le développement d’applications mobile, web et serveur.

Protéger ses données

Firestore vous offre la possibilité de lire et écrire dans votre base de données directement depuis le client. Ce mode de fonctionnement est assez simple et rapide à utiliser pour une personne qui souhaiterait développer une application sans avoir à développer un serveur.

Pour profiter de cette liberté vous devrez écrire des règles de sécurité. Ces règles définissent les accès en lecture et écriture pour les différents documents à stocker.

⚠️ La sécurisation des données pour une utilisation côté serveur diffère de celle présentée dans cet article.

Analysons ensemble les règles mises en place pour la gestion des profils dans Pair-Roulette :

rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function itsMyProfile() { return resource.data.uid == request.auth.uid }
function itsMe() { return request.resource.data.uid == request.auth.uid }
function userNotExists() { return !exists(/databases/$(database)/documents/users/$(request.auth.uid)) }
match /users/{userId} {
allow read: if itsMyProfile();
allow update: if itsMyProfile() && request.resource.data.modifiedAt == request.time;
allow create: if itsMe() && userNotExists() && request.resource.data.createdAt == request.time;
}
}
}
  • Toutes les opérations de lecture et écriture ne sont possibles que par des personnes authentifiées dans l’application.
    Le code suivant permet de lire l’identifiant du client connecté : request.auth.uid.

  • La lecture d’un profil est possible uniquement par son propriétaire.
    Nous comparons l’identifiant du client à celui du profil avec le code suivant resource.data.uid == request.auth.uid.

  • La mise à jour d’un profil est possible uniquement par son propriétaire.
    Nous nous assurons aussi de la présence de la propriété modifiedAt pour des raisons fonctionnelles et non de sécurité.

  • La création d’un profil est possible uniquement si celui-ci n’existe pas et si la personne authentifiée correspond bien au profil à créer.
    Nous nous assurons aussi de la présence de la propriété createdAt pour des raisons fonctionnelles et non de sécurité.

Assurer la sécurité de ses données

Ces règles étant maintenant en place, comment vérifier qu’elles fonctionnent comme prévu ?

Pour répondre à cette problématique je vous propose l’utilisation de Firebase emulators. Cette suite logicielle vous offre la possibilité d’émuler un service Firebase sur votre machine.

Nul besoin de :

  • créer une base de données de test
  • créer un compte de test
  • effectuer les tests en ligne
  • dépasser les quotas et faire des dépenses supplémentaires

⚠️ L’utilisation de ce service nécessite de créer un client en utilisant @firebase/testing.

Analysons ensemble comment les règles de sécurité sont vérifiées chez Pair-Roulette.

  1. Démarrons le service Firestore avec la commande suivante :
firebase emulators:start --only firestore
  1. Chargeons les règles de sécurité au démarrage de la suite de tests :
const firebase = require('@firebase/testing')
const path = require('path')
const fs = require('fs')
const util = require('util')
const readFileAsync = util.promisify(fs.readFile)
const projectId = 'pair-roulette'
describe('Firestore rules', () => {
beforeAll(async () => {
const rulesPath = path.join(__dirname, 'firestore.rules')
const rules = await readFileAsync(rulesPath, 'utf8')
await firebase.loadFirestoreRules({ projectId, rules })
})
// ...
})
  1. Nettoyons notre base de données avant chaque test afin d’éviter tout effet de bord :
// beforeAll ...
beforeEach(async () => {
await firebase.clearFirestoreData({ projectId })
})
  1. Clôturons toutes les applications créées une fois notre suite de tests terminée :
// beforeEach ...
afterAll(async () => {
await Promise.all(firebase.apps().map(app => app.delete()))
const coverageUrl = `http://localhost:8080/emulator/v1/projects/${projectId}:ruleCoverage.html`
console.log(`View rule coverage information at ${coverageUrl}\n`)
})
  1. Testons la première règle :

Toutes les opérations de lecture et écriture ne sont possibles que par des personnes authentifiées dans l’application.

Le code suivant crée un client anonyme et tente de créer le profil alice.
Nous vérifions que la création échoue.

// afterAll
it('require users to log in before creating a profile', async () => {
const anonymous = authedApp(null)
const profile = createProfile(anonymous, 'alice')
await firebase.assertFails(profile)
})
  1. Testons la deuxième règle :

La lecture d’un profil est possible uniquement par son propriétaire.

Le code suivant crée deux clients authentifiés : alice et bob.
Alice crée son compte puis Bob essaie de lire le profil d’Alice.
Nous vérifions que cette dernière action échoue.

it('should only allow owner to read document', async () => {
const alice = authedApp({ uid: 'alice' })
const bob = authedApp({ uid: 'bob' })
await createProfile(alice, 'alice')
await firebase.assertFails(getProfile(bob, 'alice'))
})

Et voilà ! Vous avez assuré la protection de vos données (en partie, car les tests ci-dessus ne sont pas exhaustifs).

Vous pouvez retrouver le code en entier en suivant ce lien.

La réalisation des règles de sécurité est disponible en re-diffusion sur YouTube.