Controllare l'accesso a campi specifici

Questa pagina si basa sui concetti descritti in Strutturazione delle regole di sicurezza e Scrittura delle condizioni per le regole di sicurezza per spiegare come utilizzare le regole di sicurezza di Cloud Firestore per creare regole che consentano ai client di eseguire operazioni su alcuni campi in un documento ma non su altri.

Potrebbero esserci momenti in cui desideri controllare le modifiche a un documento non a livello di documento ma a livello di campo.

Ad esempio, potresti voler consentire a un cliente di creare o modificare un documento, ma non consentirgli di modificare determinati campi in quel documento. Oppure potresti voler imporre che qualsiasi documento creato da un cliente contenga sempre un determinato insieme di campi. Questa guida illustra come eseguire alcune di queste attività utilizzando le regole di sicurezza di Cloud Firestore.

Consentire l'accesso in lettura solo per campi specifici

Le letture in Cloud Firestore vengono eseguite a livello di documento. O recuperi il documento completo oppure non recuperi nulla. Non è possibile recuperare un documento parziale. È impossibile utilizzare solo le regole di sicurezza per impedire agli utenti di leggere campi specifici all'interno di un documento.

Se all'interno di un documento sono presenti determinati campi che desideri tenere nascosti ad alcuni utenti, il modo migliore sarebbe inserirli in un documento separato. Ad esempio, potresti prendere in considerazione la creazione di un documento in una sottoraccolta private in questo modo:

/dipendenti/{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

Successivamente puoi aggiungere regole di sicurezza con livelli di accesso diversi per le due raccolte. In questo esempio utilizziamo le attestazioni di autenticazione personalizzata per indicare che solo gli utenti con il role di attestazione di autenticazione personalizzata uguale a Finance possono visualizzare le informazioni finanziarie di un dipendente.

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

Limitare i campi sulla creazione del documento

Cloud Firestore è senza schema, il che significa che non esistono restrizioni a livello di database per i campi contenuti in un documento. Sebbene questa flessibilità possa semplificare lo sviluppo, in alcuni casi vorrai assicurarti che i clienti possano creare solo documenti che contengono campi specifici o che non contengano altri campi.

Puoi creare queste regole esaminando il metodo keys dell'oggetto request.resource.data . Questo è un elenco di tutti i campi che il client sta tentando di scrivere in questo nuovo documento. Combinando questo insieme di campi con funzioni come hasOnly() o hasAny() , puoi aggiungere una logica che limita i tipi di documenti che un utente può aggiungere a Cloud Firestore.

Richiedere campi specifici nei nuovi documenti

Supponiamo che tu voglia assicurarti che tutti i documenti creati in una raccolta restaurant contengano almeno un campo name , location e city . Potresti farlo chiamando hasAll() nell'elenco delle chiavi nel nuovo documento.

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

Ciò consente di creare ristoranti anche con altri campi, ma garantisce che tutti i documenti creati da un cliente contengano almeno questi tre campi.

Vietare campi specifici nei nuovi documenti

Allo stesso modo, puoi impedire ai client di creare documenti che contengono campi specifici utilizzando hasAny() contro un elenco di campi vietati. Questo metodo restituisce true se un documento contiene uno di questi campi, quindi probabilmente vorrai negare il risultato per vietare determinati campi.

Ad esempio, nell'esempio seguente, ai client non è consentito creare un documento che contenga un campo average_score o rating_count poiché questi campi verranno aggiunti da una chiamata al server in un secondo momento.

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

Creazione di una lista consentita di campi per nuovi documenti

Invece di vietare determinati campi nei nuovi documenti, potresti voler creare un elenco dei soli campi esplicitamente consentiti nei nuovi documenti. Quindi puoi utilizzare la funzione hasOnly() per assicurarti che tutti i nuovi documenti creati contengano solo questi campi (o un sottoinsieme di questi campi) e nessun altro.

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

Combinazione di campi obbligatori e facoltativi

Puoi combinare insieme le operazioni hasAll e hasOnly nelle regole di sicurezza per richiedere alcuni campi e consentirne altri. Ad esempio, questo esempio richiede che tutti i nuovi documenti contengano i campi name , location e city e facoltativamente consentano i campi address , hours e 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']));
    }
  }
}

In uno scenario reale, potresti voler spostare questa logica in una funzione di supporto per evitare di duplicare il codice e combinare più facilmente i campi facoltativi e obbligatori in un unico elenco, in questo modo:

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

Limitazione dei campi durante l'aggiornamento

Una pratica di sicurezza comune consiste nel consentire ai client di modificare solo alcuni campi e non altri. Non è possibile ottenere ciò esclusivamente esaminando l'elenco request.resource.data.keys() descritto nella sezione precedente, poiché questo elenco rappresenta il documento completo così come si occuperebbe dell'aggiornamento e quindi includerebbe campi che il client non ha modifica.

Tuttavia, se dovessi utilizzare la funzione diff() , potresti confrontare request.resource.data con l'oggetto resource.data , che rappresenta il documento nel database prima dell'aggiornamento. Questo crea un oggetto mapDiff , che è un oggetto contenente tutte le modifiche tra due mappe diverse.

Chiamando il metodo affectedKeys() su questo mapDiff, puoi ottenere una serie di campi che sono stati modificati in una modifica. Quindi puoi utilizzare funzioni come hasOnly() o hasAny() per assicurarti che questo set contenga (o meno) determinati elementi.

Impedire la modifica di alcuni campi

Utilizzando il metodo hasAny() sul set generato da affectedKeys() e quindi negando il risultato, puoi rifiutare qualsiasi richiesta del client che tenta di modificare i campi che non desideri vengano modificati.

Ad esempio, potresti consentire ai clienti di aggiornare le informazioni su un ristorante ma non modificare il punteggio medio o il numero di recensioni.

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

Consentire la modifica solo di alcuni campi

Invece di specificare i campi che non desideri modificare, puoi anche utilizzare la funzione hasOnly() per specificare un elenco di campi che desideri modificare. Questo è generalmente considerato più sicuro perché le scritture su qualsiasi nuovo campo del documento non sono consentite per impostazione predefinita finché non le consenti esplicitamente nelle regole di sicurezza.

Ad esempio, invece di vietare i campi average_score e rating_count , potresti creare regole di sicurezza che consentano ai clienti di modificare solo i campi name , location , city , address , hours e 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']));
    }
  }
}

Ciò significa che se, in qualche iterazione futura della tua app, i documenti del ristorante includono un campo telephone , i tentativi di modificare quel campo fallirebbero finché non torni indietro e aggiungi quel campo all'elenco hasOnly() nelle tue regole di sicurezza.

Applicazione dei tipi di campo

Un altro effetto del fatto che Cloud Firestore è senza schema è che non vi è alcuna applicazione a livello di database per quali tipi di dati possono essere archiviati in campi specifici. Questo è qualcosa che puoi imporre nelle regole di sicurezza, tuttavia, con l'operatore is .

Ad esempio, la seguente regola di sicurezza impone che il campo score di una recensione debba essere un numero intero, i campi headline , content e author_name siano stringhe e review_date sia un timestamp.

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

I tipi di dati validi per l'operatore is sono bool , bytes , float , int , list , latlng , number , path , map , string e timestamp . L'operatore is supporta anche i tipi di dati constraint , duration , set e map_diff , ma poiché questi sono generati dal linguaggio delle regole di sicurezza stesso e non generati dai client, li usi raramente nella maggior parte delle applicazioni pratiche.

I tipi di dati list e map non supportano i generici o gli argomenti di tipo. In altre parole, è possibile utilizzare le regole di sicurezza per imporre che un determinato campo contenga un elenco o una mappa, ma non è possibile imporre che un campo contenga un elenco di tutti i numeri interi o tutte le stringhe.

Allo stesso modo, è possibile utilizzare le regole di sicurezza per imporre valori di tipo per voci specifiche in un elenco o in una mappa (utilizzando rispettivamente la notazione tra parentesi o i nomi delle chiavi), ma non esiste una scorciatoia per imporre i tipi di dati di tutti i membri in una mappa o in un elenco in una volta.

Ad esempio, le seguenti regole garantiscono che un campo tags in un documento contenga un elenco e che la prima voce sia una stringa. Garantisce inoltre che il campo product contenga una mappa che a sua volta contiene un nome di prodotto che è una stringa e una quantità che è un numero intero.

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

I tipi di campo devono essere applicati sia durante la creazione che durante l'aggiornamento di un documento. Pertanto, potresti prendere in considerazione la creazione di una funzione di supporto che puoi chiamare sia nella sezione di creazione che in quella di aggiornamento delle regole di sicurezza.

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

Applicazione dei tipi per i campi facoltativi

È importante ricordare che la chiamata request.resource.data.foo su un documento in cui foo non esiste genera un errore e pertanto qualsiasi regola di sicurezza che effettua tale chiamata rifiuterà la richiesta. Puoi gestire questa situazione utilizzando il metodo get su request.resource.data . Il metodo get ti consente di fornire un argomento predefinito per il campo che stai recuperando da una mappa se quel campo non esiste.

Ad esempio, se i documenti di revisione contengono anche un campo opzionale photo_url e un campo opzionale tags che vuoi verificare siano rispettivamente stringhe ed elenchi, puoi farlo riscrivendo la funzione reviewFieldsAreValidTypes in qualcosa di simile al seguente:

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

Questo rifiuta i documenti in cui esistono tags , ma non è un elenco, consentendo comunque i documenti che non contengono un campo tags (o photo_url ).

Non sono mai consentite scritture parziali

Un'ultima nota sulle regole di sicurezza di Cloud Firestore è che consentono al client di apportare una modifica a un documento oppure rifiutano l'intera modifica. Non è possibile creare regole di sicurezza che accettino scritture su alcuni campi del documento mentre ne rifiutano altri nella stessa operazione.