Proteger el acceso a los datos de usuarios y grupos
Muchas aplicaciones colaborativas permiten a los usuarios leer y escribir diferentes datos en función de un conjunto de permisos. Por ejemplo, en una aplicación de edición de documentos, los usuarios pueden querer permitir que algunos usuarios lean y escriban en sus documentos, al tiempo que bloquean el acceso no deseado.
Solución: control de acceso basado en roles
Puedes aprovechar el modelo de datos de Cloud Firestore, así como las reglas de seguridad personalizadas, para implementar el control de acceso basado en roles en tu aplicación.
Supongamos que estás 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", "comentadores" y "lectores".
- Los lectores solo pueden ver las historias y los comentarios. No pueden editar nada.
- Los comentadores tienen el mismo acceso que los lectores y, además, pueden añadir comentarios a una historia.
- Los editores tienen el mismo acceso que los comentaristas y, además, pueden editar el contenido de las historias.
- Los propietarios pueden editar cualquier parte de una historia, así como controlar el acceso de otros usuarios.
Estructura de datos
Supongamos que tu aplicación tiene una colección stories
en la que cada documento representa una historia. Cada historia también tiene una subcolección comments
en la que cada documento es un comentario sobre esa historia.
Para hacer un seguimiento de los roles de acceso, añade un campo roles
, que es un mapa de IDs de usuario a roles:
/stories/{storyid}
{
title: "A Great Story",
content: "Once upon a time ...",
roles: {
alice: "owner",
bob: "reader",
david: "writer",
jane: "commenter"
// ...
}
}
Los comentarios solo contienen dos campos: el ID de usuario del autor y el contenido:
/stories/{storyid}/comments/{commentid}
{
user: "alice",
content: "I think this is a great story!"
}
Reglas
Ahora que los roles de los usuarios están registrados en la base de datos, debes escribir reglas de seguridad para validarlos. Estas reglas presuponen que la aplicación usa Firebase Auth para que la variable request.auth.uid
sea el ID del usuario.
Paso 1: Empieza con un archivo de reglas básico, que incluya reglas vacías para las historias y los 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: Añade una regla write
sencilla que dé a los propietarios control total sobre las historias. Las funciones definidas ayudan a determinar los roles de un usuario y si los documentos nuevos 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: Escribe reglas que permitan a los usuarios con cualquier rol leer historias y comentarios. Si usa las funciones definidas en el paso anterior, las reglas serán concisas y fáciles de leer:
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: Permite que los autores de historias, los comentaristas y los propietarios publiquen comentarios.
Ten en cuenta que esta regla también valida que el owner
del comentario coincida con el usuario que envía la solicitud, lo que evita que los usuarios sobrescriban los comentarios de otros usuarios:
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: Permite que los redactores editen el contenido de la noticia, pero no los roles ni ninguna otra propiedad del documento. Para ello, debes dividir la regla write
en reglas independientes para create
, update
y delete
, ya que los escritores solo pueden actualizar las 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 explica cómo proteger los datos de los usuarios con reglas de seguridad, pero debes 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 en documentos más complejos, por lo que puede ser mejor dividir los documentos en varios, cada uno de los cuales pertenezca a un solo rol.
- Grupos grandes: si necesitas compartir contenido con grupos muy grandes o complejos, te recomendamos que utilices un sistema en el que los roles se almacenen en su propia colección en lugar de como un campo en el documento de destino.