Proteggere l'accesso ai dati per utenti e gruppi

Molte app collaborative consentono agli utenti di leggere e scrivere diversi dati in base a un insieme di autorizzazioni. In un'app di modifica dei documenti, ad esempio, gli utenti potrebbero voler consentire a pochi utenti di leggere e scrivere i loro documenti, bloccando al contempo l'accesso indesiderato.

Soluzione: controllo dell'accesso basato sui ruoli

Puoi sfruttare il modello di dati di Cloud Firestore e le regole di sicurezza personalizzate per implementare il controllo di accesso basato sui ruoli nella tua app.

Supponiamo che tu stia creando un'applicazione di scrittura collaborativa in cui gli utenti possono creare "storie" e "commenti" con i seguenti requisiti di sicurezza:

  • Ogni storia ha un proprietario e può essere condivisa con "autori", "commentatori" e "lettori".
  • I lettori possono visualizzare solo notizie e commenti. Non possono modificare nulla.
  • I commentatori hanno tutti i diritti dei lettori e possono anche aggiungere commenti a una storia.
  • Gli autori possono accedere a tutte le funzionalità dei commentatori e possono anche modificare i contenuti delle storie.
  • I proprietari possono modificare qualsiasi parte di una storia, nonché controllare l'accesso di altri utenti.

Struttura dei dati

Supponiamo che la tua app abbia una raccolta stories in cui ogni documento rappresenta una storia. Ogni storia ha anche una sottoraccolta comments in cui ogni documento è un commento alla storia.

Per tenere traccia dei ruoli di accesso, aggiungi un campo roles che è una mappa degli ID utente ai ruoli:

/stories/{storyid}

{
  title: "A Great Story",
  content: "Once upon a time ...",
  roles: {
    alice: "owner",
    bob: "reader",
    david: "writer",
    jane: "commenter"
    // ...
  }
}

I commenti contengono solo due campi, l'ID utente dell'autore e alcuni contenuti:

/stories/{storyid}/comments/{commentid}

{
  user: "alice",
  content: "I think this is a great story!"
}

Regole

Ora che hai registrato i ruoli degli utenti nel database, devi scrivere le regole di sicurezza per convalidarli. Queste regole presuppongono che l'app utilizzi Firebase Auth in modo che la variabile request.auth.uid sia l'ID dell'utente.

Passaggio 1: inizia con un file di regole di base, che include regole vuote per le Storie e i commenti:

service cloud.firestore {
   match /databases/{database}/documents {
     match /stories/{story} {
         // TODO: Story rules go here...

         match /comments/{comment} {
            // TODO: Comment rules go here...
         }
     }
   }
}

Passaggio 2: aggiungi una regola write semplice che offra ai proprietari il controllo completo sulle storie. Le funzioni definite aiutano a determinare i ruoli di un utente e se i nuovi documenti sono validi:

service cloud.firestore {
   match /databases/{database}/documents {
     match /stories/{story} {
        function isSignedIn() {
          return request.auth != null;
        }

        function getRole(rsc) {
          // Read from the "roles" map in the resource (rsc).
          return rsc.data.roles[request.auth.uid];
        }

        function isOneOfRoles(rsc, array) {
          // Determine if the user is one of an array of roles
          return isSignedIn() && (getRole(rsc) in array);
        }

        function isValidNewStory() {
          // Valid if story does not exist and the new story has the correct owner.
          return resource == null && isOneOfRoles(request.resource, ['owner']);
        }

        // Owners can read, write, and delete stories
        allow write: if isValidNewStory() || isOneOfRoles(resource, ['owner']);

         match /comments/{comment} {
            // ...
         }
     }
   }
}

Passaggio 3: scrivi regole che consentano a un utente di qualsiasi ruolo di leggere storie e commenti. L'utilizzo delle funzioni definite nel passaggio precedente consente di mantenere le regole concise e leggibili:

service cloud.firestore {
   match /databases/{database}/documents {
     match /stories/{story} {
        function isSignedIn() {
          return request.auth != null;
        }

        function getRole(rsc) {
          return rsc.data.roles[request.auth.uid];
        }

        function isOneOfRoles(rsc, array) {
          return isSignedIn() && (getRole(rsc) in array);
        }

        function isValidNewStory() {
          return resource == null
            && request.resource.data.roles[request.auth.uid] == 'owner';
        }

        allow write: if isValidNewStory() || isOneOfRoles(resource, ['owner']);

        // Any role can read stories.
        allow read: if isOneOfRoles(resource, ['owner', 'writer', 'commenter', 'reader']);

        match /comments/{comment} {
          // Any role can read comments.
          allow read: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
                                      ['owner', 'writer', 'commenter', 'reader']);
        }
     }
   }
}

Passaggio 4: consenti a autori, commentatori e proprietari di storie di pubblicare commenti. Tieni presente che questa regola verifica anche che il owner del commento corrisponda all'utente richiedente, il che impedisce agli utenti di scrivere i reciproci commenti:

service cloud.firestore {
   match /databases/{database}/documents {
     match /stories/{story} {
        function isSignedIn() {
          return request.auth != null;
        }

        function getRole(rsc) {
          return rsc.data.roles[request.auth.uid];
        }

        function isOneOfRoles(rsc, array) {
          return isSignedIn() && (getRole(rsc) in array);
        }

        function isValidNewStory() {
          return resource == null
            && request.resource.data.roles[request.auth.uid] == 'owner';
        }

        allow write: if isValidNewStory() || isOneOfRoles(resource, ['owner'])
        allow read: if isOneOfRoles(resource, ['owner', 'writer', 'commenter', 'reader']);

        match /comments/{comment} {
          allow read: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
                                      ['owner', 'writer', 'commenter', 'reader']);

          // Owners, writers, and commenters can create comments. The
          // user id in the comment document must match the requesting
          // user's id.
          //
          // Note: we have to use get() here to retrieve the story
          // document so that we can check the user's role.
          allow create: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
                                        ['owner', 'writer', 'commenter'])
                        && request.resource.data.user == request.auth.uid;
        }
     }
   }
}

Passaggio 5: offri agli autori la possibilità di modificare i contenuti delle storie, ma non di modificare i relativi ruoli o cambiare qualsiasi altra proprietà del documento. Per farlo, devi suddividere la regola write delle storie in regole separate per create, update e delete, poiché gli autori possono aggiornare solo le storie:

service cloud.firestore {
   match /databases/{database}/documents {
     match /stories/{story} {
        function isSignedIn() {
          return request.auth != null;
        }

        function getRole(rsc) {
          return rsc.data.roles[request.auth.uid];
        }

        function isOneOfRoles(rsc, array) {
          return isSignedIn() && (getRole(rsc) in array);
        }

        function isValidNewStory() {
          return request.resource.data.roles[request.auth.uid] == 'owner';
        }

        function onlyContentChanged() {
          // Ensure that title and roles are unchanged and that no new
          // fields are added to the document.
          return request.resource.data.title == resource.data.title
            && request.resource.data.roles == resource.data.roles
            && request.resource.data.keys() == resource.data.keys();
        }

        // Split writing into creation, deletion, and updating. Only an
        // owner can create or delete a story but a writer can update
        // story content.
        allow create: if isValidNewStory();
        allow delete: if isOneOfRoles(resource, ['owner']);
        allow update: if isOneOfRoles(resource, ['owner'])
                      || (isOneOfRoles(resource, ['writer']) && onlyContentChanged());
        allow read: if isOneOfRoles(resource, ['owner', 'writer', 'commenter', 'reader']);

        match /comments/{comment} {
          allow read: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
                                      ['owner', 'writer', 'commenter', 'reader']);
          allow create: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
                                        ['owner', 'writer', 'commenter'])
                        && request.resource.data.user == request.auth.uid;
        }
     }
   }
}

Limitazioni

La soluzione mostrata sopra mostra come proteggere i dati utente utilizzando le regole di sicurezza, ma tieni presente le seguenti limitazioni:

  • Granularità: nell'esempio precedente, più ruoli (autore e proprietario) hanno accesso in scrittura allo stesso documento, ma con limitazioni diverse. Questo può diventare difficile da gestire con documenti più complessi e potrebbe essere meglio suddividere i singoli documenti in più documenti, ciascuno di proprietà di un singolo ruolo.
  • Gruppi di grandi dimensioni: se devi condividere contenuti con gruppi molto grandi o complessi, prendi in considerazione un sistema in cui i ruoli siano archiviati in una propria raccolta anziché come un campo nel documento di destinazione.