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

Cette page s'appuie sur les concepts décrits dans les sections Structurer les règles de sécurité et Écrire des conditions pour les règles de sécurité pour expliquer comment utiliser les règles de sécurité Firestore afin de créer des règles permettant 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 au niveau du document, mais au niveau du champ.

Par exemple, vous pouvez autoriser un client à créer ou à modifier un document, mais l'empêcher de modifier certains champs de ce document. Vous pouvez également exiger que tous les documents créés par un client contiennent toujours un ensemble spécifique de champs. Ce guide explique comment réaliser certaines de ces tâches à l'aide des règles de sécurité Firestore.

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

Les lectures dans Firestore sont effectuées au niveau du document. Vous pouvez récupérer le document dans son intégralité ou pas du tout. 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 masquer certains champs d'un document pour certains utilisateurs, il est préférable de les placer dans un document distinct. Par exemple, vous pouvez envisager de créer un document dans une sous-collection private comme suit :

/employees/{emp_id}

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

/employees/{emp_id}/private/finances

    salary: 80000,
    bonus_mult: 1.25,
    perf_review: 4.2

Vous pouvez ensuite ajouter des règles de sécurité qui présentent différents niveaux d'accès pour les deux collections. Dans cet exemple, nous utilisons des revendications d'authentification personnalisées pour spécifier que seuls les utilisateurs dont la revendication d'authentification personnalisée role est égale à 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'
      }
    }
  }
}

Restreindre les champs lors de la création de documents

Firestore étant une base de données sans schéma, il n'existe pas de restrictions au niveau de la base de données pour les champs qu'un document contient. Bien que cette flexibilité puisse faciliter le développement, il peut arriver que vous souhaitiez vous assurer que les clients peuvent créer uniquement des documents contenant des champs spécifiques ou ne contenant 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 de la 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 limite le types de documents qu'un utilisateur peut ajouter à Firestore.

Exiger des champs spécifiques dans de nouveaux documents

Supposons que vous souhaitiez vous assurer que tous les documents créés dans une collection restaurant contiennent au moins un champ name, location et city. Pour ce faire, vous pouvez appeler 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 signifie que des restaurants peuvent être créés avec d'autres champs, mais cela garantit également que tous les documents créés par un client contiennent au moins ces trois champs.

Interdire des champs spécifiques dans de 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 renvoie la valeur "true" si un document contient l'un de ces champs. Vous souhaiterez donc probablement inverser le résultat afin d'empêcher 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, car ces champs seront ajoutés ultérieurement par un appel de 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éer une liste d'autorisation de champs pour les nouveaux documents

Au lieu d'interdire certains champs dans les nouveaux documents, vous pouvez créer une liste ne contenant que les champs explicitement autorisés dans les nouveaux documents. Vous pouvez ensuite utiliser la fonction hasOnly() pour vous assurer que tous les documents créés ne contiennent que 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 des champs obligatoires et facultatifs

Vous pouvez combiner les opérations hasAll et hasOnly dans vos règles de sécurité pour exiger des champs et en autoriser d'autres. Par exemple, cet exemple exige que tous les nouveaux documents contiennent les champs name, location et city, et autorisent é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 pouvez déplacer cette logique dans une fonction d'assistance pour éviter la duplication de votre code, et combiner plus facilement les champs facultatifs et obligatoires en une seule liste, comme suit :

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

Restreindre les champs lors de la mise à jour

Une pratique courante en matière de sécurité consiste à autoriser uniquement les clients à modifier certains champs, et non d'autres. Vous ne pouvez pas réaliser cette opération uniquement en consultant la liste request.resource.data.keys() décrite dans la section précédente, car celle-ci représente l'intégralité du document telle qu'il se présente après la mise à jour. Elle inclut donc des champs que le client n'a pas modifiés.

Toutefois, si vous utilisez la fonction diff(), vous pouvez comparer request.resource.data avec l'objet resource.data, ce qui représente le document dans la base de données avant la mise à jour. Cela crée un objet mapDiff contenant toutes les modifications entre deux mappages différents.

En appelant la méthode affectedKeys() sur cet objet mapDiff, vous pouvez obtenir un ensemble de champs modifiés lors d'une mise à jour. Vous pouvez ensuite utiliser des fonctions telles que 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() de l'ensemble généré par affectedKeys(), puis en inversant le résultat, vous pouvez refuser toute requête client qui tente de modifier les champs que vous ne souhaitez pas voir modifier.

Par exemple, vous pouvez autoriser les clients à mettre à jour les informations concernant un restaurant, mais les empêcher de modifier leur score moyen 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 la modification de certains champs uniquement

Plutôt que de spécifier des champs que vous ne souhaitez pas voir modifier, vous pouvez également utiliser la fonction hasOnly() pour spécifier une liste de champs qui peuvent être modifiés. Cette action est généralement considérée comme plus sécurisée, car les écritures dans les champs de nouveaux documents 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é permettant aux clients de ne modifier que 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, lors d'une itération ultérieure de votre application, les documents de restaurant contiennent un champ telephone, toute tentative de modification de ce champ échouera jusqu'à ce que vous décidiez d'ajouter le champ à la classe hasOnly() dans vos règles de sécurité.

Appliquer des types de champs

Un autre effet lié au fait que Firestore n'obéit à aucun schéma est qu'il n'existe aucune application forcée au niveau de la base de données pour les types de données pouvant être stockés dans des champs spécifiques. Toutefois, vous pouvez appliquer cette règle dans les règles de sécurité à l'aide de l'opérateur is.

Par exemple, la règle de sécurité suivante exige que le champ score d'un avis soit un entier, les champs headline, content et author_name des chaînes, et review_date 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 est également compatible avec les types de données constraint, duration, set et map_diff, mais ceux-ci étant générés par le langage des règles de sécurité, et non par les clients, vous les utilisez rarement dans la plupart des applications pratiques.

Les types de données list et map ne sont pas compatibles avec les caractères génériques, ni avec les arguments de type. En d'autres termes, vous pouvez utiliser des règles de sécurité pour faire en sorte qu'un champ contienne une liste ou un mappage, mais vous ne pouvez pas exiger 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 types à des entrées spécifiques d'une liste ou d'un mappage (en utilisant respectivement la notation entre crochets ou les noms de clés). Cependant, il n'existe pas de raccourci pour appliquer les types de données de tous les membres dans un mappage ou une liste à la fois.

Par exemple, les règles suivantes garantissent que le champ tags d'un document contient une liste et que la première entrée est une chaîne. Cela garantit également que le champ product contient un mappage qui, à son tour, contient un nom de produit qui est une 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 champ doivent être appliqués lors de la création et de la mise à jour d'un document. Par conséquent, vous pouvez envisager de créer une fonction d'assistance que vous pouvez appeler 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
      }
    }
  }
}

Appliquer des types à des champs facultatifs

Il est important de rappeler que l'appel de request.resource.data.foo sur un document où foo n'existe pas génère une erreur. Par conséquent, toute règle de sécurité effectuant cet appel refuse la requête. Vous pouvez gérer cette situation à l'aide de 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 à partir d'un mappage si ce champ n'existe pas.

Par exemple, si les documents d'avis contiennent également les champs facultatifs photo_url et tags que vous souhaitez valider et qui se présentent sous forme de chaînes et de listes, vous pouvez réécrire la fonction reviewFieldsAreValidTypes en utilisant une chaîne semblable à celle-ci :

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

Cette opération rejette les documents dans lesquels tags existe, mais qui n'est pas une liste, tout en continuant à autoriser les documents qui ne contiennent pas de champ tags (ou photo_url).

Les écritures partielles ne sont jamais autorisées

Dernière remarque sur les règles de sécurité Firestore : elles permettent soit au client d'apporter des modifications à un document, soit de rejeter l'ensemble 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 refusant d'autres instances de la même opération.