1. Antes de comenzar
Cloud Firestore, Cloud Storage para Firebase y Realtime Database dependen de archivos de configuración que escribes para otorgar acceso de lectura y escritura. Esa configuración, llamada reglas de seguridad, también puede actuar como un tipo de esquema para tu app. Es una de las partes más importantes del desarrollo de tu aplicación. Y este codelab te guiará en el proceso.
Requisitos previos
- Un editor simple, como Visual Studio Code, Atom o Sublime Text
- Node.js 8.6.0 o una versión posterior (para instalar Node.js, usa nvm; para verificar tu versión, ejecuta
node --version
) - Java 7 o posterior (para instalar Java, sigue estas instrucciones; para verificar tu versión, ejecuta
java -version
)
Actividades
En este codelab, protegerás una plataforma de blogs simple compilada en Firestore. Usarás el emulador de Firestore para ejecutar pruebas de unidades en las reglas de seguridad y asegurarte de que las reglas permitan y rechacen el acceso que esperas.
Aprenderás a hacer lo siguiente:
- Cómo otorgar permisos detallados
- Aplicar validaciones de datos y tipos
- Implementa el control de acceso basado en atributos
- Otorga acceso según el método de autenticación
- Crea funciones personalizadas
- Crea reglas de seguridad basadas en el tiempo
- Implementa una lista de entidades denegadas y borrados no definitivos
- Comprende cuándo desnormalizar los datos para satisfacer varios patrones de acceso
2. Configurar
Esta es una aplicación de blogs. A continuación, se incluye un resumen general de la funcionalidad de la aplicación:
Borradores de entradas de blog:
- Los usuarios pueden crear borradores de publicaciones de blog, que se almacenan en la colección
drafts
. - El autor puede seguir actualizando un borrador hasta que esté listo para publicarse.
- Cuando está lista para publicarse, se activa una función de Firebase que crea un documento nuevo en la colección
published
. - Los borradores pueden ser borrados por el autor o por los moderadores del sitio.
Entradas de blog publicadas:
- Los usuarios no pueden crear publicaciones publicadas, solo se pueden crear a través de una función.
- Solo se pueden borrar de forma no definitiva, lo que actualiza un atributo
visible
a falso.
Comentarios
- Las publicaciones permiten comentarios, que son una subcolección en cada publicación.
- Para reducir el abuso, los usuarios deben tener una dirección de correo electrónico verificada y no estar en una lista de bloqueo para dejar un comentario.
- Los comentarios solo se pueden actualizar dentro de la hora posterior a su publicación.
- Los comentarios pueden borrarlos el autor del comentario, el autor de la publicación original o los moderadores.
Además de las reglas de acceso, crearás reglas de seguridad que apliquen los campos obligatorios y las validaciones de datos.
Todo sucederá de forma local con Firebase Emulator Suite.
Obtén el código fuente
En este codelab, comenzarás con pruebas para las reglas de seguridad, pero con reglas de seguridad mínimas. Por lo tanto, lo primero que debes hacer es clonar la fuente para ejecutar las pruebas:
$ git clone https://github.com/FirebaseExtended/codelab-rules.git
Luego, muévete al directorio initial-state, en el que trabajarás durante el resto de este codelab:
$ cd codelab-rules/initial-state
Ahora, instala las dependencias para poder ejecutar las pruebas. Si tienes una conexión a Internet más lenta, esto puede tardar uno o dos minutos:
# Move into the functions directory, install dependencies, jump out. $ cd functions && npm install && cd -
Obtén Firebase CLI
El paquete Emulator Suite que usarás para ejecutar las pruebas forma parte de Firebase CLI (interfaz de línea de comandos), que se puede instalar en tu máquina con el siguiente comando:
$ npm install -g firebase-tools
A continuación, confirma que tienes la versión más reciente de la CLI. Este codelab debería funcionar con la versión 8.4.0 o posterior, pero las versiones posteriores incluyen más correcciones de errores.
$ firebase --version 9.10.2
3. Ejecuta las pruebas
En esta sección, ejecutarás las pruebas de forma local. Esto significa que es hora de iniciar Emulator Suite.
Inicia los emuladores
La aplicación con la que trabajarás tiene tres colecciones principales de Firestore: drafts
contiene las entradas de blog en curso, la colección published
contiene las entradas de blog que se publicaron y comments
es una subcolección de las entradas publicadas. El repo incluye pruebas unitarias para las reglas de seguridad que definen los atributos del usuario y otras condiciones necesarias para que un usuario cree, lea, actualice y borre documentos en las colecciones drafts
, published
y comments
. Escribirás las reglas de seguridad para que esas pruebas pasen.
Para comenzar, tu base de datos está bloqueada: se rechazan universalmente las lecturas y escrituras en la base de datos, y todas las pruebas fallan. A medida que escribas reglas de seguridad, las pruebas se aprobarán. Para ver las pruebas, abre functions/test.js
en tu editor.
En la línea de comandos, inicia los emuladores con emulators:exec
y ejecuta las pruebas:
$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test"
Desplázate hasta la parte superior del resultado:
$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test" i emulators: Starting emulators: functions, firestore, hosting ⚠ functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: auth, database, pubsub ⚠ functions: Unable to fetch project Admin SDK configuration, Admin SDK behavior in Cloud Functions emulator may be incorrect. i firestore: Importing data from /Users/user/src/firebase/rules-codelab/initial-state/.seed/firestore_export/firestore_export.overall_export_metadata i firestore: Firestore Emulator logging to firestore-debug.log ⚠ hosting: Authentication error when trying to fetch your current web app configuration, have you run firebase login? ⚠ hosting: Could not fetch web app configuration and there is no cached configuration on this machine. Check your internet connection and make sure you are authenticated. To continue, you must call firebase.initializeApp({...}) in your code before using Firebase. i hosting: Serving hosting files from: public ✔ hosting: Local server: http://localhost:5000 i functions: Watching "/Users/user/src/firebase/rules-codelab/initial-state/functions" for Cloud Functions... ✔ functions[publishPost]: http function initialized (http://localhost:5001/codelab/us-central1/publishPost). ✔ functions[softDelete]: http function initialized (http://localhost:5001/codelab/us-central1/softDelete). i Running script: pushd functions; npm test ~/src/firebase/rules-codelab/initial-state/functions ~/src/firebase/rules-codelab/initial-state > functions@ test /Users/user/src/firebase/rules-codelab/initial-state/functions > mocha (node:76619) ExperimentalWarning: Conditional exports is an experimental feature. This feature could change at any time Draft blog posts 1) can be created with required fields by the author 2) can be updated by author if immutable fields are unchanged 3) can be read by the author and moderator Published blog posts 4) can be read by everyone; created or deleted by no one 5) can be updated by author or moderator Comments on published blog posts 6) can be read by anyone with a permanent account 7) can be created if email is verfied and not blocked 8) can be updated by author for 1 hour after creation 9) can be deleted by an author or moderator 0 passing (848ms) 9 failing ...
En este momento, hay 9 fallas. A medida que compilas el archivo de reglas, puedes medir el progreso observando cómo se aprueban más pruebas.
4. Crea borradores de entradas de blog.
Dado que el acceso a las entradas de blog en borrador es muy diferente del acceso a las entradas de blog publicadas, esta app de blogs almacena las entradas de blog en borrador en una colección separada, /drafts
. Solo el autor o un moderador pueden acceder a los borradores, que tienen validaciones para los campos obligatorios e inmutables.
Si abres el archivo firestore.rules
, encontrarás un archivo de reglas predeterminado:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
}
}
La declaración de coincidencia, match /{document=**}
, usa la sintaxis **
para aplicarse de forma recursiva a todos los documentos de las subcolecciones. Y, como está en el nivel superior, en este momento se aplica la misma regla general a todas las solicitudes, sin importar quién las realice ni qué datos intente leer o escribir.
Comienza por quitar la sentencia de coincidencia más interna y reemplázala por match /drafts/{draftID}
. (Los comentarios sobre la estructura de los documentos pueden ser útiles en las reglas y se incluirán en este codelab, pero siempre son opcionales).
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
}
}
}
La primera regla que escribirás para los borradores controlará quién puede crear los documentos. En esta aplicación, solo la persona que figura como autor puede crear borradores. Verifica que el UID de la persona que realiza la solicitud sea el mismo que se indica en el documento.
La primera condición para la creación será la siguiente:
request.resource.data.authorUID == request.auth.uid
Luego, los documentos solo se pueden crear si incluyen los tres campos obligatorios: authorUID
,createdAt
y title
. (El usuario no proporciona el campo createdAt
; esto exige que la app lo agregue antes de intentar crear un documento). Como solo necesitas verificar que se estén creando los atributos, puedes comprobar que request.resource
tenga todas esas claves:
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
])
El requisito final para crear una entrada de blog es que el título no puede tener más de 50 caracteres:
request.resource.data.title.size() < 50
Dado que todas estas condiciones deben ser verdaderas, concaténalas con el operador lógico AND, &&
. La primera regla se convierte en la siguiente:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
}
}
}
En la terminal, vuelve a ejecutar las pruebas y confirma que la primera se apruebe.
5. Actualiza los borradores de las entradas de blog.
A continuación, a medida que los autores mejoren sus borradores de entradas de blog, editarán los documentos de borrador. Crea una regla para las condiciones en las que se puede actualizar una publicación. Primero, solo el autor puede actualizar sus borradores. Ten en cuenta que aquí verificas el UID que ya está escrito,resource.data.authorUID
:
resource.data.authorUID == request.auth.uid
El segundo requisito para una actualización es que no deben cambiar dos atributos, authorUID
y createdAt
:
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]);
Por último, el título debe tener 50 caracteres o menos:
request.resource.data.title.size() < 50;
Dado que se deben cumplir todas estas condiciones, concaténalas con &&
:
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
Las reglas completas serán las siguientes:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
}
}
}
Vuelve a ejecutar las pruebas y confirma que se apruebe otra prueba.
6. Borra y lee borradores: Control de acceso basado en atributos
Del mismo modo que los autores pueden crear y actualizar borradores, también pueden borrarlos.
resource.data.authorUID == request.auth.uid
Además, los autores con un atributo isModerator
en su token de autenticación pueden borrar borradores:
request.auth.token.isModerator == true
Dado que cualquiera de estas condiciones es suficiente para una eliminación, concaténalas con un operador OR lógico, ||
:
allow delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true
Las mismas condiciones se aplican a las lecturas, por lo que se puede agregar ese permiso a la regla:
allow read, delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true
Las reglas completas ahora son las siguientes:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow read, delete: if
// User is draft author
resource.data.authorUID == request.auth.uid ||
// User is a moderator
request.auth.token.isModerator == true;
}
}
}
Vuelve a ejecutar las pruebas y confirma que ahora se aprueba otra prueba.
7. Lecturas, creaciones y eliminaciones de publicaciones: desnormalización para diferentes patrones de acceso
Debido a que los patrones de acceso para las publicaciones publicadas y las publicaciones en borrador son muy diferentes, esta app desnormaliza las publicaciones en colecciones draft
y published
separadas. Por ejemplo, cualquier persona puede leer las publicaciones publicadas, pero no se pueden borrar de forma definitiva, mientras que los borradores se pueden borrar, pero solo los pueden leer el autor y los moderadores. En esta app, cuando un usuario quiere publicar una entrada de blog en borrador, se activa una función que creará la nueva entrada publicada.
A continuación, escribirás las reglas para las publicaciones publicadas. Las reglas más simples que se pueden escribir son que cualquier persona puede leer las publicaciones publicadas y que nadie puede crearlas ni borrarlas. Agrega estas reglas:
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
}
Si agregas estas reglas a las existentes, el archivo de reglas completo se verá de la siguiente manera:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow read, delete: if
// User is draft author
resource.data.authorUID == request.auth.uid ||
// User is a moderator
request.auth.token.isModerator == true;
}
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
}
}
}
Vuelve a ejecutar las pruebas y confirma que se apruebe otra prueba.
8. Actualización de publicaciones: Funciones personalizadas y variables locales
Las condiciones para actualizar una publicación publicada son las siguientes:
- Solo el autor o el moderador pueden hacerlo.
- Debe contener todos los campos obligatorios.
Como ya escribiste las condiciones para ser autor o moderador, podrías copiarlas y pegarlas, pero, con el tiempo, eso podría ser difícil de leer y mantener. En su lugar, crearás una función personalizada que encapsule la lógica para ser autor o moderador. Luego, la llamarás desde varias condiciones.
Cómo crear una función personalizada
Sobre la instrucción de coincidencia para borradores, crea una nueva función llamada isAuthorOrModerator
que tome como argumentos un documento de publicación (esto funcionará para borradores o publicaciones) y el objeto de autenticación del usuario:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Returns true if user is post author or a moderator
function isAuthorOrModerator(post, auth) {
}
match /drafts/{postID} {
allow create: ...
allow update: ...
...
}
match /published/{postID} {
allow read: ...
allow create, delete: ...
}
}
}
Usa variables locales
Dentro de la función, usa la palabra clave let
para establecer las variables isAuthor
y isModerator
. Todas las funciones deben finalizar con una sentencia de retorno, y la nuestra devolverá un valor booleano que indica si alguna de las variables es verdadera:
function isAuthorOrModerator(post, auth) {
let isAuthor = auth.uid == post.authorUID;
let isModerator = auth.token.isModerator == true;
return isAuthor || isModerator;
}
Llama a la función
Ahora actualizarás la regla para borradores para llamar a esa función, teniendo cuidado de pasar resource.data
como primer argumento:
// Draft blog posts
match /drafts/{draftID} {
...
// Can be deleted by author or moderator
allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
}
Ahora puedes escribir una condición para actualizar las publicaciones publicadas que también use la nueva función:
allow update: if isAuthorOrModerator(resource.data, request.auth);
Agrega validaciones
Algunos campos de una entrada publicada no deben cambiarse, específicamente los campos url
, authorUID
y publishedAt
son inmutables. Los otros dos campos, title
y content
, y visible
deben seguir presentes después de una actualización. Agrega condiciones para aplicar estos requisitos a las actualizaciones de las publicaciones:
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
])
Crea una función personalizada por tu cuenta
Por último, agrega una condición para que el título tenga menos de 50 caracteres. Como se trata de lógica reutilizada, podrías crear una función nueva, titleIsUnder50Chars
. Con la nueva función, la condición para actualizar una publicación publicada se convierte en la siguiente:
allow update: if
isAuthorOrModerator(resource.data, request.auth) &&
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
]) &&
titleIsUnder50Chars(request.resource.data);
Y el archivo de reglas completo es el siguiente:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Returns true if user is post author or a moderator
function isAuthorOrModerator(post, auth) {
let isAuthor = auth.uid == post.authorUID;
let isModerator = auth.token.isModerator == true;
return isAuthor || isModerator;
}
function titleIsUnder50Chars(post) {
return post.title.size() < 50;
}
// Draft blog posts
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
titleIsUnder50Chars(request.resource.data);
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
titleIsUnder50Chars(request.resource.data);
// Can be read or deleted by author or moderator
allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
}
// Published blog posts are denormalized from drafts
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
allow update: if
isAuthorOrModerator(resource.data, request.auth) &&
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
]) &&
titleIsUnder50Chars(request.resource.data);
}
}
}
Vuelve a ejecutar las pruebas. En este punto, deberías tener 5 pruebas aprobadas y 4 reprobadas.
9. Comentarios: Subcolecciones y permisos del proveedor de acceso
Las publicaciones publicadas permiten comentarios, y estos se almacenan en una subcolección de la publicación publicada (/published/{postID}/comments/{commentID}
). De forma predeterminada, las reglas de una colección no se aplican a las subcolecciones. No quieres que se apliquen a los comentarios las mismas reglas que se aplican al documento principal de la publicación, por lo que crearás reglas diferentes.
Para escribir reglas de acceso a los comentarios, comienza con la instrucción de coincidencia:
match /published/{postID}/comments/{commentID} {
// `authorUID`: string, required
// `comment`: string, < 500 characters, required
// `createdAt`: timestamp, required
// `editedAt`: timestamp, optional
Leer comentarios: No se puede hacer de forma anónima
En esta app, solo los usuarios que crearon una cuenta permanente, no una anónima, pueden leer los comentarios. Para aplicar esa regla, busca el atributo sign_in_provider
que se encuentra en cada objeto auth.token
:
allow read: if request.auth.token.firebase.sign_in_provider != "anonymous";
Vuelve a ejecutar las pruebas y confirma que se aprueba una prueba más.
Creación de comentarios: Verificación de una lista de bloqueo
Existen tres condiciones para crear un comentario:
- El usuario debe tener un correo electrónico verificado.
- El comentario debe tener menos de 500 caracteres.
- No pueden estar en una lista de usuarios prohibidos, que se almacena en Firestore en la colección
bannedUsers
. Analicemos estas condiciones una por una:
request.auth.token.email_verified == true
request.resource.data.comment.size() < 500
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
La regla final para crear comentarios es la siguiente:
allow create: if
// User has verified email
(request.auth.token.email_verified == true) &&
// UID is not on bannedUsers list
!(exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
El archivo de reglas completo ahora es el siguiente:
For bottom of step 9
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Returns true if user is post author or a moderator
function isAuthorOrModerator(post, auth) {
let isAuthor = auth.uid == post.authorUID;
let isModerator = auth.token.isModerator == true;
return isAuthor || isModerator;
}
function titleIsUnder50Chars(post) {
return post.title.size() < 50;
}
// Draft blog posts
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User is author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and createdAt fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
titleIsUnder50Chars(request.resource.data);
allow update: if
// User is author
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
titleIsUnder50Chars(request.resource.data);
// Can be read or deleted by author or moderator
allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
}
// Published blog posts are denormalized from drafts
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
allow update: if
isAuthorOrModerator(resource.data, request.auth) &&
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
]) &&
titleIsUnder50Chars(request.resource.data);
}
match /published/{postID}/comments/{commentID} {
// `authorUID`: string, required
// `createdAt`: timestamp, required
// `editedAt`: timestamp, optional
// `comment`: string, < 500 characters, required
// Must have permanent account to read comments
allow read: if !(request.auth.token.firebase.sign_in_provider == "anonymous");
allow create: if
// User has verified email
request.auth.token.email_verified == true &&
// Comment is under 500 characters
request.resource.data.comment.size() < 500 &&
// UID is not on the block list
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
}
}
}
Vuelve a ejecutar las pruebas y asegúrate de que se apruebe una prueba más.
10. Actualización de comentarios: reglas basadas en el tiempo
La lógica empresarial de los comentarios indica que el autor puede editarlos durante una hora después de su creación. Para implementar esto, usa la marca de tiempo createdAt
.
Primero, para establecer que el usuario es el autor, haz lo siguiente:
request.auth.uid == resource.data.authorUID
A continuación, que el comentario se creó en la última hora:
(request.time - resource.data.createdAt) < duration.value(1, 'h');
Si combinamos estos elementos con el operador lógico AND, la regla para actualizar comentarios se convierte en la siguiente:
allow update: if
// is author
request.auth.uid == resource.data.authorUID &&
// within an hour of comment creation
(request.time - resource.data.createdAt) < duration.value(1, 'h');
Vuelve a ejecutar las pruebas y asegúrate de que se apruebe una prueba más.
11. Cómo borrar comentarios: Se verifica la propiedad parental
Los comentarios pueden borrarlos el autor del comentario, un moderador o el autor de la entrada de blog.
Primero, como la función auxiliar que agregaste antes verifica si hay un campo authorUID
que podría existir en una publicación o en un comentario, puedes reutilizar la función auxiliar para verificar si el usuario es el autor o el moderador:
isAuthorOrModerator(resource.data, request.auth)
Para verificar si el usuario es el autor de la entrada de blog, usa un get
para buscar la entrada en Firestore:
request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID
Como cualquiera de estas condiciones es suficiente, usa un operador OR lógico entre ellas:
allow delete: if
// is comment author or moderator
isAuthorOrModerator(resource.data, request.auth) ||
// is blog post author
request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID;
Vuelve a ejecutar las pruebas y asegúrate de que se apruebe una prueba más.
Y el archivo de reglas completo es el siguiente:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Returns true if user is post author or a moderator
function isAuthorOrModerator(post, auth) {
let isAuthor = auth.uid == post.authorUID;
let isModerator = auth.token.isModerator == true;
return isAuthor || isModerator;
}
function titleIsUnder50Chars(post) {
return post.title.size() < 50;
}
// Draft blog posts
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User is author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and createdAt fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
titleIsUnder50Chars(request.resource.data);
allow update: if
// User is author
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
titleIsUnder50Chars(request.resource.data);
// Can be read or deleted by author or moderator
allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
}
// Published blog posts are denormalized from drafts
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
allow update: if
isAuthorOrModerator(resource.data, request.auth) &&
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
]) &&
titleIsUnder50Chars(request.resource.data);
}
match /published/{postID}/comments/{commentID} {
// `authorUID`: string, required
// `createdAt`: timestamp, required
// `editedAt`: timestamp, optional
// `comment`: string, < 500 characters, required
// Must have permanent account to read comments
allow read: if !(request.auth.token.firebase.sign_in_provider == "anonymous");
allow create: if
// User has verified email
request.auth.token.email_verified == true &&
// Comment is under 500 characters
request.resource.data.comment.size() < 500 &&
// UID is not on the block list
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
allow update: if
// is author
request.auth.uid == resource.data.authorUID &&
// within an hour of comment creation
(request.time - resource.data.createdAt) < duration.value(1, 'h');
allow delete: if
// is comment author or moderator
isAuthorOrModerator(resource.data, request.auth) ||
// is blog post author
request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID;
}
}
}
12. Próximos pasos
¡Felicitaciones! Escribiste las reglas de seguridad que hicieron que todas las pruebas pasaran y protegiste la aplicación.
Estos son algunos temas relacionados que puedes explorar a continuación:
- Entrada de blog: Cómo revisar el código de las reglas de seguridad
- Codelab: Recorrido por el desarrollo local primero con los emuladores
- Video: Cómo configurar la CI para pruebas basadas en emuladores con Acciones de GitHub