Menggunakan kondisi dalam Aturan Keamanan Realtime Database

Panduan ini dibuat berdasarkan panduan mempelajari bahasa Aturan Keamanan Firebase inti untuk menunjukkan cara menambahkan kondisi ke Aturan Keamanan Firebase Realtime Database.

Elemen penyusun utama dari Aturan Keamanan Realtime Database adalah kondisi. Kondisi adalah ekspresi Boolean yang menentukan diizinkannya atau ditolaknya suatu operasi. Untuk aturan dasar, penggunaan literal true dan false sebagai kondisi berfungsi sangat baik. Namun, bahasa Aturan Keamanan Realtime Database memberi Anda cara untuk menulis kondisi yang lebih kompleks yang dapat:

  • Memeriksa autentikasi pengguna
  • Mengevaluasi data yang ada terhadap data yang baru saja dikirim
  • Mengakses dan membandingkan berbagai bagian database Anda
  • Memvalidasi data yang masuk
  • Menggunakan struktur kueri yang masuk untuk logika keamanan

Menggunakan Variabel $ untuk Menangkap Segmen Jalur

Anda dapat menangkap bagian dari jalur untuk membaca atau menulis dengan menyatakan variabel tangkapan berawalan $. Hal ini berfungsi sebagai karakter pengganti, dan menyimpan nilai kunci tersebut untuk digunakan di dalam kondisi aturan:

{
  "rules": {
    "rooms": {
      // this rule applies to any child of /rooms/, the key for each room id
      // is stored inside $room_id variable for reference
      "$room_id": {
        "topic": {
          // the room's topic can be changed if the room id has "public" in it
          ".write": "$room_id.contains('public')"
        }
      }
    }
  }
}

Variabel $ dinamis juga dapat digunakan secara paralel dengan nama jalur yang tetap. Dalam contoh ini, kita menggunakan variabel $other untuk mendeklarasikan aturan .validate yang memastikan bahwa widget tidak memiliki turunan selain title dan color. Setiap operasi tulis yang menyebabkan terciptanya turunan tambahan akan gagal.

{
  "rules": {
    "widget": {
      // a widget can have a title or color attribute
      "title": { ".validate": true },
      "color": { ".validate": true },

      // but no other child paths are allowed
      // in this case, $other means any key excluding "title" and "color"
      "$other": { ".validate": false }
    }
  }
}

Autentikasi

Salah satu pola aturan keamanan yang paling umum adalah mengontrol akses berdasarkan status autentikasi pengguna. Misalnya, aplikasi Anda mungkin hanya ingin mengizinkan pengguna yang telah login untuk menulis data.

Jika aplikasi Anda menggunakan Firebase Authentication, variabel request.auth akan berisi informasi autentikasi untuk klien yang meminta data. Untuk mengetahui informasi lebih lanjut tentang request.auth, lihat dokumentasi referensi .

Firebase Authentication terintegrasi dengan Firebase Realtime Database agar Anda dapat mengontrol akses data setiap pengguna menggunakan kondisi. Setelah pengguna melakukan autentikasi, variabel auth di aturan Aturan Keamanan Realtime Database Anda akan diisi dengan informasi pengguna. Informasi ini berisi ID unik (uid) serta data akun tertaut, seperti ID Facebook atau alamat email, dan info lainnya. Jika menerapkan penyedia autentikasi khusus, Anda dapat menambahkan kolom Anda sendiri ke payload autentikasi pengguna.

Bagian ini menjelaskan cara menggabungkan bahasa Aturan Keamanan Firebase Realtime Database dengan informasi autentikasi tentang pengguna Anda. Dengan menggabungkan dua konsep ini, Anda bisa mengontrol akses ke data berdasarkan identitas pengguna.

Variabel auth

Variabel auth yang telah ditetapkan dalam aturan bernilai null sebelum autentikasi dilakukan.

Jika pengguna sudah diautentikasi dengan Firebase Authentication, variabel tersebut akan berisi atribut berikut:

penyedia Metode autentikasi yang digunakan ("sandi", "anonim", "facebook", "github", "google", atau "twitter").
uid ID pengguna yang unik, harus unik di semua penyedia.
token Isi token ID Firebase Auth. Baca dokumentasi referensi mengenai auth.token untuk mendapatkan informasi selengkapnya.

Berikut adalah contoh aturan yang menggunakan variabel auth untuk memastikan bahwa setiap pengguna hanya dapat menulis ke jalur khusus pengguna:

{
  "rules": {
    "users": {
      "$user_id": {
        // grants write access to the owner of this user account
        // whose uid must exactly match the key ($user_id)
        ".write": "$user_id === auth.uid"
      }
    }
  }
}

Membuat Struktur Database untuk Mendukung Kondisi Autentikasi

Sebaiknya buat struktur database Anda dengan cara yang memudahkan penulisan Aturan. Satu pola umum untuk menyimpan data pengguna di Realtime Database adalah dengan menyimpan semua pengguna di satu node users yang turunannya merupakan nilai uid untuk setiap pengguna. Jika Anda ingin membatasi akses ke data ini sehingga hanya pengguna yang login yang bisa melihat data miliknya, aturannya akan terlihat seperti ini.

{
  "rules": {
    "users": {
      "$uid": {
        ".read": "auth !== null && auth.uid === $uid"
      }
    }
  }
}

Bekerja dengan Klaim Kustom Authentication

Untuk aplikasi yang memerlukan kontrol akses kustom untuk pengguna yang berbeda, Firebase Authentication memungkinkan developer menetapkan klaim pada pengguna Firebase. Klaim ini dapat diakses dalam variabel auth.token di aturan Anda. Berikut adalah contoh aturan yang menggunakan klaim kustom hasEmergencyTowel:

{
  "rules": {
    "frood": {
      // A towel is about the most massively useful thing an interstellar
      // hitchhiker can have
      ".read": "auth.token.hasEmergencyTowel === true"
    }
  }
}

Developer yang membuat token autentikasi kustomnya sendiri bisa menambahkan klaim ke token tersebut secara opsional. Klaim ini tersedia pada variabel auth.token di aturan Anda.

Data yang Sudah Ada vs. Data Baru

Variabel data yang telah ditetapkan digunakan untuk merujuk ke data sebelum operasi tulis terjadi. Sebaliknya, variabel newData berisi data baru yang akan ada jika operasi tulis berhasil. newData mewakili hasil gabungan dari data baru yang sedang ditulis dan data yang sudah ada.

Sebagai ilustrasi, aturan ini memungkinkan kita membuat data baru atau menghapus yang sudah ada, tetapi bukan untuk mengubah data selain null yang sudah ada:

// we can write as long as old data or new data does not exist
// in other words, if this is a delete or a create, but not an update
".write": "!data.exists() || !newData.exists()"

Mereferensikan Data di Jalur lain

Setiap data bisa digunakan sebagai kriteria untuk aturan. Dengan menggunakan variabel root, data, dan newData yang telah ditetapkan, kita dapat mengakses setiap jalur yang ada sebelum atau setelah peristiwa tulis.

Perhatikan contoh berikut, yang memungkinkan operasi tulis selama nilai node /allow_writes/ adalah true, node induk tidak memiliki flag readOnly yang disetel, dan terdapat turunan bernama foo di data yang baru ditulis:

".write": "root.child('allow_writes').val() === true &&
          !data.parent().child('readOnly').exists() &&
          newData.child('foo').exists()"

Memvalidasi Data

Menerapkan struktur data serta memvalidasi format dan konten data harus dilakukan menggunakan aturan .validate, yang dijalankan hanya setelah aturan .write berhasil memberikan akses. Berikut adalah contoh definisi aturan .validate yang hanya mengizinkan tanggal dalam format YYYY-MM-DD antara tahun 1900-2099, yang diperiksa menggunakan ekspresi reguler.

".validate": "newData.isString() &&
              newData.val().matches(/^(19|20)[0-9][0-9][-\\/. ](0[1-9]|1[012])[-\\/. ](0[1-9]|[12][0-9]|3[01])$/)"

Aturan .validate adalah satu-satunya jenis aturan keamanan yang tidak bersifat menurun. Jika ada aturan validasi yang gagal pada record turunan, seluruh operasi tulis akan ditolak. Selain itu, definisi validasi akan diabaikan jika data dihapus (yaitu ketika nilai baru yang ditulis berupa null).

Meskipun terkesan sepele, sebenarnya ini adalah fitur penting untuk menulis Aturan Keamanan Firebase Realtime Database yang efektif. Perhatikan aturan berikut ini:

{
  "rules": {
    // write is allowed for all paths
    ".write": true,
    "widget": {
      // a valid widget must have attributes "color" and "size"
      // allows deleting widgets (since .validate is not applied to delete rules)
      ".validate": "newData.hasChildren(['color', 'size'])",
      "size": {
        // the value of "size" must be a number between 0 and 99
        ".validate": "newData.isNumber() &&
                      newData.val() >= 0 &&
                      newData.val() <= 99"
      },
      "color": {
        // the value of "color" must exist as a key in our mythical
        // /valid_colors/ index
        ".validate": "root.child('valid_colors/' + newData.val()).exists()"
      }
    }
  }
}

Dengan mempertimbangkan varian ini, lihatlah hasil untuk operasi tulis berikut:

JavaScript
var ref = db.ref("/widget");

// PERMISSION_DENIED: does not have children color and size
ref.set('foo');

// PERMISSION DENIED: does not have child color
ref.set({size: 22});

// PERMISSION_DENIED: size is not a number
ref.set({ size: 'foo', color: 'red' });

// SUCCESS (assuming 'blue' appears in our colors list)
ref.set({ size: 21, color: 'blue'});

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
ref.child('size').set(99);
Objective-C
Catatan: Produk Firebase ini tidak tersedia di target App Clip.
FIRDatabaseReference *ref = [[[FIRDatabase database] reference] child: @"widget"];

// PERMISSION_DENIED: does not have children color and size
[ref setValue: @"foo"];

// PERMISSION DENIED: does not have child color
[ref setValue: @{ @"size": @"foo" }];

// PERMISSION_DENIED: size is not a number
[ref setValue: @{ @"size": @"foo", @"color": @"red" }];

// SUCCESS (assuming 'blue' appears in our colors list)
[ref setValue: @{ @"size": @21, @"color": @"blue" }];

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
[[ref child:@"size"] setValue: @99];
Swift
Catatan: Produk Firebase ini tidak tersedia di target App Clip.
var ref = FIRDatabase.database().reference().child("widget")

// PERMISSION_DENIED: does not have children color and size
ref.setValue("foo")

// PERMISSION DENIED: does not have child color
ref.setValue(["size": "foo"])

// PERMISSION_DENIED: size is not a number
ref.setValue(["size": "foo", "color": "red"])

// SUCCESS (assuming 'blue' appears in our colors list)
ref.setValue(["size": 21, "color": "blue"])

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
ref.child("size").setValue(99);
Java
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("widget");

// PERMISSION_DENIED: does not have children color and size
ref.setValue("foo");

// PERMISSION DENIED: does not have child color
ref.child("size").setValue(22);

// PERMISSION_DENIED: size is not a number
Map<String,Object> map = new HashMap<String, Object>();
map.put("size","foo");
map.put("color","red");
ref.setValue(map);

// SUCCESS (assuming 'blue' appears in our colors list)
map = new HashMap<String, Object>();
map.put("size", 21);
map.put("color","blue");
ref.setValue(map);

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
ref.child("size").setValue(99);
REST
# PERMISSION_DENIED: does not have children color and size
curl -X PUT -d 'foo' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# PERMISSION DENIED: does not have child color
curl -X PUT -d '{"size": 22}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# PERMISSION_DENIED: size is not a number
curl -X PUT -d '{"size": "foo", "color": "red"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# SUCCESS (assuming 'blue' appears in our colors list)
curl -X PUT -d '{"size": 21, "color": "blue"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# If the record already exists and has a color, this will
# succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
# will fail to validate
curl -X PUT -d '99' \
https://docs-examples.firebaseio.com/rest/securing-data/example/size.json

Sekarang mari kita lihat struktur yang sama, tetapi dengan aturan .write, sebagai ganti .validate:

{
  "rules": {
    // this variant will NOT allow deleting records (since .write would be disallowed)
    "widget": {
      // a widget must have 'color' and 'size' in order to be written to this path
      ".write": "newData.hasChildren(['color', 'size'])",
      "size": {
        // the value of "size" must be a number between 0 and 99, ONLY IF WE WRITE DIRECTLY TO SIZE
        ".write": "newData.isNumber() && newData.val() >= 0 && newData.val() <= 99"
      },
      "color": {
        // the value of "color" must exist as a key in our mythical valid_colors/ index
        // BUT ONLY IF WE WRITE DIRECTLY TO COLOR
        ".write": "root.child('valid_colors/'+newData.val()).exists()"
      }
    }
  }
}

Dalam varian ini, salah satu operasi berikut akan berhasil diterapkan:

JavaScript
var ref = new Firebase(URL + "/widget");

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
ref.set({size: 99999, color: 'red'});

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
ref.child('size').set(99);
Objective-C
Catatan: Produk Firebase ini tidak tersedia di target App Clip.
Firebase *ref = [[Firebase alloc] initWithUrl:URL];

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
[ref setValue: @{ @"size": @9999, @"color": @"red" }];

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
[[ref childByAppendingPath:@"size"] setValue: @99];
Swift
Catatan: Produk Firebase ini tidak tersedia di target App Clip.
var ref = Firebase(url:URL)

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
ref.setValue(["size": 9999, "color": "red"])

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
ref.childByAppendingPath("size").setValue(99)
Java
Firebase ref = new Firebase(URL + "/widget");

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
Map<String,Object> map = new HashMap<String, Object>();
map.put("size", 99999);
map.put("color", "red");
ref.setValue(map);

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
ref.child("size").setValue(99);
REST
# ALLOWED? Even though size is invalid, widget has children color and size,
# so write is allowed and the .write rule under color is ignored
curl -X PUT -d '{size: 99999, color: "red"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# ALLOWED? Works even if widget does not exist, allowing us to create a widget
# which is invalid and does not have a valid color.
# (allowed by the write rule under "color")
curl -X PUT -d '99' \
https://docs-examples.firebaseio.com/rest/securing-data/example/size.json

Hal ini menunjukkan perbedaan antara aturan .write dan .validate. Seperti yang ditunjukkan, semua aturan ini harus ditulis menggunakan .validate, dengan kemungkinan pengecualian aturan newData.hasChildren(), yang akan bergantung pada perizinan penghapusan.

Aturan Berbasis Kueri

Meskipun tidak dapat menggunakan aturan sebagai filter, Anda dapat membatasi akses ke subset data menggunakan parameter kueri dalam aturan Anda. Gunakan ekspresi query. dalam aturan Anda untuk memberikan akses baca atau tulis berdasarkan parameter kueri.

Misalnya, aturan berbasis kueri berikut menggunakan aturan keamanan berbasis pengguna dan aturan berbasis kueri untuk membatasi akses ke data yang ada di koleksi baskets hanya bagi keranjang belanja milik pengguna aktif:

"baskets": {
  ".read": "auth.uid !== null &&
            query.orderByChild === 'owner' &&
            query.equalTo === auth.uid" // restrict basket access to owner of basket
}

Kueri berikut, yang mencakup parameter kueri dalam aturan, akan berhasil diterapkan:

db.ref("baskets").orderByChild("owner")
                 .equalTo(auth.currentUser.uid)
                 .on("value", cb)                 // Would succeed

Namun, kueri yang tidak menyertakan parameter dalam aturan akan gagal dengan error PermissionDenied:

db.ref("baskets").on("value", cb)                 // Would fail with PermissionDenied

Anda juga dapat menggunakan aturan berbasis kueri untuk membatasi jumlah data yang didownload klien melalui operasi baca.

Misalnya, aturan berikut membatasi akses baca hanya untuk 1.000 hasil kueri pertama, yang diurutkan berdasarkan prioritas:

messages: {
  ".read": "query.orderByKey &&
            query.limitToFirst <= 1000"
}

// Example queries:

db.ref("messages").on("value", cb)                // Would fail with PermissionDenied

db.ref("messages").limitToFirst(1000)
                  .on("value", cb)                // Would succeed (default order by key)

Ekspresi query. berikut tersedia di Aturan Keamanan Realtime Database.

Ekspresi aturan berbasis kueri
Ekspresi Jenis Deskripsi
query.orderByKey
query.orderByPriority
query.orderByValue
boolean Benar untuk kueri yang diurutkan berdasarkan kunci, prioritas, atau nilai. Salah untuk kueri yang tidak diurutkan.
query.orderByChild string
null
Gunakan string untuk mewakili jalur relatif ke node turunan. Misalnya, query.orderByChild === "address/zip". Jika kueri tidak diurutkan berdasarkan node turunan, nilai ini adalah null.
query.startAt
query.endAt
query.equalTo
string
number
boolean
null
Mengambil ikatan kueri yang mengeksekusi, atau menampilkan null jika tidak ada ikatan yang ditetapkan.
query.limitToFirst
query.limitToLast
number
null
Mengambil batasan kueri pelaksana, atau menampilkan null jika tidak ada batasan yang ditetapkan.

Langkah berikutnya

Setelah membaca pembahasan kondisi di atas, Anda telah memiliki pemahaman yang lebih mendalam tentang Aturan dan siap untuk:

Mempelajari cara menangani kasus penggunaan inti, dan mempelajari alur kerja untuk mengembangkan, menguji, dan men-deploy Aturan:

Mempelajari fitur Aturan yang bersifat spesifik untuk Realtime Database: