Melindungi data Firestore Anda dengan Aturan Keamanan Firebase

1. Sebelum memulai

Cloud Firestore, Cloud Storage for Firebase, dan Realtime Database mengandalkan file konfigurasi yang Anda tulis untuk memberikan akses baca dan tulis. Konfigurasi tersebut, yang disebut Aturan Keamanan, juga dapat bertindak sebagai jenis skema untuk aplikasi Anda. Ini adalah salah satu bagian terpenting dalam pengembangan aplikasi Anda. Dan codelab ini akan memandu Anda.

Prasyarat

  • Editor sederhana seperti Visual Studio Code, Atom, atau Sublime Text
  • Node.js 8.6.0 atau yang lebih tinggi (untuk menginstal Node.js, gunakan nvm; untuk memeriksa versi Anda, jalankan node --version)
  • Java 7 atau yang lebih tinggi (untuk menginstal Java gunakan petunjuk ini; untuk memeriksa versi Anda, jalankan java -version)

Yang akan Anda lakukan

Dalam codelab ini, Anda akan mengamankan platform blog sederhana yang dibangun di Firestore. Anda akan menggunakan emulator Firestore untuk menjalankan pengujian unit terhadap Aturan Keamanan, dan memastikan bahwa aturan mengizinkan dan melarang akses yang Anda harapkan.

Anda akan mempelajari cara:

  • Memberikan izin terperinci
  • Terapkan validasi data dan jenis
  • Menerapkan Kontrol Akses Berbasis Atribut
  • Memberikan akses berdasarkan metode autentikasi
  • Membuat fungsi kustom
  • Membuat Aturan Keamanan berbasis waktu
  • Mengimplementasikan daftar tolak dan penghapusan sementara
  • Memahami kapan harus melakukan denormalisasi data untuk memenuhi beberapa pola akses

2. Siapkan

Ini adalah aplikasi blogging. Berikut adalah ringkasan tingkat tinggi dari fungsi aplikasi:

Draf postingan blog:

  • Pengguna dapat membuat draf postingan blog, yang ada di koleksi drafts.
  • Penulis dapat terus memperbarui draf hingga siap untuk dipublikasikan.
  • Setelah siap dipublikasikan, fungsi Firebase akan dipicu, yang membuat dokumen baru di koleksi published.
  • Draf dapat dihapus oleh penulis atau moderator situs

Postingan blog yang dipublikasikan:

  • Postingan yang dipublikasikan tidak dapat dibuat oleh pengguna, hanya dapat dibuat melalui fungsi.
  • Atribut ini hanya dapat dihapus untuk sementara, yang akan mengubah atribut visible menjadi salah (false).

Komentar

  • Postingan yang dipublikasikan memungkinkan komentar, yang merupakan subkoleksi pada setiap postingan yang dipublikasikan.
  • Untuk mengurangi penyalahgunaan, pengguna harus memiliki alamat email terverifikasi dan bukan merupakan penolak untuk memberikan komentar.
  • Komentar hanya dapat diperbarui dalam waktu satu jam setelah diposting.
  • Komentar dapat dihapus oleh penulis komentar, penulis postingan asli, atau oleh moderator.

Selain aturan akses, Anda akan membuat Aturan Keamanan yang menerapkan kolom wajib diisi dan validasi data.

Semuanya akan terjadi secara lokal, menggunakan Firebase Emulator Suite.

Mendapatkan kode sumber

Dalam codelab ini, Anda akan memulai dengan pengujian Aturan Keamanan, tetapi meniru Aturan Keamanan itu sendiri. Jadi, hal pertama yang perlu Anda lakukan adalah meng-clone sumber untuk menjalankan pengujian:

$ git clone https://github.com/FirebaseExtended/codelab-rules.git

Kemudian pindah ke direktori status awal, tempat Anda akan bekerja selama sisa codelab ini:

$ cd codelab-rules/initial-state

Sekarang, instal dependensi agar Anda dapat menjalankan pengujian. Jika Anda menggunakan koneksi internet yang lebih lambat, proses ini mungkin perlu waktu satu atau dua menit:

# Move into the functions directory, install dependencies, jump out.
$ cd functions && npm install && cd -

Mendapatkan Firebase CLI

Emulator Suite yang akan Anda gunakan untuk menjalankan pengujian adalah bagian dari Firebase CLI (antarmuka command line) yang dapat diinstal di mesin Anda dengan perintah berikut:

$ npm install -g firebase-tools

Selanjutnya, pastikan Anda memiliki CLI versi terbaru. Codelab ini harus berfungsi dengan versi 8.4.0 atau yang lebih tinggi, tetapi versi yang lebih baru menyertakan lebih banyak perbaikan bug.

$ firebase --version
9.10.2

3. Menjalankan pengujian

Di bagian ini, Anda akan menjalankan pengujian secara lokal. Ini berarti saatnya melakukan booting Emulator Suite.

Memulai Emulator

Aplikasi yang akan Anda gunakan memiliki tiga koleksi utama Firestore: drafts berisi postingan blog yang sedang berlangsung, koleksi published berisi postingan blog yang telah dipublikasikan, dan comments adalah subkoleksi pada postingan yang dipublikasikan. Repositori ini dilengkapi dengan pengujian unit untuk Aturan Keamanan yang menentukan atribut pengguna dan kondisi lain yang diperlukan bagi pengguna untuk membuat, membaca, memperbarui, dan menghapus dokumen dalam koleksi drafts, published, dan comments. Anda akan menulis Aturan Keamanan agar pengujian tersebut lulus.

Untuk memulai, database Anda dikunci: operasi baca dan tulis ke database ditolak secara universal, dan semua pengujian gagal. Saat Anda menulis Aturan Keamanan, pengujian akan lulus. Untuk melihat pengujian, buka functions/test.js di editor Anda.

Pada command line, mulai emulator menggunakan emulators:exec dan jalankan pengujian:

$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test"

Scroll ke bagian atas output:

$ 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

...

Saat ini ada 9 kegagalan. Saat membuat file aturan, Anda dapat mengukur progres dengan melihat lebih banyak pengujian yang lulus.

4. Membuat draf postingan blog.

Karena akses untuk draf postingan blog sangat berbeda dengan akses untuk postingan blog yang dipublikasikan, aplikasi blogging ini menyimpan draf postingan blog dalam koleksi terpisah, /drafts. Draf hanya dapat diakses oleh penulis atau moderator, dan memiliki validasi untuk kolom yang wajib diisi dan tidak dapat diubah.

Membuka file firestore.rules, Anda akan menemukan file aturan default:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }
  }
}

Pernyataan kecocokan, match /{document=**}, menggunakan sintaksis ** untuk diterapkan secara berulang ke semua dokumen dalam subkoleksi. Karena berada di tingkat teratas, saat ini aturan menyeluruh yang sama berlaku untuk semua permintaan, tidak peduli siapa yang membuat permintaan atau data apa yang mereka coba baca atau tulis.

Mulai dengan menghapus pernyataan kecocokan paling dalam dan menggantinya dengan match /drafts/{draftID}. (Komentar tentang struktur dokumen dapat berguna dalam aturan, dan akan disertakan dalam codelab ini; komentar ini selalu bersifat opsional.)

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
    }
  }
}

Aturan pertama yang akan Anda tulis untuk draf akan mengontrol siapa yang dapat membuat dokumen. Dalam aplikasi ini, draf hanya dapat dibuat oleh orang yang tercantum sebagai penulis. Periksa apakah UID orang yang membuat permintaan adalah UID yang sama dengan yang tercantum dalam dokumen.

Kondisi pertama untuk pembuatan adalah:

request.resource.data.authorUID == request.auth.uid

Selanjutnya, dokumen hanya dapat dibuat jika menyertakan tiga kolom wajib diisi, authorUID,createdAt, dan title. (Pengguna tidak memberikan kolom createdAt; ini memaksakan bahwa aplikasi harus menambahkannya sebelum mencoba membuat dokumen.) Karena Anda hanya perlu memeriksa apakah atribut sedang dibuat, Anda dapat memeriksa apakah request.resource memiliki semua kunci tersebut:

request.resource.data.keys().hasAll([
  "authorUID",
  "createdAt",
  "title"
])

Persyaratan akhir untuk membuat postingan blog adalah panjang judul tidak boleh lebih dari 50 karakter:

request.resource.data.title.size() < 50

Karena semua kondisi ini harus benar, gabungkan kondisi ini bersama dengan operator AND logis, &&. Aturan pertama menjadi:

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;
    }
  }
}

Di terminal, jalankan kembali pengujian dan pastikan pengujian pertama berhasil.

5. Memperbarui draf postingan blog.

Selanjutnya, saat penulis menyempurnakan draf postingan blognya, mereka akan mengedit draf dokumen. Buat aturan untuk ketentuan kapan postingan dapat diperbarui. Pertama, hanya penulis yang dapat memperbarui drafnya. Perhatikan bahwa di sini Anda memeriksa UID yang sudah ditulis,resource.data.authorUID:

resource.data.authorUID == request.auth.uid

Persyaratan kedua untuk update adalah dua atribut, authorUID dan createdAt tidak boleh berubah:

request.resource.data.diff(resource.data).unchangedKeys().hasAll([
    "authorUID",
    "createdAt"
]);

Terakhir, judul harus terdiri dari 50 karakter atau kurang:

request.resource.data.title.size() < 50;

Karena semua kondisi ini harus terpenuhi, gabungkan dengan &&:

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;

Aturan lengkapnya akan menjadi:

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;
    }
  }
}

Jalankan kembali pengujian Anda dan pastikan pengujian lainnya lulus.

6. Menghapus dan membaca draf: Kontrol Akses Berbasis Atribut

Sama halnya seperti penulis yang dapat membuat dan memperbarui draf, mereka juga dapat menghapus draf.

resource.data.authorUID == request.auth.uid

Selain itu, penulis dengan atribut isModerator di token autentikasi mereka diizinkan untuk menghapus draf:

request.auth.token.isModerator == true

Karena salah satu kondisi ini memadai untuk penghapusan, gabungkan dengan operator OR logis, ||:

allow delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true

Kondisi yang sama berlaku untuk operasi baca, sehingga izin dapat ditambahkan ke aturan:

allow read, delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true

Aturan lengkapnya sekarang:

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;
    }
  }
}

Jalankan kembali pengujian Anda dan pastikan pengujian lain kini berhasil.

7. Membaca, membuat, dan menghapus postingan yang dipublikasikan: melakukan denormalisasi untuk berbagai pola akses

Karena pola akses untuk postingan dan draf yang dipublikasikan sangat berbeda, aplikasi ini akan mendenormalisasi postingan menjadi koleksi draft dan published yang terpisah. Misalnya, postingan yang dipublikasikan dapat dibaca oleh siapa saja tetapi tidak dapat dihapus secara permanen, sedangkan draf dapat dihapus tetapi hanya dapat dibaca oleh penulis dan moderator. Di aplikasi ini, saat pengguna ingin memublikasikan draf postingan blog, sebuah fungsi akan dipicu, yang akan membuat postingan baru yang dipublikasikan.

Berikutnya, Anda akan menulis aturan untuk postingan yang dipublikasikan. Aturan paling sederhana untuk ditulis adalah bahwa postingan yang dipublikasikan dapat dibaca oleh siapa saja, dan tidak dapat dibuat atau dihapus oleh siapa pun. Tambahkan aturan ini:

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;
}

Menambahkan ini ke aturan yang ada, seluruh file aturan akan menjadi:

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;
    }
  }
}

Jalankan kembali pengujian, dan pastikan pengujian lainnya lulus.

8. Memperbarui postingan yang dipublikasikan: Fungsi kustom dan variabel lokal

Kondisi untuk memperbarui postingan yang dipublikasikan adalah:

  • hanya dapat dilakukan oleh penulis atau moderator, dan
  • isian harus berisi semua bidang yang diperlukan.

Karena Anda telah menulis ketentuan untuk menjadi penulis atau moderator, Anda dapat menyalin dan menempelkan ketentuan tersebut, tetapi seiring waktu kondisi tersebut dapat menjadi sulit untuk dibaca dan dikelola. Sebagai gantinya, Anda akan membuat fungsi kustom yang mengenkapsulasi logika untuk menjadi penulis atau moderator. Kemudian, Anda akan memanggilnya dari beberapa kondisi.

Membuat fungsi kustom

Di atas pernyataan kecocokan untuk draf, buat fungsi baru bernama isAuthorOrModerator yang menggunakan dokumen postingan sebagai argumen (ini akan berfungsi untuk draf atau postingan yang dipublikasikan) dan objek autentikasi pengguna:

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: ...
    }
  }
}

Menggunakan variabel lokal

Di dalam fungsi, gunakan kata kunci let untuk menetapkan variabel isAuthor dan isModerator. Semua fungsi harus diakhiri dengan pernyataan return, dan fungsi kita akan menampilkan boolean yang menunjukkan apakah salah satu variabel bernilai benar:

function isAuthorOrModerator(post, auth) {
  let isAuthor = auth.uid == post.authorUID;
  let isModerator = auth.token.isModerator == true;
  return isAuthor || isModerator;
}

Memanggil fungsi

Sekarang Anda akan memperbarui aturan draf untuk memanggil fungsi tersebut, dengan hati-hati meneruskan resource.data sebagai argumen pertama:

  // Draft blog posts
  match /drafts/{draftID} {
    ...
    // Can be deleted by author or moderator
    allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
  }

Sekarang Anda dapat menulis kondisi untuk memperbarui postingan yang dipublikasikan yang juga menggunakan fungsi baru:

allow update: if isAuthorOrModerator(resource.data, request.auth);

Menambahkan validasi

Beberapa kolom postingan yang dipublikasikan tidak boleh diubah, khususnya kolom url, authorUID, dan publishedAt yang tidak dapat diubah. Dua kolom lainnya, title dan content, serta visible harus tetap ada setelah update. Tambahkan ketentuan untuk menerapkan persyaratan ini untuk update pada postingan yang dipublikasikan:

// 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"
])

Membuat fungsi kustom sendiri

Terakhir, tambahkan ketentuan bahwa panjang judul kurang dari 50 karakter. Karena ini adalah logika yang digunakan ulang, Anda dapat melakukannya dengan membuat fungsi baru, titleIsUnder50Chars. Dengan fungsi baru ini, kondisi untuk memperbarui postingan yang dipublikasikan menjadi:

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);

Dan file aturan lengkapnya adalah:

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);
    }
  }
}

Jalankan kembali pengujian. Pada titik ini, Anda seharusnya memiliki 5 pengujian yang lulus dan 4 pengujian yang gagal.

9. Komentar: Subkoleksi dan izin penyedia login

Postingan yang dipublikasikan mengizinkan komentar, dan komentar disimpan di subkoleksi postingan yang dipublikasikan (/published/{postID}/comments/{commentID}). Secara default, aturan koleksi tidak berlaku untuk subkoleksi. Anda tidak ingin aturan yang sama yang berlaku pada dokumen induk dari postingan yang dipublikasikan diterapkan ke komentar; Anda akan membuat aturan yang berbeda.

Untuk menulis aturan dalam mengakses komentar, mulailah dengan pernyataan kecocokan:

match /published/{postID}/comments/{commentID} {
  // `authorUID`: string, required
  // `comment`: string, < 500 characters, required
  // `createdAt`: timestamp, required
  // `editedAt`: timestamp, optional

Membaca komentar: Tidak boleh anonim

Untuk aplikasi ini, hanya pengguna yang telah membuat akun permanen, bukan akun anonim, yang dapat membaca komentar. Untuk menerapkan aturan tersebut, cari atribut sign_in_provider yang ada di setiap objek auth.token:

allow read: if request.auth.token.firebase.sign_in_provider != "anonymous";

Jalankan kembali pengujian Anda, dan pastikan satu pengujian lagi berhasil.

Membuat komentar: Memeriksa daftar tolak

Ada tiga ketentuan untuk membuat komentar:

  • pengguna harus memiliki email terverifikasi
  • komentar harus kurang dari 500 karakter, dan
  • mereka tidak bisa tercantum dalam daftar pengguna yang diblokir, yang disimpan di firestore dalam koleksi bannedUsers. Dengan mengambil kondisi ini satu per satu:
request.auth.token.email_verified == true
request.resource.data.comment.size() < 500
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));

Aturan terakhir untuk membuat komentar adalah:

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));

Seluruh file aturan sekarang menjadi:

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));
    }
  }
}

Jalankan kembali pengujian, dan pastikan satu pengujian lagi lulus.

10. Memperbarui komentar: Aturan berbasis waktu

Logika bisnis untuk komentar adalah komentar dapat diedit oleh penulis komentar selama satu jam setelah dibuat. Untuk menerapkannya, gunakan stempel waktu createdAt.

Pertama, untuk menetapkan bahwa pengguna adalah penulisnya:

request.auth.uid == resource.data.authorUID

Selanjutnya, komentar dibuat dalam satu jam terakhir:

(request.time - resource.data.createdAt) < duration.value(1, 'h');

Jika digabungkan dengan operator AND logis, aturan untuk memperbarui komentar menjadi:

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');

Jalankan kembali pengujian, dan pastikan satu pengujian lagi lulus.

11. Menghapus komentar: memeriksa kepemilikan orang tua

Komentar dapat dihapus oleh penulis komentar, moderator, atau penulis postingan blog.

Pertama, karena fungsi bantuan yang Anda tambahkan sebelumnya memeriksa kolom authorUID yang mungkin ada di postingan atau komentar, Anda dapat menggunakan kembali fungsi bantuan untuk memeriksa apakah pengguna tersebut adalah penulis atau moderator:

isAuthorOrModerator(resource.data, request.auth)

Untuk memeriksa apakah pengguna adalah penulis postingan blog, gunakan get untuk mencari postingan di Firestore:

request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID

Karena salah satu kondisi ini memadai, gunakan operator OR logis di antara keduanya:

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;

Jalankan kembali pengujian, dan pastikan satu pengujian lagi lulus.

Dan seluruh file aturan adalah:

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. Langkah berikutnya

Selamat! Anda telah menulis Aturan Keamanan yang membuat semua pengujian lulus dan mengamankan aplikasi.

Berikut adalah beberapa topik terkait yang akan dibahas selanjutnya:

  • Postingan blog: Cara membuat kode untuk meninjau Aturan Keamanan
  • Codelab: memandu pengembangan pertama lokal dengan Emulator
  • Video: Cara menggunakan CI penyiapan untuk pengujian berbasis emulator menggunakan GitHub Actions