Acceso seguro a datos para usuarios y grupos

Muchas aplicaciones colaborativas permiten a los usuarios leer y escribir diferentes datos según un conjunto de permisos. En una aplicación de edición de documentos, por ejemplo, es posible que los usuarios quieran permitir que algunos usuarios lean y escriban sus documentos mientras bloquean el acceso no deseado.

Solución: control de acceso basado en roles

Puede aprovechar el modelo de datos de Cloud Firestore, así como las reglas de seguridad personalizadas, para implementar control de acceso basado en roles en su aplicación.

Suponga que está creando una aplicación de escritura colaborativa en la que los usuarios pueden crear "historias" y "comentarios" con los siguientes requisitos de seguridad:

  • Cada historia tiene un propietario y se puede compartir con "escritores", "comentaristas" y "lectores".
  • Los lectores sólo pueden ver historias y comentarios. No pueden editar nada.
  • Los comentaristas tienen todo el acceso de los lectores y también pueden agregar comentarios a una historia.
  • Los escritores tienen todo el acceso de los comentaristas y también pueden editar el contenido de la historia.
  • Los propietarios pueden editar cualquier parte de una historia y controlar el acceso de otros usuarios.

Estructura de datos

Suponga que su aplicación tiene una colección stories donde cada documento representa una historia. Cada historia también tiene una subcolección comments donde cada documento es un comentario sobre esa historia.

Para realizar un seguimiento de los roles de acceso, agregue un campo roles que sea un mapa de ID de usuario a roles:

/historias/{id de historia}

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

Los comentarios contienen solo dos campos, la identificación de usuario del autor y algo de contenido:

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

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

Normas

Ahora que tiene los roles de los usuarios registrados en la base de datos, necesita escribir reglas de seguridad para validarlas. Estas reglas suponen que la aplicación usa Firebase Auth para que la variable request.auth.uid sea el ID del usuario.

Paso 1 : Comience con un archivo de reglas básicas, que incluye reglas vacías para historias y comentarios:

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

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

Paso 2 : agregue una regla write simple que brinde a los propietarios un control total sobre las historias. Las funciones definidas ayudan a determinar los roles de un usuario y si los nuevos documentos son válidos:

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} {
            // ...
         }
     }
   }
}

Paso 3 : escriba reglas que permitan a un usuario de cualquier rol leer historias y comentarios. El uso de las funciones definidas en el paso anterior mantiene las reglas concisas y legibles:

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

Paso 4 : permita que los escritores, comentaristas y propietarios de historias publiquen comentarios. Tenga en cuenta que esta regla también valida que el owner del comentario coincida con el usuario solicitante, lo que evita que los usuarios escriban sobre los comentarios de los demás:

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

Paso 5 : Ofrezca a los escritores la posibilidad de editar el contenido de la historia, pero no editar las funciones de la historia ni cambiar ninguna otra propiedad del documento. Esto requiere dividir la regla write de historias en reglas separadas para create , update y delete , ya que los escritores solo pueden actualizar historias:

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

Limitaciones

La solución que se muestra arriba demuestra cómo proteger los datos del usuario mediante reglas de seguridad, pero debe tener en cuenta las siguientes limitaciones:

  • Granularidad : en el ejemplo anterior, varios roles (escritor y propietario) tienen acceso de escritura al mismo documento pero con diferentes limitaciones. Esto puede resultar difícil de gestionar con documentos más complejos y puede ser mejor dividir documentos individuales en varios documentos, cada uno de los cuales pertenece a una única función.
  • Grupos grandes : si necesita compartir con grupos muy grandes o complejos, considere un sistema donde los roles se almacenen en su propia colección en lugar de como un campo en el documento de destino.