1. Antes de comenzar
Cloud Firestore, Cloud Storage para Firebase y Realtime Database dependen de los 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. En este codelab, te explicaremos cómo hacerlo.
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 superior (para instalar Java, sigue 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 las reglas permitan y deshabiliten 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
- Cómo crear funciones personalizadas
- Crea reglas de seguridad basadas en el tiempo
- Implementa una lista de denegación y eliminaciones de forma no definitiva
- Comprender cuándo desnormalizar los datos para cumplir con varios patrones de acceso
2. Configurar
Esta es una aplicación de blogs. Este es un resumen de alto nivel de las funciones de la aplicación:
Borradores de entradas de blog:
- Los usuarios pueden crear borradores de entradas de blog, que se encuentran en la colección
drafts
. - El autor puede seguir actualizando un borrador hasta que esté listo para su publicación.
- Cuando está lista para publicarse, se activa una función de Firebase que crea 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 entradas 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
- Las entradas publicadas permiten realizar comentarios, que son una subcolección de cada entrada publicada.
- Para reducir los abusos, los usuarios deben tener una dirección de correo electrónico verificada y no tener una entidad de bloqueo para dejar un comentario.
- Los comentarios solo se pueden actualizar en el plazo de una hora después de su publicación.
- El autor, 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 para aplicar validaciones de datos y campos obligatorios.
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 imitarás las reglas, 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 de estado inicial, 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 lenta, es posible que este proceso demore unos minutos:
# Move into the functions directory, install dependencies, jump out. $ cd functions && npm install && cd -
Cómo obtener Firebase CLI
El conjunto de herramientas 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 forma local. Esto significa que es momento de iniciar Emulator Suite.
Cómo iniciar los emuladores
La aplicación con la que trabajarás tiene tres colecciones principales de Firestore: drafts
contiene entradas de blog que están en curso, la colección published
contiene las entradas de blog que se publicaron y comments
es una subcolección de entradas publicadas. El repositorio 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, se aprobarán las pruebas. 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 errores. A medida que creas el archivo de reglas, puedes medir el progreso viendo más pruebas aprobadas.
4. Crear borradores para entradas de blog
Debido a que el acceso para las entradas de blog en borrador es muy diferente del acceso para 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 puede acceder a los borradores, estos tienen validaciones para los campos inmutables y obligatorios.
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 sentencia de coincidencia, match /{document=**}
, usa la sintaxis **
para aplicarla de manera recursiva a todos los documentos de las subcolecciones. Y, como se encuentra en el nivel superior, en este momento se aplica la misma regla general a todas las solicitudes, sin importar quién la realice o qué datos intente leer o escribir.
Primero, quita 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 escribas para los borradores controlará quiénes pueden crear los documentos. En esta aplicación, solo la persona que figura como autor puede crear los 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 implica que la app deba agregarlo antes de intentar crear un documento). Dado que 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, concatenelas con el operador lógico AND, &&
. La primera regla 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;
}
}
}
En la terminal, vuelve a ejecutar las pruebas y confirma que se haya aprobado la primera prueba.
5. Actualizar los borradores de las entradas de blog
A continuación, a medida que los autores definen mejor sus borradores de entradas de blog, editarán los documentos en borrador. Cree una regla para las condiciones en las que se puede actualizar una publicación. En primer lugar, solo el autor puede actualizar sus borradores. Ten en cuenta que aquí puedes verificar 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 todas estas condiciones deben cumplirse, concatenelas 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 tus pruebas y confirma que se haya aprobado otra.
6. Borrar y leer borradores: control de acceso basado en atributos
Así como los autores pueden crear y actualizar borradores, también pueden eliminarlos.
resource.data.authorUID == request.auth.uid
Además, los autores que tengan 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, concatenalas con un operador lógico OR, ||
:
allow delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true
Se aplican las mismas condiciones a las lecturas, de modo que se pueda agregar 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 tus pruebas y confirma que ahora se apruebe otra prueba.
7. Lee, crea y borra publicaciones: desnormalización para patrones de acceso diferentes
Debido a que los patrones de acceso para las entradas publicadas y las entradas en borrador son tan diferentes, esta app desnormaliza las publicaciones en colecciones de draft
y published
independientes. Por ejemplo, cualquier persona puede leer las entradas publicadas, pero no se pueden borrar de forma definitiva, mientras que los borradores se pueden borrar, pero solo el autor y los moderadores pueden leerlos. En esta app, cuando un usuario quiere publicar el borrador de una entrada de blog, se activa una función que creará la nueva entrada publicada.
A continuación, escribirás las reglas para las entradas publicadas. Las reglas más simples de escribir son que cualquier persona puede leer las entradas publicadas, pero 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 se agregan a las reglas existentes, todo el archivo de reglas tendrá el siguiente aspecto:
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.
8. Actualiza las entradas publicadas: funciones personalizadas y variables locales
Las condiciones para actualizar una publicación son las siguientes:
- solo lo puede hacer el autor o moderador, y
- debe contener todos los campos obligatorios.
Dado que ya escribiste condiciones para ser autor o moderador, puedes copiarlas y pegarlas, pero con el tiempo esto podría volverse 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, lo llamarás desde varias condiciones.
Cómo crear una función personalizada
Arriba de la declaración de coincidencia para borradores, crea una nueva función llamada isAuthorOrModerator
que tome como argumento un documento de entrada (esto funcionará para borradores o publicaciones 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 configurar las variables isAuthor
y isModerator
. Todas las funciones deben terminar con una sentencia return, y las nuestras muestran 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 que los borradores llamen a esa función y asegúrate 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 publicaciones que también use la nueva función:
allow update: if isAuthorOrModerator(resource.data, request.auth);
Agregar validaciones
Algunos de los campos de una entrada publicada no se deben cambiar. Específicamente, los campos url
, authorUID
y publishedAt
son inmutables. Los otros dos campos, title
, content
y visible
, deben seguir presentes después de una actualización. Agrega condiciones para aplicar estos requisitos en relación con 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"
])
Cómo crear una función personalizada por tu cuenta
Por último, agrega la condición de que el título tenga menos de 50 caracteres. Debido a que esta es una lógica reutilizada, puedes crear una función nueva, titleIsUnder50Chars
. Con la nueva función, la condición para actualizar una entrada publicada pasa a 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 con errores.
9. Comentarios: Subcolecciones y permisos del proveedor de acceso
Las entradas publicadas permiten comentarios, que se almacenan en una subcolección de la entrada publicada (/published/{postID}/comments/{commentID}
). De forma predeterminada, no se aplican las reglas de una colección a las subcolecciones. No querrás que las mismas reglas que se aplican al documento principal de la entrada publicada se apliquen a los comentarios; deberás crear diferentes.
Para escribir reglas de acceso a los comentarios, comienza con la sentencia match:
match /published/{postID}/comments/{commentID} {
// `authorUID`: string, required
// `comment`: string, < 500 characters, required
// `createdAt`: timestamp, required
// `editedAt`: timestamp, optional
Lectura de comentarios: No puede ser anónimo
En esta app, solo los usuarios que hayan creado una cuenta permanente, no una cuenta 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 una más.
Crea comentarios: revisa una lista de rechazo
Hay tres condiciones para crear un comentario:
- un usuario debe tener un correo electrónico verificado
- El comentario debe tener menos de 500 caracteres.
- No pueden estar en una lista de usuarios bloqueados, que está almacenada en Firestore en la colección
bannedUsers
. Realice 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));
Ahora, todo el archivo de reglas tiene el siguiente aspecto:
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 aprueben una o más.
10. Actualización de los comentarios: Reglas basadas en el tiempo
La lógica empresarial de los comentarios indica que su 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
Luego, verifica que el comentario se haya creado en la última hora:
(request.time - resource.data.createdAt) < duration.value(1, 'h');
Si la combinas 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 aprueben una o más.
11. Eliminación de comentarios: verificando la propiedad parental
El autor, el moderador o el autor de la entrada de blog pueden borrarlos.
En primer lugar, debido a que 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 usar 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
Debido a que cualquiera de estas condiciones es suficiente, usa un operador lógico OR 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 aprueben una o más.
Y el archivo de reglas completo 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 fueran exitosas y protegieran la aplicación.
Estos son algunos temas relacionados para profundizar 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 CI para pruebas basadas en emuladores con Acciones de GitHub