Mengontrol akses ke kolom tertentu

Halaman ini menjabarkan konsep dalam Membuat Struktur Aturan Keamanan dan Menulis Kondisi untuk Aturan Keamanan guna menjelaskan cara menggunakan Aturan Keamanan Firestore untuk membuat aturan yang memungkinkan klien melakukan operasi pada beberapa kolom dalam sebuah dokumen, tetapi tidak pada yang lain.

Ada kalanya Anda ingin mengontrol perubahan pada dokumen, bukan di level dokumen, tetapi di level kolom.

Misalnya, Anda dapat mengizinkan klien untuk membuat atau mengubah dokumen, tetapi tidak mengizinkan mereka untuk mengedit kolom tertentu dalam dokumen tersebut. Selain itu, Anda dapat menerapkan aturan agar setiap dokumen yang dibuat oleh klien selalu berisi kumpulan kolom tertentu. Panduan ini membahas cara menyelesaikan beberapa tugas tersebut menggunakan Aturan Keamanan Firestore.

Mengizinkan akses baca hanya untuk kolom tertentu

Operasi baca di Firestore dilakukan di level dokumen. Hanya dokumen lengkap yang bisa Anda ambil. Pengambilan dokumen secara parsial tidak dapat dilakukan. Untuk mencegah pengguna membaca kolom tertentu dalam dokumen diperlukan lebih dari sekadar aturan keamanan.

Cara terbaik untuk menyembunyikan kolom tertentu dalam dokumen dari sebagian pengguna adalah dengan menempatkannya di dokumen terpisah. Misalnya, Anda dapat mempertimbangkan untuk membuat dokumen dalam subkoleksi private, seperti:

/employees/{emp_id}

  name: "Alice Hamilton",
  department: 461,
  start_date: <timestamp>

/employees/{emp_id}/private/finances

    salary: 80000,
    bonus_mult: 1.25,
    perf_review: 4.2

Kemudian, Anda dapat menambahkan aturan keamanan dengan level akses yang berbeda untuk kedua koleksi. Dalam contoh ini, kita menggunakan klaim autentikasi kustom agar hanya pengguna dengan klaim autentikasi kustom role, yang setara dengan Finance, yang dapat melihat informasi keuangan karyawan.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow any logged in user to view the public employee data
    match /employees/{emp_id} {
      allow read: if request.resource.auth != null
      // Allow only users with the custom auth claim of "Finance" to view
      // the employee's financial data
      match /private/finances {
        allow read: if request.resource.auth &&
          request.resource.auth.token.role == 'Finance'
      }
    }
  }
}

Membatasi kolom pada pembuatan dokumen

Firestore tidak memiliki skema, artinya tidak ada batasan di level database untuk kolom yang ada dalam dokumen. Meskipun fleksibilitas ini dapat mempermudah pengembangan, ada kalanya Anda ingin memastikan klien hanya dapat membuat dokumen yang berisi kolom tertentu, atau tidak berisi kolom lain.

Anda dapat membuat aturan ini dengan memeriksa metode keys dari objek request.resource.data. Ini adalah daftar semua kolom yang ingin ditulis klien dalam dokumen baru ini. Dengan menggabungkan kumpulan kolom ini dengan fungsi seperti hasOnly() atau hasAny(), Anda dapat menambahkan logika yang membatasi jenis dokumen yang dapat ditambahkan pengguna ke Firestore.

Mewajibkan kolom tertentu dalam dokumen baru

Misalnya Anda ingin memastikan bahwa semua dokumen yang dibuat dalam koleksi restaurant berisi setidaknya kolom name, location, dan city. Anda dapat melakukannya dengan memanggil hasAll() di daftar kunci dalam dokumen baru.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document contains a name
    // location, and city field
    match /restaurant/{restId} {
      allow create: if request.resource.data.keys().hasAll(['name', 'location', 'city']);
    }
  }
}

Tindakan ini memungkinkan pembuatan kolom restoran bersama dengan kolom lain. Selain itu, tindakan ini juga memastikan semua dokumen yang dibuat oleh klien berisi setidaknya tiga kolom ini.

Melarang kolom tertentu dalam dokumen baru

Demikian pula, Anda dapat mencegah klien agar tidak membuat dokumen yang berisi kolom tertentu dengan menggunakan hasAny() terhadap daftar kolom terlarang. Metode ini bernilai benar jika dokumen berisi salah satu kolom tersebut. Oleh karena itu, Anda perlu menegasikan hasilnya untuk melarang kolom tertentu.

Misalnya, di contoh berikut, klien tidak diizinkan membuat dokumen yang berisi kolom average_score atau rating_count karena kolom tersebut akan ditambahkan oleh panggilan server pada tahap berikutnya.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document does *not*
    // contain an average_score or rating_count field.
    match /restaurant/{restId} {
      allow create: if (!request.resource.data.keys().hasAny(
        ['average_score', 'rating_count']));
    }
  }
}

Membuat daftar kolom yang diizinkan untuk dokumen baru

Daripada melarang kolom tertentu dalam dokumen baru, Anda dapat membuat daftar kolom tertentu yang secara eksplisit diizinkan dalam dokumen baru. Kemudian, Anda dapat menggunakan fungsi hasOnly() untuk memastikan bahwa setiap dokumen baru yang dibuat hanya berisi kolom tersebut (atau subkumpulan kolom tersebut) dan bukan kolom yang lain.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document doesn't contain
    // any fields besides the ones listed below.
    match /restaurant/{restId} {
      allow create: if (request.resource.data.keys().hasOnly(
        ['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

Menggabungkan kolom wajib dan opsional

Anda dapat menggabungkan operasi hasAll dan hasOnly dalam aturan keamanan untuk mewajibkan beberapa kolom dan mengizinkan kolom lainnya. Misalnya, contoh ini mengharuskan semua dokumen baru berisi kolom name, location, dan city, serta secara opsional mengizinkan kolom address, hours, dan cuisine.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document has a name,
    // location, and city field, and optionally address, hours, or cuisine field
    match /restaurant/{restId} {
      allow create: if (request.resource.data.keys().hasAll(['name', 'location', 'city'])) &&
       (request.resource.data.keys().hasOnly(
           ['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

Dalam praktiknya, Anda mungkin ingin memindahkan logika ini ke fungsi bantuan untuk menghindari duplikasi kode serta untuk menggabungkan kolom wajib dan opsional ke dalam satu daftar dengan mudah, seperti:

service cloud.firestore {
  match /databases/{database}/documents {
    function verifyFields(required, optional) {
      let allAllowedFields = required.concat(optional);
      return request.resource.data.keys().hasAll(required) &&
        request.resource.data.keys().hasOnly(allAllowedFields);
    }
    match /restaurant/{restId} {
      allow create: if verifyFields(['name', 'location', 'city'],
        ['address', 'hours', 'cuisine']);
    }
  }
}

Membatasi kolom pada pembaruan

Salah satu praktik keamanan yang sering dilakukan adalah dengan hanya mengizinkan klien mengedit sebagian kolom. Anda tidak dapat melakukannya hanya dengan melihat daftar request.resource.data.keys() yang dijelaskan di bagian sebelumnya, karena daftar ini mewakili tampilan dokumen lengkap setelah pembaruan, dan oleh karena itu juga akan berisi kolom yang tidak diubah klien.

Namun, jika menggunakan fungsi diff(), Anda dapat membandingkan request.resource.data dengan objek resource.data, yang mewakili dokumen dalam database sebelum pembaruan. Tindakan ini akan membuat objek mapDiff, yang merupakan objek yang berisi semua perubahan di antara dua peta yang berbeda.

Dengan memanggil metode affectedKeys() di mapDiff ini, Anda dapat membuat kumpulan kolom yang telah diubah dalam pengeditan. Lalu, Anda dapat menggunakan fungsi seperti hasOnly() atau hasAny() untuk memastikan kumpulan ini berisi (atau tidak berisi) item tertentu.

Mencegah perubahan pada kolom tertentu

Dengan menggunakan metode hasAny() di kumpulan yang dihasilkan oleh affectedKeys() lalu menegasikan hasilnya, Anda dapat menolak permintaan klien yang mencoba mengubah kolom yang tidak ingin Anda ubah.

Misalnya, Anda dapat mengizinkan klien untuk memperbarui informasi tentang restoran, tetapi tidak mengizinkan mereka untuk mengubah skor rata-rata atau jumlah ulasannya.

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
      // Allow the client to update a document only if that document doesn't
      // change the average_score or rating_count fields
      allow update: if (!request.resource.data.diff(resource.data).affectedKeys()
        .hasAny(['average_score', 'rating_count']));
    }
  }
}

Mengizinkan perubahan di kolom tertentu saja

Daripada menentukan kolom yang tidak ingin diubah, Anda dapat menggunakan fungsi hasOnly() untuk menentukan daftar kolom yang ingin diubah. Secara umum, hal ini dianggap lebih aman karena operasi tulis ke kolom dokumen baru tidak diizinkan secara default sampai Anda mengizinkannya secara eksplisit dalam aturan keamanan.

Misalnya, daripada melarang kolom average_score dan rating_count, Anda dapat membuat aturan keamanan yang membuat klien hanya dapat mengubah kolom name, location, city, address, hours, dan cuisine.

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
    // Allow a client to update only these 6 fields in a document
      allow update: if (request.resource.data.diff(resource.data).affectedKeys()
        .hasOnly(['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

Artinya, jika dokumen restoran menyertakan kolom telephone pada suatu iterasi mendatang aplikasi Anda, upaya untuk mengedit kolom tersebut akan gagal sampai Anda kembali dan menambahkan kolom tersebut ke daftar hasOnly() dalam aturan keamanan.

Menerapkan jenis kolom

Efek lain dari tidak adanya skema pada Firestore adalah tidak adanya penerapan di tingkat database untuk jenis data yang dapat disimpan di kolom tertentu. Namun, hal ini dapat Anda terapkan dalam aturan keamanan, dengan operator is.

Misalnya, aturan keamanan berikut memberlakukan bahwa kolom score ulasan harus berupa bilangan bulat, kolom headline, content, dan author_name adalah string, dan kolom review_date adalah stempel waktu.

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
      // Restaurant rules go here...
      match /review/{reviewId} {
        allow create: if (request.resource.data.score is int &&
          request.resource.data.headline is string &&
          request.resource.data.content is string &&
          request.resource.data.author_name is string &&
          request.resource.data.review_date is timestamp
        );
      }
    }
  }
}

Jenis data yang valid untuk operator is adalah bool, bytes, float, int, list, latlng, number, path, map, string, dan timestamp. Operator is juga mendukung jenis data constraint, duration, set, dan map_diff. Namun, karena data tersebut dibuat oleh bahasa aturan keamanan itu sendiri dan bukan oleh klien, Anda jarang menggunakannya dalam penerapan yang praktis.

Jenis data list dan map tidak mendukung argumen jenis atau generik. Dengan kata lain, Anda dapat menggunakan aturan keamanan untuk memastikan bahwa kolom tertentu berisi daftar atau peta, tetapi Anda tidak dapat memastikan bahwa sebuah kolom harus berisi daftar semua bilangan bulat atau semua string.

Demikian pula, Anda dapat menggunakan aturan keamanan untuk menerapkan nilai jenis untuk entri tertentu dalam daftar atau peta (menggunakan notasi tanda kurung atau nama kunci). Namun, tidak ada pintasan untuk menerapkan jenis data dari semua anggota di peta atau daftar secara sekaligus.

Misalnya, aturan berikut memastikan bahwa kolom tags dalam dokumen berisi daftar dan entri pertama berupa string. Aturan tersebut juga memastikan bahwa kolom product berisi peta yang kemudian berisi nama produk yang berupa string dan kuantitas yang berupa bilangan bulat.

service cloud.firestore {
  match /databases/{database}/documents {
  match /orders/{orderId} {
    allow create: if request.resource.data.tags is list &&
      request.resource.data.tags[0] is string &&
      request.resource.data.product is map &&
      request.resource.data.product.name is string &&
      request.resource.data.product.quantity is int
      }
    }
  }
}

Jenis kolom harus diterapkan saat membuat dan memperbarui dokumen. Oleh karena itu, Anda dapat mempertimbangkan untuk membuat fungsi bantuan yang dapat Anda panggil di bagian pembuatan dan pembaruan pada aturan keamanan Anda.

service cloud.firestore {
  match /databases/{database}/documents {

  function reviewFieldsAreValidTypes(docData) {
     return docData.score is int &&
          docData.headline is string &&
          docData.content is string &&
          docData.author_name is string &&
          docData.review_date is timestamp;
  }

   match /restaurant/{restId} {
      // Restaurant rules go here...
      match /review/{reviewId} {
        allow create: if reviewFieldsAreValidTypes(request.resource.data) &&
          // Other rules may go here
        allow update: if reviewFieldsAreValidTypes(request.resource.data) &&
          // Other rules may go here
      }
    }
  }
}

Menerapkan jenis untuk kolom opsional

Penting untuk diingat bahwa memanggil request.resource.data.foo di dokumen yang tidak memiliki foo akan menghasilkan error. Oleh karena itu, setiap aturan keamanan yang melakukan panggilan tersebut akan menolak permintaan tersebut. Anda dapat menangani situasi ini menggunakan metode get di request.resource.data. Dengan metode get, Anda dapat memberikan argumen default untuk kolom yang Anda ambil dari peta jika kolom tersebut tidak ada.

Misalnya, jika dokumen ulasan juga berisi kolom photo_url opsional dan kolom tags opsional yang ingin Anda verifikasi, yang masing-masing berupa string dan daftar, Anda dapat melakukannya dengan menulis ulang fungsi reviewFieldsAreValidTypes seperti berikut:

  function reviewFieldsAreValidTypes(docData) {
     return docData.score is int &&
          docData.headline is string &&
          docData.content is string &&
          docData.author_name is string &&
          docData.review_date is timestamp &&
          docData.get('photo_url', '') is string &&
          docData.get('tags', []) is list;
  }

Tindakan ini menolak dokumen jika tags ada, tetapi tidak berupa daftar, sementara tetap mengizinkan dokumen yang tidak berisi kolom tags (atau photo_url).

Penulisan parsial tidak pernah diizinkan

Satu catatan terakhir tentang Aturan Keamanan Firestore adalah bahwa aturan mengizinkan klien untuk membuat perubahan pada dokumen, atau menolak seluruh hasil edit. Anda tidak dapat membuat aturan keamanan yang mengizinkan operasi tulis di sebagian kolom dalam dokumen sekaligus menolak operasi tulis di kolom lainnya dalam operasi yang sama.