Supongamos que tienen una app de chat en la que desean habilitar una conversación entre personas de un grupo selecto o una app de intercambio de fotos a través de la cual buscan que un grupo de amigos cree un álbum en conjunto. ¿Cómo se puede limitar este uso compartido de datos a un grupo pequeño de usuarios y evitar exponerlo al público en general?

En este punto, las reglas de seguridad de Firebase pueden servirles. Las reglas de seguridad de Firebase pueden tener mucho alcance, pero a veces se requiere un poco de orientación para usarlas. Esto no se debe realmente a que sean complicadas, sino a que la mayoría de las personas suelen usarlas con suficiente frecuencia como para adquirir mucha experiencia.

Por fortuna, para todos ustedes, trabajo junto a personas que han acumulado experiencia en seguridad de Firebase y las he fastidiado durante varias semanas últimamente para que se redactara esta publicación. Lo que es más importante, descubrí un truco único y extraño que permite descubrir de manera sencilla reglas que compartiré con ustedes... al final de este artículo.

Por el momento, regresemos al ejemplo hipotético de una app de chat pensada para conversaciones grupales privadas. Quienes forman parte del grupo de chat pueden leer y escribir mensajes de este, pero no deseamos que otras personas participen.

Imagina que hemos estructurado nuestra base de datos para tal fin. Existen muchas formas de hacerlo, por supuesto, pero esta probablemente sea la más sencilla para una demostración.

Dentro de cada chat semiprivado, se encuentran una lista de personas que tienen permitido participar del chat y la lista de mensajes de chat. (En la realidad, estos userID tendrán un aspecto mucho más desorganizado que el de user_abc).

Por lo tanto, la primera regla de seguridad que nos convendrá configurar es la que establece que solo las personas de la lista de miembros puedan ver mensajes de chat. Podríamos crear algo así usando un conjunto de reglas de seguridad como la siguiente:
{
    "rules": {
      "chats": {
        "$chatID": {
          "messages": {
            ".read": "data.parent().child('members').child(auth.uid).exists()"
          }
        }
      }
    }
}

Con esto se otorga al usuario permiso para leer los chats de chats//messages mientras su userID exista en la sección members de ese mismo chat.

¿Les despierta curiosidad la línea $chatID? Es casi equivalente a un comodín totalmente adaptable, pero introduce la coincidencia a una variable $chatID a la que pueden hacer referencia posteriormente si lo desean.

¿Y user_abc? Supone un permiso absoluto para leer mensajes de chat. Sin embargo, user_xyz supone la denegación de tal permiso porque no hay entradas members/user_xyz dentro de ese grupo de chat.

Una vez hecho eso, será irrelevante agregar una regla similar que establezca que solo los miembros podrán escribir mensajes de chat.
"chats": {
  "$chatID": {
    "messages": {
      ".read": "data.parent().child('members').child(auth.uid).exists()",
      ".write": "data.parent().child('members').child(auth.uid).exists()"
    }
  }
}

Y es posible alcanzar un mayor nivel de detalle si se desea. ¿Y si nuestra app de chat tuviera un tipo de usuario "lurker" con permiso para ver mensajes y sin permiso para escribirlos?

Para esto también hay respuestas. Nos convendrá modificar nuestras reglas para decir “Puedes escribir mensajes, aunque solo si apareces en la lista de propietarios o miembros del chat”. El resultado se asemejaría a lo siguiente:
"chats": {
  "$chatID": {
    "messages": {
      ".read": "data.parent().child('members').child(auth.uid).exists()",
      ".write": "data.parent().child('members').child(auth.uid).val() == 'owner' || data.parent().child('members').child(auth.uid).val()=='chatter'"
    }
  }
}

(Tengan en cuenta que retiré la línea de “reglas” del resto de estos ejemplos para ser más breve).

A propósito, probablemente pienses “Cielos, ¿no sería más fácil simplemente permitir que las personas escriban mensajes de chat solo si no aparecen como ‘lurker’”? Esto, por supuesto, restaría una línea de código.
"chats": {
  "$chatID": {
    "messages": {
      ".read": "data.parent().child('members').child(auth.uid).exists()",
      ".write": "data.parent().child('members').child(auth.uid).val() != 'lurker'"
    }
  }

Con frecuencia, sin embargo, cuando se trata de la seguridad los resultados son mejores cuando esta se basa en listas blancas en lugar de listas negras. Considera lo siguiente: ¿qué sucedería si en su app de pronto se agregara una nueva clase de usuarios (los llamaremos “principiantes”) y olvidaras actualizar esas reglas?

Con el primer conjunto de reglas, los usuarios de ese grupo nuevo no podrían realizar publicaciones, pero con el segundo podrían publicar lo que deseen. Los dos podrían resultarles inconvenientes si no ofrecen los resultados que buscan, pero el último podría ser mucho más perjudicial en términos de seguridad.

En todo esto, claro, se pasa por alto un pequeño problema: ¿cómo fue posible completar esas listas de usuarios en primer lugar?

Supongamos, por un momento, que un usuario puede de alguna manera obtener una lista de sus amigos a través de la app. (Dejaremos esto como un ejercicio para el lector). Existen algunas opciones que se pueden tener en cuenta para agregar usuarios nuevos a un chat grupal.
  1. Todos aquellos que ya formen parte del chat pueden agregar otras personas a este.
  2. Solo el propietario de un chat puede agregar otras personas a este.
  3. Cualquiera puede solicitar unirse al chat, pero el propietario debe dar su aprobación.

A decir verdad, cualquiera de estas opciones funcionará. Realmente será el desarrollador de la app quien determine la mejor experiencia de usuario para su app.

Veamos las opciones en orden.

Todos aquellos que ya formen parte del chat pueden agregar otras personas a este.


Para administrar esa primera opción, necesitaremos configurar una regla que indique “Quienes ya aparezcan en la lista de miembros tienen permiso para escribir mensajes a los miembros de esta”.

Esto se asemeja mucho a las reglas que ya configuramos para la publicación en la lista de miembros:
"chats": {
  "$chatID": {
    "messages": {
      ".read": "data.parent().child('members').child(auth.uid).exists()",
      ".write": "data.parent().child('members').child(auth.uid).val() == 'owner' || data.parent().child('members').child(auth.uid).val()=='chatter'"
    },
    "members": {
      ".read": "data.child(auth.uid).exists()",
      ".write": "data.child(auth.uid).exists()"
    }
  }
}

Básicamente, se afirma que cualquier usuario tiene permiso para leer mensajes de miembros de la lista o escribir mensajes a ellos mientras su ID actual ya exista en la lista.

Solo el propietario de un chat puede agregar otras personas a este


Aplicar la limitación para que un propietario tenga permiso para escribir mensajes a miembros de la lista también es sencillo.
"chats": {
  "$chatID": {
    "messages": {
      ".read": "data.parent().child('members').child(auth.uid).exists()",
      ".write": "data.parent().child('members').child(auth.uid).val() == 'owner' || data.parent().child('members').child(auth.uid).val()=='chatter'"
    },
    "members": {
      ".read": "data.child(auth.uid).val() == 'owner'",
      ".write": "data.child(auth.uid).val() == 'owner'"
    }
  }
}

Lo que se plantea es “Puedes escribirles a los miembros de una rama de un chat, pero solo si tu userID ya se encuentra allí y apareces como propietario”.

Por lo tanto, ese segundo caso queda abarcado.

Cualquiera puede solicitar unirse al chat, pero el propietario debe dar su aprobación


¿Qué sucede con la idea de permitir que los usuarios soliciten su incorporación y luego hacer que el propietario del chat los apruebe? Para esto, una buena opción sería agregar una lista pendiente a la base de datos junto a la lista de members, en la cual las personas pudieran agregarse a sí mismas.

El propietario del grupo tendría, entonces, permiso para agregar estos usuarios potenciales a la lista de miembros y borrarlos de la lista pendiente.

La primera regla que convendrá declarar es “Puedes agregar una entrada a la lista pendiente, aunque solo si te agregas tú mismo”. Dicho en otros términos, el aspecto fundamental del elemento que se agregará debe ser el ID de usuario propio.

La regla para agregar esto tiene el siguiente aspecto:
"chats": {
  "$chatID": {
    "messages": {
      ".read": "data.parent().child('members').child(auth.uid).exists()",
      ".write": "data.parent().child('members').child(auth.uid).val() == 'owner' || data.parent().child('members').child(auth.uid).val()=='chatter'"
    },
    "members": {
      ".read": "data.child(auth.uid).val() == 'owner'",
      ".write": "data.child(auth.uid).val() == 'owner'"
    },
    "pending": {
      "$uid": {
        ".write": "$uid === auth.uid"
      }
    }
  }
}

Aquí, el mensaje es “Escribe lo que desees en la rama pending/ mientras el ID de usuario sea tu propio userID”.

Para aumentar el nivel de detalle, también se puede especificar que el usuario podrá hacer esto si no se agregó a sí mismo a la lista “pendiente”, que tendrá un aspecto más parecido al siguiente:
"pending": {
  "$uid": {
    ".write": "$uid === auth.uid && !data.exists()"
  }
}

Aprovechando la situación, también podemos especificar que una persona no puede agregarse a sí misma si ya forma parte del chat. No tendría sentido. Como resultado, se obtendrían reglas como la siguiente:
"pending": {
  "$uid": {
    ".write": "$uid === auth.uid && !data.exists() && !data.parent().parent().child('members').child($uid).exists()"
  }
}

Luego, podemos agregar algunas reglas a la carpeta pending con las cuales se determine que un propietario tiene permisos para leer mensajes de ella o enviarlos allí.
"pending": {
  ".read": "data.parent().child('members').child(auth.uid).val() === 'owner'",
  ".write": "data.parent().child('members').child(auth.uid).val() === 'owner'",
  "$uid": {
    ".write": "$uid === auth.uid && !data.exists() && !data.parent().parent().child('members').child($uid).exists()"
  }
}

¡Eso es todo! Suponiendo que conservamos las reglas de la sección anterior, que solo otorgan al propietario de un chat permisos para leer mensajes de miembros de una lista o escribir mensajes a ellos, hemos agregado con éxito las reglas de seguridad que permiten a un usuario quitar una entrada de la lista pendiente y luego agregarla a la lista de miembros.

Supongo que nos olvidamos una última regla que es bastante importante: permitir que alguien cree un nuevo chat grupal. ¿Cómo se puede configurar esto? Si se piensa en ello, es posible agregar la regla afirmando “Cualquiera tiene permiso para escribir mensajes a miembros de una lista si esta está vacía y te designas a ti mismo como propietario”.
"members": {
  ".read": "data.child(auth.uid).val() == 'owner'",
  ".write": "data.child(auth.uid).val() == 'owner' ||(!data.exists()&&newData.child(auth.uid).val()=='owner')"
}

A los efectos de este proceso de escritura, imaginen que comienzan realizando una operación de escritura en /chats/chat_345/members con un objeto { "user_zzz" : "owner" }. Esa nueva línea newData controlará este objeto y confirmará que el campo secundario con la clave del usuario con sesión activa (user_zzz) aparezca en la lista como propietario.

Una vez que hagas esto, podrás agregar los mensajes o usuarios adicionales que el propietario desee. Debido a que ahora oficialmente se mencionarán como propietarios, las reglas se seguridad deben permitir esas acciones sin problemas.

Tengan en cuenta que las reglas de seguridad realmente no incorporan un concepto de acción de “creación de directorios” separada. Si un usuario puede escribir mensajes a chat_456/messages/abc, la regla se aplica independientemente de que los mensajes ya existan. (Para el caso, chat_456).

¿Cómo hice para descubrir todo esto?


No soy experto en seguridad de Firebase, pero tengo la posibilidad de actuar como tal en las publicaciones del blog; sobre todo, al ejecutar el simulador de reglas.

Cada vez que se aplica un cambio a las reglas, y antes de su publicación, se puede probar su funcionamiento simulando operaciones de lectura o escritura en la base de datos. En la sección de reglas de la consola de Firebase, hay un botón “Simulator” que se puede accionar en la parte superior derecha. Este hará aparecer un formulario que les permitirá probar cualquier clase de acciones de lectura o escritura que deseen.

En este ejemplo, probaré esa última regla haciendo que un usuario con sesión activa como "user_zzz" intente agregarse a sí mismo como propietario en una lista /chats/chat_987/members vacía. El simulador de reglas me indica que esto está permitido y destaca la línea en la cual la acción de escritura se evalúa como true.

Técnicamente, se destaca la línea incorrecta. Se evalúa como true la parte de la regla en el paso 13. Creo que el marcador de resaltado no maneja bien los saltos de línea en particular.

Por otra parte, si ese usuario intenta agregarse a sí mismo como propietario de una lista que no está vacía, se producirá un error. Esto es exactamente lo que se busca.

Optimizaciones adicionales


Tengan en cuenta que pueden aplicarse otras optimizaciones a esto. En este momento, hemos aplicado una configuración que permite a los propietarios agregar miembros designados como propietarios. Esto puede, o no, ser lo que deseamos.

Si pensamos en ello, no hemos tomado medidas que validen la adición de miembros nuevos con roles legítimos. También existen, por supuesto, reglas de validación que se pueden agregar a los mensajes de chat para asegurarnos de que nuestra IU pueda controlar su extensión. Sin embargo, tal vez puedan experimentar en esta área.

Copien estas reglas finales y péguenlas en sus propias versiones de una app de chat, y vean lo que pueden hacer para agregar estas optimizaciones.
{
  "rules": {
    "chats": {
      "$chatID": {
        "messages": {
          ".read": "data.parent().child('members').child(auth.uid).exists()",
          ".write": "data.parent().child('members').child(auth.uid).val() == 'owner' || data.parent().child('members').child(auth.uid).val()=='chatter'"
        },
        "members": {
          ".read": "data.child(auth.uid).val() == 'owner'",
          ".write": "data.child(auth.uid).val() == 'owner' ||(!data.exists()&&newData.child(auth.uid).val()=='owner')"
        },
        "pending": {
          ".read": "data.parent().child('members').child(auth.uid).val() === 'owner'",
          ".write": "data.parent().child('members').child(auth.uid).val() === 'owner'",
          "$uid": {
            ".write": "$uid === auth.uid && !data.exists() && !data.parent().parent().child('members').child($uid).exists()"
          }
        }
      }
    }
  }
}

Los invito con gusto a consultar la documentación si necesitan más ayuda, y a hacer pruebas con el simulador. Les garantizo que usar un simulador de reglas de seguridad de bases de datos será la experiencia más entretenida que tendrán esta semana o que al menos estará entre las tres mejores.