Contrôler l'accès à des champs spécifiques

Cette page s'appuie sur les concepts de Structuration des règles de sécurité et d'écriture des conditions pour les règles de sécurité pour expliquer comment vous pouvez utiliser les règles de sécurité Cloud Firestore pour créer des règles qui permettent aux clients d'effectuer des opérations sur certains champs d'un document mais pas sur d'autres.

Il peut arriver que vous souhaitiez contrôler les modifications apportées à un document non pas au niveau du document mais au niveau du champ.

Par exemple, vous souhaiterez peut-être autoriser un client à créer ou modifier un document, mais ne pas lui permettre de modifier certains champs de ce document. Ou vous souhaiterez peut-être imposer que tout document créé par un client contienne toujours un certain ensemble de champs. Ce guide explique comment vous pouvez accomplir certaines de ces tâches à l'aide des règles de sécurité Cloud Firestore.

Autoriser l'accès en lecture uniquement pour des champs spécifiques

Les lectures dans Cloud Firestore sont effectuées au niveau du document. Soit vous récupérez le document complet, soit vous ne récupérez rien. Il n'existe aucun moyen de récupérer un document partiel. Il est impossible d’utiliser uniquement des règles de sécurité pour empêcher les utilisateurs de lire des champs spécifiques dans un document.

Si vous souhaitez cacher certains champs d'un document à certains utilisateurs, le meilleur moyen serait de les placer dans un document distinct. Par exemple, vous pourriez envisager de créer un document dans une sous-collection private comme ceci :

/employés/{emp_id}

  name: "Alice Hamilton",
  department: 461,
  start_date: <timestamp>

/employés/{emp_id}/private/finances

    salary: 80000,
    bonus_mult: 1.25,
    perf_review: 4.2

Vous pouvez ensuite ajouter des règles de sécurité ayant différents niveaux d'accès pour les deux collections. Dans cet exemple, nous utilisons des revendications d'authentification personnalisées pour indiquer que seuls les utilisateurs dotés du role de revendication d'authentification personnalisée égal à Finance peuvent afficher les informations financières d'un employé.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow any logged in user to view the public employee data
    match /employees/{emp_id} {
      allow read: if request.resource.auth != null
      // Allow only users with the custom auth claim of "Finance" to view
      // the employee's financial data
      match /private/finances {
        allow read: if request.resource.auth &&
          request.resource.auth.token.role == 'Finance'
      }
    }
  }
}

Restriction des champs lors de la création de documents

Cloud Firestore est sans schéma, ce qui signifie qu'il n'y a aucune restriction au niveau de la base de données quant aux champs qu'un document contient. Bien que cette flexibilité puisse faciliter le développement, vous souhaiterez parfois vous assurer que les clients ne peuvent créer que des documents contenant des champs spécifiques ou ne contiennent pas d'autres champs.

Vous pouvez créer ces règles en examinant la méthode keys de l'objet request.resource.data . Il s'agit d'une liste de tous les champs que le client tente d'écrire dans ce nouveau document. En combinant cet ensemble de champs avec des fonctions telles que hasOnly() ou hasAny() , vous pouvez ajouter une logique qui restreint les types de documents qu'un utilisateur peut ajouter à Cloud Firestore.

Exiger des champs spécifiques dans les nouveaux documents

Supposons que vous vouliez vous assurer que tous les documents créés dans une collection restaurant contiennent au moins un champ name , location et city . Vous pouvez le faire en appelant hasAll() sur la liste des clés du nouveau document.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document contains a name
    // location, and city field
    match /restaurant/{restId} {
      allow create: if request.resource.data.keys().hasAll(['name', 'location', 'city']);
    }
  }
}

Cela permet également de créer des restaurants avec d'autres champs, mais garantit que tous les documents créés par un client contiennent au moins ces trois champs.

Interdire des champs spécifiques dans les nouveaux documents

De même, vous pouvez empêcher les clients de créer des documents contenant des champs spécifiques en utilisant hasAny() sur une liste de champs interdits. Cette méthode est évaluée comme vraie si un document contient l'un de ces champs, vous souhaiterez donc probablement annuler le résultat afin d'interdire certains champs.

Par exemple, dans l'exemple suivant, les clients ne sont pas autorisés à créer un document contenant un champ average_score ou rating_count puisque ces champs seront ajoutés ultérieurement par un appel au serveur.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document does *not*
    // contain an average_score or rating_count field.
    match /restaurant/{restId} {
      allow create: if (!request.resource.data.keys().hasAny(
        ['average_score', 'rating_count']));
    }
  }
}

Création d'une liste autorisée de champs pour les nouveaux documents

Au lieu d'interdire certains champs dans les nouveaux documents, vous souhaiterez peut-être créer une liste contenant uniquement les champs explicitement autorisés dans les nouveaux documents. Ensuite, vous pouvez utiliser la fonction hasOnly() pour vous assurer que tous les nouveaux documents créés contiennent uniquement ces champs (ou un sous-ensemble de ces champs) et aucun autre.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document doesn't contain
    // any fields besides the ones listed below.
    match /restaurant/{restId} {
      allow create: if (request.resource.data.keys().hasOnly(
        ['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

Combiner les champs obligatoires et facultatifs

Vous pouvez combiner les opérations hasAll et hasOnly dans vos règles de sécurité pour exiger certains champs et en autoriser d’autres. Par exemple, cet exemple nécessite que tous les nouveaux documents contiennent les champs name , location et city , et autorise éventuellement les champs address , hours et cuisine .

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document has a name,
    // location, and city field, and optionally address, hours, or cuisine field
    match /restaurant/{restId} {
      allow create: if (request.resource.data.keys().hasAll(['name', 'location', 'city'])) &&
       (request.resource.data.keys().hasOnly(
           ['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

Dans un scénario réel, vous souhaiterez peut-être déplacer cette logique dans une fonction d'assistance pour éviter de dupliquer votre code et combiner plus facilement les champs facultatifs et obligatoires en une seule liste, comme ceci :

service cloud.firestore {
  match /databases/{database}/documents {
    function verifyFields(required, optional) {
      let allAllowedFields = required.concat(optional);
      return request.resource.data.keys().hasAll(required) &&
        request.resource.data.keys().hasOnly(allAllowedFields);
    }
    match /restaurant/{restId} {
      allow create: if verifyFields(['name', 'location', 'city'],
        ['address', 'hours', 'cuisine']);
    }
  }
}

Restriction des champs lors de la mise à jour

Une pratique de sécurité courante consiste à autoriser uniquement les clients à modifier certains champs et pas d’autres. Vous ne pouvez pas y parvenir uniquement en regardant la liste request.resource.data.keys() décrite dans la section précédente, puisque cette liste représente le document complet tel qu'il serait après la mise à jour, et inclurait donc des champs que le client n'a pas changement.

Cependant, si vous deviez utiliser la fonction diff() , vous pourriez comparer request.resource.data avec l'objet resource.data , qui représente le document dans la base de données avant la mise à jour. Cela crée un objet mapDiff , qui est un objet contenant toutes les modifications entre deux cartes différentes.

En appelant la méthode affectedKeys() sur ce mapDiff, vous pouvez obtenir un ensemble de champs qui ont été modifiés lors d'une modification. Ensuite, vous pouvez utiliser des fonctions comme hasOnly() ou hasAny() pour vous assurer que cet ensemble contient (ou non) certains éléments.

Empêcher la modification de certains champs

En utilisant la méthode hasAny() sur l'ensemble généré par affectedKeys() , puis en annulant le résultat, vous pouvez rejeter toute demande client qui tente de modifier des champs que vous ne souhaitez pas modifier.

Par exemple, vous souhaiterez peut-être autoriser les clients à mettre à jour les informations sur un restaurant, mais pas à modifier leur note moyenne ou le nombre d'avis.

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
      // Allow the client to update a document only if that document doesn't
      // change the average_score or rating_count fields
      allow update: if (!request.resource.data.diff(resource.data).affectedKeys()
        .hasAny(['average_score', 'rating_count']));
    }
  }
}

Autoriser uniquement la modification de certains champs

Plutôt que de spécifier les champs que vous ne souhaitez pas modifier, vous pouvez également utiliser la fonction hasOnly() pour spécifier une liste de champs que vous souhaitez modifier. Ceci est généralement considéré comme plus sécurisé car les écritures dans les nouveaux champs de document sont interdites par défaut jusqu'à ce que vous les autorisiez explicitement dans vos règles de sécurité.

Par exemple, plutôt que d'interdire les champs average_score et rating_count , vous pouvez créer des règles de sécurité qui permettent aux clients de modifier uniquement les champs name , location , city , address , hours et cuisine .

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
    // Allow a client to update only these 6 fields in a document
      allow update: if (request.resource.data.diff(resource.data).affectedKeys()
        .hasOnly(['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

Cela signifie que si, dans une future itération de votre application, les documents du restaurant incluent un champ telephone , les tentatives de modification de ce champ échoueront jusqu'à ce que vous reveniez en arrière et ajoutiez ce champ à la liste hasOnly() dans vos règles de sécurité.

Application des types de champs

Un autre effet du fait que Cloud Firestore est sans schéma est qu'il n'y a aucune application au niveau de la base de données concernant les types de données qui peuvent être stockés dans des champs spécifiques. C'est cependant quelque chose que vous pouvez appliquer dans les règles de sécurité, avec l'opérateur is .

Par exemple, la règle de sécurité suivante impose que le champ score d'un avis soit un nombre entier, que les champs headline , content et author_name soient des chaînes et que review_date soit un horodatage.

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
      // Restaurant rules go here...
      match /review/{reviewId} {
        allow create: if (request.resource.data.score is int &&
          request.resource.data.headline is string &&
          request.resource.data.content is string &&
          request.resource.data.author_name is string &&
          request.resource.data.review_date is timestamp
        );
      }
    }
  }
}

Les types de données valides pour l'opérateur is sont bool , bytes , float , int , list , latlng , number , path , map , string et timestamp . L'opérateur is prend également en charge les types de données constraint , duration , set et map_diff , mais comme ceux-ci sont générés par le langage des règles de sécurité lui-même et non générés par les clients, vous les utilisez rarement dans la plupart des applications pratiques.

Les types de données list et map ne prennent pas en charge les génériques ou les arguments de type. En d’autres termes, vous pouvez utiliser des règles de sécurité pour imposer qu’un certain champ contienne une liste ou une carte, mais vous ne pouvez pas imposer qu’un champ contienne une liste de tous les entiers ou de toutes les chaînes.

De même, vous pouvez utiliser des règles de sécurité pour appliquer des valeurs de type pour des entrées spécifiques dans une liste ou une carte (en utilisant respectivement la notation entre crochets ou les noms de clé), mais il n'existe pas de raccourci pour appliquer les types de données de tous les membres d'une carte ou d'une liste à une fois.

Par exemple, les règles suivantes garantissent qu'un champ tags dans un document contient une liste et que la première entrée est une chaîne. Cela garantit également que le champ product contient une carte qui contient à son tour un nom de produit sous forme de chaîne et une quantité sous forme d'entier.

service cloud.firestore {
  match /databases/{database}/documents {
  match /orders/{orderId} {
    allow create: if request.resource.data.tags is list &&
      request.resource.data.tags[0] is string &&
      request.resource.data.product is map &&
      request.resource.data.product.name is string &&
      request.resource.data.product.quantity is int
      }
    }
  }
}

Les types de champs doivent être appliqués lors de la création et de la mise à jour d'un document. Par conséquent, vous souhaiterez peut-être envisager de créer une fonction d’assistance que vous pourrez appeler à la fois dans les sections de création et de mise à jour de vos règles de sécurité.

service cloud.firestore {
  match /databases/{database}/documents {

  function reviewFieldsAreValidTypes(docData) {
     return docData.score is int &&
          docData.headline is string &&
          docData.content is string &&
          docData.author_name is string &&
          docData.review_date is timestamp;
  }

   match /restaurant/{restId} {
      // Restaurant rules go here...
      match /review/{reviewId} {
        allow create: if reviewFieldsAreValidTypes(request.resource.data) &&
          // Other rules may go here
        allow update: if reviewFieldsAreValidTypes(request.resource.data) &&
          // Other rules may go here
      }
    }
  }
}

Application des types pour les champs facultatifs

Il est important de se rappeler que l'appel request.resource.data.foo sur un document où foo n'existe pas entraîne une erreur et que, par conséquent, toute règle de sécurité effectuant cet appel refusera la demande. Vous pouvez gérer cette situation en utilisant la méthode get sur request.resource.data . La méthode get vous permet de fournir un argument par défaut pour le champ que vous récupérez d'une carte si ce champ n'existe pas.

Par exemple, si les documents de révision contiennent également un champ photo_url facultatif et un champ tags facultatif que vous souhaitez vérifier respectivement sont des chaînes et des listes, vous pouvez y parvenir en réécrivant la fonction reviewFieldsAreValidTypes comme suit :

  function reviewFieldsAreValidTypes(docData) {
     return docData.score is int &&
          docData.headline is string &&
          docData.content is string &&
          docData.author_name is string &&
          docData.review_date is timestamp &&
          docData.get('photo_url', '') is string &&
          docData.get('tags', []) is list;
  }

Cela rejette les documents pour lesquels tags existent, mais ne constituent pas une liste, tout en autorisant les documents qui ne contiennent pas de champ tags (ou photo_url ).

Les écritures partielles ne sont jamais autorisées

Une dernière remarque concernant les règles de sécurité Cloud Firestore est qu'elles permettent soit au client d'apporter une modification à un document, soit de rejeter l'intégralité de la modification. Vous ne pouvez pas créer de règles de sécurité qui acceptent les écritures dans certains champs de votre document tout en en rejetant d'autres au cours de la même opération.