Lindungi data Firestore Anda dengan Aturan Keamanan Firebase

Tetap teratur dengan koleksi Simpan dan kategorikan konten berdasarkan preferensi Anda.

1. Sebelum Anda mulai

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

Prasyarat

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

Apa yang akan kamu lakukan?

Di 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 belajar cara:

  • Berikan izin terperinci
  • Terapkan validasi data dan tipe
  • Menerapkan Kontrol Akses Berbasis Atribut
  • Berikan akses berdasarkan metode otentikasi
  • Buat fungsi khusus
  • Buat Aturan Keamanan berbasis waktu
  • Menerapkan daftar tolak dan penghapusan lunak
  • Pahami kapan harus mendenormalisasi data untuk memenuhi beberapa pola akses

2. Siapkan

Ini adalah aplikasi blog. Berikut adalah ringkasan tingkat tinggi dari fungsionalitas aplikasi:

Draf posting blog:

  • Pengguna dapat membuat draf posting blog, yang tinggal di koleksi drafts .
  • Penulis dapat terus memperbarui draf hingga siap untuk diterbitkan.
  • Saat siap untuk diterbitkan, Fungsi Firebase dipicu yang membuat dokumen baru dalam koleksi yang published .
  • Draf dapat dihapus oleh penulis atau oleh moderator situs

Posting blog yang diterbitkan:

  • Postingan yang diterbitkan tidak dapat dibuat oleh pengguna, hanya melalui suatu fungsi.
  • Mereka hanya dapat dihapus dengan lembut, yang memperbarui atribut yang visible menjadi salah.

Komentar

  • Posting yang diterbitkan memungkinkan komentar, yang merupakan subkoleksi pada setiap posting yang diterbitkan.
  • Untuk mengurangi penyalahgunaan, pengguna harus memiliki alamat email yang terverifikasi dan tidak dalam status penyangkalan untuk dapat memberikan komentar.
  • Komentar hanya dapat diperbarui dalam waktu satu jam setelah diposting.
  • Komentar dapat dihapus oleh penulis komentar, penulis posting asli, atau oleh moderator.

Selain aturan akses, Anda akan membuat Aturan Keamanan yang memberlakukan bidang wajib dan validasi data.

Semuanya akan terjadi secara lokal, menggunakan Firebase Emulator Suite.

Dapatkan kode sumbernya

Dalam codelab ini, Anda akan memulai dengan pengujian untuk Aturan Keamanan, tetapi meminimalkan Aturan Keamanan itu sendiri, jadi hal pertama yang perlu Anda lakukan adalah mengkloning sumber untuk menjalankan pengujian:

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

Kemudian pindah ke direktori keadaan awal, tempat Anda akan mengerjakan sisa codelab ini:

$ cd codelab-rules/initial-state

Sekarang, instal dependensi sehingga Anda dapat menjalankan tes. Jika Anda menggunakan koneksi internet yang lebih lambat, ini mungkin memakan waktu satu atau dua menit:

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

Dapatkan Firebase CLI

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

$ npm install -g firebase-tools

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

$ firebase --version
9.10.2

3. Jalankan tes

Di bagian ini, Anda akan menjalankan tes secara lokal. Ini berarti sudah waktunya untuk mem-boot Emulator Suite.

Mulai Emulator

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

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

Pada baris perintah, mulai emulator menggunakan emulators:exec dan jalankan tes:

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

Gulir ke 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 Anda membuat file aturan, Anda dapat mengukur kemajuan dengan melihat lebih banyak tes lulus.

4. Buat draft posting blog.

Karena akses untuk entri blog draf sangat berbeda dengan akses untuk entri blog yang diterbitkan, aplikasi blog ini menyimpan entri blog konsep dalam koleksi terpisah, /drafts . Draf hanya dapat diakses oleh penulis atau moderator, dan memiliki validasi untuk bidang wajib 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 sintaks ** untuk diterapkan secara rekursif ke semua dokumen dalam subkoleksi. Dan karena berada di tingkat atas, 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.

Mulailah dengan menghapus pernyataan kecocokan paling dalam dan menggantinya dengan match /drafts/{draftID} . (Komentar tentang struktur dokumen dapat membantu dalam aturan, dan akan disertakan dalam codelab ini; komentar tersebut selalu 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, konsep hanya dapat dibuat oleh orang yang terdaftar sebagai penulis. Periksa apakah UID orang yang membuat permintaan adalah UID yang sama yang tercantum dalam dokumen.

Kondisi pertama untuk pembuatannya adalah:

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

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

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

Persyaratan terakhir untuk membuat posting blog adalah judul tidak boleh lebih dari 50 karakter:

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

Karena semua kondisi ini harus benar, gabungkan ini bersama dengan operator logika AND, && . 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 tes dan konfirmasikan bahwa tes pertama lulus.

5. Perbarui draf posting blog.

Selanjutnya, saat penulis memperbaiki draf posting blog mereka, mereka akan mengedit dokumen draf. Buat aturan untuk kondisi ketika sebuah posting 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 pembaruan adalah bahwa dua atribut, authorUID dan createdAt tidak boleh berubah:

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

Dan terakhir, judul harus terdiri dari 50 karakter atau kurang:

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

Karena semua kondisi ini harus dipenuhi, gabungkan semuanya 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 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 tes Anda dan konfirmasikan bahwa tes lain lolos.

6. Hapus dan baca draf: Kontrol Akses Berbasis Atribut

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

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

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

request.auth.token.isModerator == true

Karena salah satu dari kondisi ini cukup untuk menghapus, gabungkan dengan operator logika OR, || :

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

Kondisi yang sama berlaku untuk pembacaan, 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 konfirmasikan bahwa pengujian lain telah lulus.

7. Membaca, membuat, dan menghapus posting yang dipublikasikan: denormalisasi untuk pola akses yang berbeda

Karena pola akses untuk posting yang diterbitkan dan posting draf sangat berbeda, aplikasi ini mendenormalisasi posting menjadi draft terpisah dan koleksi yang published . Misalnya, posting yang diterbitkan dapat dibaca oleh siapa saja tetapi tidak dapat dihapus dengan susah payah, sedangkan draf dapat dihapus tetapi hanya dapat dibaca oleh penulis dan moderator. Di aplikasi ini, ketika pengguna ingin menerbitkan draf posting blog, fungsi dipicu yang akan membuat posting baru yang diterbitkan.

Selanjutnya, Anda akan menulis aturan untuk posting yang dipublikasikan. Aturan paling sederhana untuk menulis adalah bahwa posting yang diterbitkan 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 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 tes, dan konfirmasikan bahwa tes lain lolos.

8. Memperbarui posting yang diterbitkan: Fungsi khusus dan variabel lokal

Ketentuan untuk memperbarui posting yang dipublikasikan adalah:

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

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

Buat fungsi khusus

Di atas pernyataan kecocokan untuk draf, buat fungsi baru bernama isAuthorOrModerator yang menggunakan dokumen pos sebagai argumen (ini akan berfungsi untuk draf atau pos yang diterbitkan) dan objek auth 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: ...
    }
  }
}

Gunakan variabel lokal

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

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

Panggil fungsinya

Sekarang Anda akan memperbarui aturan untuk draf untuk memanggil fungsi itu, berhati-hatilah untuk 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 posting yang diterbitkan yang juga menggunakan fungsi baru:

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

Tambahkan validasi

Beberapa bidang dari kiriman yang publishedAt tidak boleh diubah, khususnya bidang url , authorUID , dan publishingAt tidak dapat diubah. Dua bidang lainnya, title dan content , dan visible harus tetap ada setelah pembaruan. Tambahkan ketentuan untuk menerapkan persyaratan ini untuk pembaruan pada posting yang diterbitkan:

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

Buat fungsi kustom Anda sendiri

Dan terakhir, tambahkan syarat bahwa judul harus di bawah 50 karakter. Karena ini adalah logika yang digunakan kembali, Anda dapat melakukannya dengan membuat fungsi baru, titleIsUnder50Chars . Dengan fungsi baru, kondisi untuk memperbarui posting yang diterbitkan 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 tes. Pada titik ini, Anda harus memiliki 5 tes yang lulus dan 4 yang gagal.

9. Komentar: Subkoleksi dan izin penyedia masuk

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

Untuk menulis aturan untuk 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 bisa anonim

Untuk aplikasi ini, hanya pengguna yang telah membuat akun permanen, bukan akun anonim yang dapat membaca komentar. Untuk menegakkan aturan itu, 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 tes Anda, dan konfirmasikan bahwa satu tes lagi lulus.

Membuat komentar: Memeriksa daftar penolakan

Ada tiga syarat untuk membuat komentar:

  • pengguna harus memiliki email terverifikasi
  • komentar harus kurang dari 500 karakter, dan
  • mereka tidak dapat berada di daftar pengguna yang diblokir, yang disimpan di firestore dalam koleksi bannedUsers . 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:

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 charachters
        request.resource.data.comment.size() < 500 &&
        // UID is not on the block list
        !exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
    }
  }
}

Jalankan kembali tes, dan pastikan satu tes lagi lulus.

10. Memperbarui komentar: Aturan berbasis waktu

Logika bisnis untuk komentar adalah bahwa komentar tersebut dapat diedit oleh penulis komentar selama satu jam setelah pembuatan. Untuk mengimplementasikan ini, gunakan stempel waktu createdAt .

Pertama, untuk menetapkan bahwa pengguna adalah penulis:

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

Selanjutnya, bahwa komentar dibuat dalam satu jam terakhir:

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

Menggabungkan ini 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 tes, dan pastikan satu tes lagi lulus.

11. Menghapus komentar: memeriksa kepemilikan orang tua

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

Pertama, karena fungsi pembantu yang Anda tambahkan sebelumnya memeriksa bidang authorUID yang mungkin ada pada kiriman atau komentar, Anda dapat menggunakan kembali fungsi pembantu untuk memeriksa apakah pengguna adalah penulis atau moderator:

isAuthorOrModerator(resource.data, request.auth)

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

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

Karena salah satu dari kondisi ini cukup, gunakan operator OR logis di antara mereka:

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 tes, dan pastikan satu tes 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 charachters
        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 selanjutnya

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

Berikut adalah beberapa topik terkait untuk dipelajari selanjutnya:

  • Posting blog : Cara meninjau kode Aturan Keamanan
  • Codelab : berjalan melalui pengembangan pertama lokal dengan Emulator
  • Video : Cara menggunakan setup CI untuk pengujian berbasis emulator menggunakan GitHub Actions
,

1. Sebelum Anda mulai

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

Prasyarat

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

Apa yang akan kamu lakukan?

Di 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 belajar cara:

  • Berikan izin terperinci
  • Terapkan validasi data dan tipe
  • Menerapkan Kontrol Akses Berbasis Atribut
  • Berikan akses berdasarkan metode otentikasi
  • Buat fungsi khusus
  • Buat Aturan Keamanan berbasis waktu
  • Menerapkan daftar tolak dan penghapusan lunak
  • Pahami kapan harus mendenormalisasi data untuk memenuhi beberapa pola akses

2. Siapkan

Ini adalah aplikasi blog. Berikut adalah ringkasan tingkat tinggi dari fungsionalitas aplikasi:

Draf posting blog:

  • Pengguna dapat membuat draf posting blog, yang tinggal di koleksi drafts .
  • Penulis dapat terus memperbarui draf hingga siap untuk diterbitkan.
  • Saat siap untuk diterbitkan, Fungsi Firebase dipicu yang membuat dokumen baru dalam koleksi yang published .
  • Draf dapat dihapus oleh penulis atau oleh moderator situs

Posting blog yang diterbitkan:

  • Postingan yang diterbitkan tidak dapat dibuat oleh pengguna, hanya melalui suatu fungsi.
  • Mereka hanya dapat dihapus dengan lembut, yang memperbarui atribut yang visible menjadi salah.

Komentar

  • Posting yang diterbitkan memungkinkan komentar, yang merupakan subkoleksi pada setiap posting yang diterbitkan.
  • Untuk mengurangi penyalahgunaan, pengguna harus memiliki alamat email yang terverifikasi dan tidak dalam status penyangkalan untuk dapat memberikan komentar.
  • Komentar hanya dapat diperbarui dalam waktu satu jam setelah diposting.
  • Komentar dapat dihapus oleh penulis komentar, penulis posting asli, atau oleh moderator.

Selain aturan akses, Anda akan membuat Aturan Keamanan yang memberlakukan bidang wajib dan validasi data.

Semuanya akan terjadi secara lokal, menggunakan Firebase Emulator Suite.

Dapatkan kode sumbernya

Dalam codelab ini, Anda akan memulai dengan pengujian untuk Aturan Keamanan, tetapi meminimalkan Aturan Keamanan itu sendiri, jadi hal pertama yang perlu Anda lakukan adalah mengkloning sumber untuk menjalankan pengujian:

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

Kemudian pindah ke direktori keadaan awal, tempat Anda akan mengerjakan sisa codelab ini:

$ cd codelab-rules/initial-state

Sekarang, instal dependensi sehingga Anda dapat menjalankan tes. Jika Anda menggunakan koneksi internet yang lebih lambat, ini mungkin memakan waktu satu atau dua menit:

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

Dapatkan Firebase CLI

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

$ npm install -g firebase-tools

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

$ firebase --version
9.10.2

3. Jalankan tes

Di bagian ini, Anda akan menjalankan tes secara lokal. Ini berarti sudah waktunya untuk mem-boot Emulator Suite.

Mulai Emulator

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

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

Pada baris perintah, mulai emulator menggunakan emulators:exec dan jalankan tes:

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

Gulir ke 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 Anda membuat file aturan, Anda dapat mengukur kemajuan dengan melihat lebih banyak tes lulus.

4. Buat draft posting blog.

Karena akses untuk entri blog draf sangat berbeda dengan akses untuk entri blog yang diterbitkan, aplikasi blog ini menyimpan entri blog konsep dalam koleksi terpisah, /drafts . Draf hanya dapat diakses oleh penulis atau moderator, dan memiliki validasi untuk bidang wajib 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 sintaks ** untuk diterapkan secara rekursif ke semua dokumen dalam subkoleksi. Dan karena berada di tingkat atas, 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.

Mulailah dengan menghapus pernyataan kecocokan paling dalam dan menggantinya dengan match /drafts/{draftID} . (Komentar tentang struktur dokumen dapat membantu dalam aturan, dan akan disertakan dalam codelab ini; komentar tersebut selalu 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, konsep hanya dapat dibuat oleh orang yang terdaftar sebagai penulis. Periksa apakah UID orang yang membuat permintaan adalah UID yang sama yang tercantum dalam dokumen.

Kondisi pertama untuk pembuatannya adalah:

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

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

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

Persyaratan terakhir untuk membuat posting blog adalah judul tidak boleh lebih dari 50 karakter:

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

Karena semua kondisi ini harus benar, gabungkan ini bersama dengan operator logika AND, && . 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 tes dan konfirmasikan bahwa tes pertama lulus.

5. Perbarui draf posting blog.

Selanjutnya, saat penulis memperbaiki draf posting blog mereka, mereka akan mengedit dokumen draf. Buat aturan untuk kondisi ketika sebuah posting 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 pembaruan adalah bahwa dua atribut, authorUID dan createdAt tidak boleh berubah:

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

Dan terakhir, judul harus terdiri dari 50 karakter atau kurang:

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

Karena semua kondisi ini harus dipenuhi, gabungkan semuanya 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 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 tes Anda dan konfirmasikan bahwa tes lain lolos.

6. Hapus dan baca draf: Kontrol Akses Berbasis Atribut

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

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

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

request.auth.token.isModerator == true

Karena salah satu dari kondisi ini cukup untuk menghapus, gabungkan dengan operator logika OR, || :

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

Kondisi yang sama berlaku untuk pembacaan, 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 konfirmasikan bahwa pengujian lain telah lulus.

7. Membaca, membuat, dan menghapus posting yang dipublikasikan: denormalisasi untuk pola akses yang berbeda

Karena pola akses untuk posting yang diterbitkan dan posting draf sangat berbeda, aplikasi ini mendenormalisasi posting menjadi draft terpisah dan koleksi yang published . Misalnya, posting yang diterbitkan dapat dibaca oleh siapa saja tetapi tidak dapat dihapus dengan susah payah, sedangkan draf dapat dihapus tetapi hanya dapat dibaca oleh penulis dan moderator. Di aplikasi ini, ketika pengguna ingin menerbitkan draf posting blog, fungsi dipicu yang akan membuat posting baru yang diterbitkan.

Selanjutnya, Anda akan menulis aturan untuk posting yang dipublikasikan. Aturan paling sederhana untuk menulis adalah bahwa posting yang diterbitkan 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 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 tes, dan konfirmasikan bahwa tes lain lolos.

8. Memperbarui posting yang diterbitkan: Fungsi khusus dan variabel lokal

Ketentuan untuk memperbarui posting yang dipublikasikan adalah:

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

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

Buat fungsi khusus

Di atas pernyataan kecocokan untuk draf, buat fungsi baru bernama isAuthorOrModerator yang menggunakan dokumen pos sebagai argumen (ini akan berfungsi untuk draf atau pos yang diterbitkan) dan objek auth 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: ...
    }
  }
}

Gunakan variabel lokal

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

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

Panggil fungsinya

Sekarang Anda akan memperbarui aturan untuk draf untuk memanggil fungsi itu, berhati-hatilah untuk 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 posting yang diterbitkan yang juga menggunakan fungsi baru:

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

Tambahkan validasi

Beberapa bidang dari kiriman yang publishedAt tidak boleh diubah, khususnya bidang url , authorUID , dan publishingAt tidak dapat diubah. Dua bidang lainnya, title dan content , dan visible harus tetap ada setelah pembaruan. Tambahkan ketentuan untuk menerapkan persyaratan ini untuk pembaruan pada posting yang diterbitkan:

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

Buat fungsi kustom Anda sendiri

Dan terakhir, tambahkan syarat bahwa judul harus di bawah 50 karakter. Karena ini adalah logika yang digunakan kembali, Anda dapat melakukannya dengan membuat fungsi baru, titleIsUnder50Chars . Dengan fungsi baru, kondisi untuk memperbarui posting yang diterbitkan 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 tes. Pada titik ini, Anda harus memiliki 5 tes yang lulus dan 4 yang gagal.

9. Komentar: Subkoleksi dan izin penyedia masuk

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

Untuk menulis aturan untuk 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 bisa anonim

Untuk aplikasi ini, hanya pengguna yang telah membuat akun permanen, bukan akun anonim yang dapat membaca komentar. Untuk menegakkan aturan itu, 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 tes Anda, dan konfirmasikan bahwa satu tes lagi lulus.

Membuat komentar: Memeriksa daftar penolakan

Ada tiga syarat untuk membuat komentar:

  • pengguna harus memiliki email terverifikasi
  • komentar harus kurang dari 500 karakter, dan
  • mereka tidak dapat berada di daftar pengguna yang diblokir, yang disimpan di firestore dalam koleksi bannedUsers . 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:

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 charachters
        request.resource.data.comment.size() < 500 &&
        // UID is not on the block list
        !exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
    }
  }
}

Jalankan kembali tes, dan pastikan satu tes lagi lulus.

10. Memperbarui komentar: Aturan berbasis waktu

Logika bisnis untuk komentar adalah bahwa komentar tersebut dapat diedit oleh penulis komentar selama satu jam setelah pembuatan. Untuk mengimplementasikan ini, gunakan stempel waktu createdAt .

Pertama, untuk menetapkan bahwa pengguna adalah penulis:

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

Selanjutnya, bahwa komentar dibuat dalam satu jam terakhir:

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

Menggabungkan ini 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 tes, dan pastikan satu tes lagi lulus.

11. Menghapus komentar: memeriksa kepemilikan orang tua

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

Pertama, karena fungsi pembantu yang Anda tambahkan sebelumnya memeriksa bidang authorUID yang mungkin ada pada kiriman atau komentar, Anda dapat menggunakan kembali fungsi pembantu untuk memeriksa apakah pengguna adalah penulis atau moderator:

isAuthorOrModerator(resource.data, request.auth)

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

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

Karena salah satu dari kondisi ini cukup, gunakan operator OR logis di antara mereka:

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 tes, dan pastikan satu tes 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 charachters
        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 selanjutnya

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

Berikut adalah beberapa topik terkait untuk dipelajari selanjutnya:

  • Posting blog : Cara meninjau kode Aturan Keamanan
  • Codelab : berjalan melalui pengembangan pertama lokal dengan Emulator
  • Video : How to use set up CI for emulator-based tests using GitHub Actions