1. Antes de comenzar
Cloud Firestore, Cloud Storage para Firebase y Realtime Database se basan en 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 una especie 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 comprobar tu versión, ejecuta
node --version
) - Java 7 o superior (para instalar Java, usa estas instrucciones; para comprobar tu versión, ejecuta
java -version
)
Actividades
En este codelab, protegerás una plataforma de blog simple compilada en Firestore. Usarás el emulador de Firestore para ejecutar pruebas de unidades con las reglas de seguridad y asegurarte de que estas permitan y no permitan el acceso que esperas.
Aprenderás a hacer lo siguiente:
- Otorga permisos detallados
- Aplica validaciones de datos y tipos
- Implementa el control de acceso basado en atributos
- Otorgar acceso según el método de autenticación
- Crea funciones personalizadas
- Crea reglas de seguridad basadas en el tiempo
- Cómo implementar una lista de bloqueo y eliminaciones no definitivas
- Comprender cuándo desnormalizar los datos para cumplir con múltiples patrones de acceso
2. Configurar
Esta es una aplicación de blogs. Este es un resumen de alto nivel 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é todo listo para publicarlo, se activará una función de Firebase que creará un documento nuevo en la colección
published
. - El autor o los moderadores del sitio pueden borrar los borradores.
Entradas de blog publicadas:
- Los usuarios no pueden crear publicaciones publicadas, solo a través de una función.
- Solo se pueden borrar de forma no definitiva, lo que actualiza un atributo
visible
a falso.
Comentarios
- En las entradas publicadas, se admiten comentarios, que son una subcolección de cada publicación publicada.
- Para reducir los abusos, los usuarios deben tener una dirección de correo electrónico verificada y no estar en una lista de usuarios rechazados para poder dejar un comentario.
- Los comentarios solo se pueden actualizar dentro de una hora después de su publicación.
- El autor del comentario, el autor de la publicación original o los moderadores pueden borrar los comentarios.
Además de las reglas de acceso, crearás reglas de seguridad que apliquen campos obligatorios y validaciones de datos.
Todo ocurrirá 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 las reglas de seguridad mínimas en sí, por lo que lo primero que debes hacer es clonar la fuente para ejecutar las pruebas:
$ git clone https://github.com/FirebaseExtended/codelab-rules.git
Luego, ve 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 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 una 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 manera local. Esto significa que es momento 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 publicadas y comments
es una subcolección de las entradas publicadas. El repo incluye pruebas de unidades 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 se aprueben esas pruebas.
Para comenzar, tu base de datos está bloqueada: las operaciones de lectura y escritura en la base de datos se rechazan universalmente y todas las pruebas fallan. A medida que escribas las reglas de seguridad, las pruebas se superará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 que se aprueben más pruebas.
4. Crear borradores de entradas de blog
Como el acceso a los borradores de entradas de blog es muy diferente al acceso a las entradas de blog publicadas, esta app de blog almacena las entradas de blog en borrador en una colección separada, /drafts
. Solo el autor o un moderador puede acceder a los borradores, que tienen validaciones para campos obligatorios e inmutables.
Cuando abras el archivo firestore.rules
, encontrarás un archivo de reglas predeterminadas:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
}
}
La sentencia 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 o qué datos intente leer o escribir.
Comienza quitando la declaración 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; 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 app, 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 aparece en el documento.
La primera condición para la creación será la siguiente:
request.resource.data.authorUID == request.auth.uid
A continuación, los documentos solo se pueden crear si incluyen los tres campos obligatorios, authorUID
, createdAt
y title
. (El usuario no proporciona el campo createdAt
; esto hace que la app deba agregarlo antes de intentar crear un documento). Como solo necesitas verificar que se creen los atributos, puedes comprobar que request.resource
tenga todas esas claves:
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
])
El último requisito 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, concatenalas 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 sea exitosa.
5. Actualiza los borradores de las publicaciones de blog.
A continuación, a medida que los autores definan mejor 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 dos atributos, authorUID
y createdAt
, no deben cambiar:
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, puedes concatenarlas junto 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 se convierten en 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 haya aprobado otra prueba.
6. Cómo borrar y leer borradores: Control de acceso basado en atributos
Así como 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 borrar, concatenarlas con un operador lógico O, ||
:
allow delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true
Las mismas condiciones se aplican a las operaciones de lectura, de modo que se puede agregar el permiso a la regla:
allow read, delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true
Ahora las reglas completas 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 se haya aprobado otra prueba.
7. Lee, crea y borra publicaciones publicadas: desnormaliza para diferentes patrones de acceso
Debido a que los patrones de acceso de las publicaciones publicadas y de los borradores 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 pueden borrarse, pero solo pueden leerlas el autor y los moderadores. En esta app, cuando un usuario quiere publicar un borrador de entrada de blog, 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 para 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 convierte en lo 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;
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 haya aprobado otra prueba.
8. Actualiza las entradas publicadas: funciones personalizadas y variables locales
Las condiciones para actualizar una publicación publicada son las siguientes:
- solo lo puede hacer el autor o el moderador
- debe contener todos los campos obligatorios.
Como ya escribiste condiciones para ser autor o moderador, puedes copiar y pegar las condiciones, pero con el tiempo podría ser difícil leerlas y mantenerlas. En su lugar, crearás una función personalizada que encapsule la lógica para ser autor o moderador. Luego, lo llamarás a partir de varias condiciones.
Crea una función personalizada
Arriba de la declaración de coincidencia de borradores, crea una nueva función llamada isAuthorOrModerator
que tome como argumentos un documento de entrada (esto funcionará para borradores o entradas publicadas) y el objeto auth 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 terminar con una sentencia de retorno, y la nuestra mostrará un valor booleano que indicará 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 que los borradores llamen 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 de los campos de una publicación publicada no deben cambiarse, en particular, 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 hacer cumplir estos requisitos en las actualizaciones de las entradas publicadas:
// 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 una lógica reutilizada, puedes hacerlo creando una función nueva: titleIsUnder50Chars
. Con la función nueva, la condición para actualizar una entrada publicada será 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 pruebas fallidas.
9. Comentarios: Permisos de subcolecciones y proveedores 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 las mismas reglas que se aplican al documento principal de la entrada publicada se apliquen a los comentarios. crearás diferentes.
Para escribir reglas de acceso a los comentarios, comienza con la sentencia de coincidencia:
match /published/{postID}/comments/{commentID} {
// `authorUID`: string, required
// `comment`: string, < 500 characters, required
// `createdAt`: timestamp, required
// `editedAt`: timestamp, optional
Lectura de comentarios: No se puede ser anónimo
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 tus pruebas y confirma que se haya aprobado otra prueba.
Creación de comentarios: Verificar una lista de bloqueo
Existen tres condiciones para crear un comentario:
- los usuarios deben 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
. Cómo interpretar estas afecciones de a una a la vez:
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));
Todo el archivo de reglas ahora es:
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 más.
10. Actualización de comentarios: Reglas basadas en el tiempo
La lógica empresarial de los comentarios es 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:
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 los combinas con el operador lógico AND, la regla para actualizar comentarios será 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 haya aprobado otra prueba.
11. Eliminación de comentarios: verificación de la propiedad de los elementos superiores
El autor del comentario, un moderador o el autor de la entrada de blog pueden borrar los comentarios.
Primero, como la función auxiliar que agregaste antes busca un campo authorUID
que podría existir en una publicación o un comentario, puedes volver a usarla 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
Dado que 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 más.
Y todo el archivo de reglas es:
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 se aprobaran y protegieron 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: Explicación del primer desarrollo local con los emuladores
- Video: Cómo usar la configuración de la CI para pruebas basadas en emuladores con acciones de GitHub