Firestore verilerinizi Firebase Güvenlik Kuralları ile koruyun

1. Başlamadan önce

Cloud Firestore, Cloud Storage for Firebase ve Realtime Database, okuma ve yazma erişimi vermek için yazdığınız yapılandırma dosyalarına güvenir. Güvenlik Kuralları adı verilen bu yapılandırma, uygulamanız için bir tür şema görevi de görebilir. Uygulamanızı geliştirmenin en önemli kısımlarından biridir. Ve bu codelab size yol gösterecektir.

Önkoşullar

  • Visual Studio Code, Atom veya Sublime Text gibi basit bir düzenleyici
  • Node.js 8.6.0 veya üzeri (Node.js'yi yüklemek için nvm ; sürümünüzü kontrol etmek için node --version çalıştırın)
  • Java 7 veya üzeri (Java'yı yüklemek için bu talimatları kullanın ; sürümünüzü kontrol etmek için java -version çalıştırın)

ne yapacaksın

Bu kod laboratuvarında, Firestore üzerine kurulmuş basit bir blog platformunu güvence altına alacaksınız. Güvenlik Kurallarına göre birim testleri çalıştırmak için Firestore öykünücüsünü kullanacak ve kuralların beklediğiniz erişime izin verip vermediğinden emin olacaksınız.

Nasıl yapılacağını öğreneceksiniz:

  • Parçalı izinler verme
  • Verileri ve tür doğrulamalarını zorunlu kılın
  • Nitelik Tabanlı Erişim Kontrolü Uygulayın
  • Kimlik doğrulama yöntemine göre erişim izni verin
  • Özel işlevler oluşturun
  • Zamana dayalı Güvenlik Kuralları oluşturun
  • Reddetme listesi ve geçici silme işlemleri uygulayın
  • Birden çok erişim modelini karşılamak için verilerin ne zaman normalleştirilmesi gerektiğini anlayın

2. Kurulum

Bu bir blog uygulamasıdır. İşte uygulama işlevselliğinin üst düzey bir özeti:

Taslak blog gönderileri:

  • Kullanıcılar, drafts koleksiyonunda yer alan taslak blog gönderileri oluşturabilir.
  • Yazar, yayınlanmaya hazır olana kadar bir taslağı güncellemeye devam edebilir.
  • Yayınlanmaya hazır olduğunda, published koleksiyonda yeni bir belge oluşturan bir Firebase İşlevi tetiklenir.
  • Taslaklar, yazar veya site moderatörleri tarafından silinebilir.

Yayınlanan blog yazıları:

  • Yayınlanan gönderiler kullanıcılar tarafından oluşturulamaz, yalnızca bir işlev aracılığıyla oluşturulur.
  • Yalnızca geçici olarak silinebilirler, bu da visible bir özniteliği false olarak günceller.

Yorumlar

  • Yayınlanan gönderiler, yayınlanan her gönderide bir alt koleksiyon olan yorumlara izin verir.
  • Kötüye kullanımı azaltmak için, kullanıcıların doğrulanmış bir e-posta adresine sahip olmaları ve yorum bırakabilmeleri için red listesinde bulunmamaları gerekir.
  • Yorumlar, yayınlandıktan sonra yalnızca bir saat içinde güncellenebilir.
  • Yorumlar, yorum yazarı, orijinal gönderinin yazarı veya moderatörler tarafından silinebilir.

Erişim kurallarına ek olarak, gerekli alanları ve veri doğrulamalarını zorunlu kılan Güvenlik Kuralları oluşturacaksınız.

Firebase Emulator Suite kullanılarak her şey yerel olarak gerçekleşecek.

Kaynak kodunu al

Bu codelab'de, Güvenlik Kuralları için testlerle başlayacaksınız, ancak Güvenlik Kuralları minimum düzeyde olacak, bu nedenle yapmanız gereken ilk şey, testleri çalıştırmak için kaynağı klonlamaktır:

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

Ardından, bu codelab'in geri kalanında çalışacağınız ilk durum dizinine geçin:

$ cd codelab-rules/initial-state

Şimdi, testleri çalıştırabilmeniz için bağımlılıkları kurun. Daha yavaş bir internet bağlantınız varsa bu işlem bir veya iki dakika sürebilir:

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

Firebase CLI'yi edinin

Testleri çalıştırmak için kullanacağınız Emulator Suite, aşağıdaki komutla makinenize kurulabilen Firebase CLI'nin (komut satırı arayüzü) bir parçasıdır:

$ npm install -g firebase-tools

Ardından, CLI'nin en son sürümüne sahip olduğunuzu onaylayın. Bu codelab, 8.4.0 veya sonraki sürümlerle çalışmalıdır, ancak sonraki sürümler daha fazla hata düzeltmesi içerir.

$ firebase --version
9.10.2

3. Testleri çalıştırın

Bu bölümde, testleri yerel olarak çalıştıracaksınız. Bu, Emulator Suite'i başlatma zamanının geldiği anlamına gelir.

Emülatörleri Başlat

Çalışacağınız uygulamanın üç ana Firestore koleksiyonu vardır: drafts , devam etmekte olan blog gönderilerini içerir, published koleksiyon, yayınlanmış blog gönderilerini içerir ve comments , yayınlanan gönderilerin bir alt koleksiyonudur. Depo, bir kullanıcının drafts , published ve comments koleksiyonlarındaki belgeleri oluşturması, okuması, güncellemesi ve silmesi için gerekli olan kullanıcı özniteliklerini ve diğer koşulları tanımlayan Güvenlik Kuralları için birim testleriyle birlikte gelir. Bu testleri geçmek için Güvenlik Kurallarını yazacaksınız.

Başlamak için veritabanınız kilitlendi: veritabanına okuma ve yazma işlemleri evrensel olarak reddedildi ve tüm testler başarısız oldu. Güvenlik Kurallarını yazdıkça testler geçecektir. Testleri görmek için editörünüzde functions/test.js dosyasını açın.

Komut satırında, emulators:exec kullanarak emülatörleri başlatın ve testleri çalıştırın:

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

Çıktının en üstüne gidin:

$ 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

...

Şu anda 9 arıza var. Kural dosyasını oluştururken, daha fazla testin geçtiğini izleyerek ilerlemeyi ölçebilirsiniz.

4. Blog yazısı taslakları oluşturun.

Taslak blog gönderilerine erişim, yayınlanan blog gönderilerine erişimden çok farklı olduğundan, bu blog uygulaması, taslak blog gönderilerini ayrı bir koleksiyonda saklar /drafts . Taslaklara yalnızca yazar veya moderatör tarafından erişilebilir ve gerekli ve değişmez alanlar için doğrulamaları vardır.

firestore.rules dosyasını açtığınızda, bir varsayılan kurallar dosyası bulacaksınız:

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

match deyimi, match /{document=**} , alt koleksiyonlardaki tüm belgelere yinelemeli olarak uygulamak için ** sözdizimini kullanıyor. Ve en üst düzeyde olduğu için, isteği kimin yaptığına veya hangi verileri okumaya veya yazmaya çalıştıklarına bakılmaksızın, şu anda tüm istekler için aynı genel kural geçerlidir.

En içteki match deyimini kaldırıp yerine match /drafts/{draftID} ile başlayın. (Belgelerin yapısına ilişkin yorumlar, kurallarda yardımcı olabilir ve bu kod laboratuvarına dahil edilecektir; bunlar her zaman isteğe bağlıdır.)

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

Taslaklar için yazacağınız ilk kural, belgeleri kimlerin oluşturabileceğini kontrol edecektir. Bu uygulamada taslaklar yalnızca yazar olarak listelenen kişi tarafından oluşturulabilir. Talepte bulunan kişinin UID'sinin belgede listelenen UID ile aynı olduğunu kontrol edin.

Oluşturmanın ilk koşulu şöyle olacaktır:

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

Daha sonra, belgeler yalnızca üç gerekli alanı, authorUID , createdAt ve title içermeleri halinde oluşturulabilir. (Kullanıcı createdAt alanını sağlamaz; bu, uygulamanın bir belge oluşturmaya çalışmadan önce bunu eklemesi gerektiğini zorunlu kılar.) Yalnızca özniteliklerin oluşturulduğunu kontrol etmeniz gerektiğinden, request.resource tüm özelliklere sahip olduğunu kontrol edebilirsiniz. bu anahtarlar:

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

Bir blog gönderisi oluşturmak için son koşul, başlığın 50 karakterden uzun olmamasıdır:

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

Tüm bu koşulların doğru olması gerektiğinden, bunları mantıksal AND işleci && ile birleştirin. İlk kural şöyle olur:

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

Terminalde testleri yeniden çalıştırın ve ilk testin geçtiğini onaylayın.

5. Blog yazısı taslaklarını güncelleyin.

Ardından, yazarlar taslak blog gönderilerini iyileştirdikçe taslak belgeleri düzenlerler. Bir gönderinin güncellenebileceği koşullar için bir kural oluşturun. İlk olarak, yalnızca yazar taslaklarını güncelleyebilir. Burada zaten yazılmış olan UID'yi kontrol ettiğinizi unutmayın, resource.data.authorUID :

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

Bir güncelleme için ikinci gereklilik, iki özniteliğin, authorUID ve createdAt değişmemesidir:

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

Son olarak, başlık en fazla 50 karakter olmalıdır:

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

Bu koşulların hepsinin karşılanması gerektiğinden, bunları && ile birleştirin:

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;

Tam kurallar şöyle olur:

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

Testlerinizi yeniden çalıştırın ve başka bir testin geçtiğini onaylayın.

6. Taslakları silin ve okuyun: Nitelik Tabanlı Erişim Kontrolü

Yazarlar taslak oluşturup güncelleyebildikleri gibi taslakları da silebilir.

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

Ek olarak, auth belirteçlerinde isModerator özniteliğine sahip yazarların taslakları silmesine izin verilir:

request.auth.token.isModerator == true

Bu koşullardan herhangi biri silme işlemi için yeterli olduğundan, bunları mantıksal bir VEYA işleci || :

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

Aynı koşullar okumalar için de geçerlidir, böylece kurala izin eklenebilir:

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

Tam kurallar şimdi:

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

Testlerinizi yeniden çalıştırın ve başka bir testin şimdi geçtiğini onaylayın.

7. Yayınlanan gönderileri okur, oluşturur ve siler: farklı erişim kalıpları için denormalize etme

Yayınlanan gönderiler ve taslak gönderiler için erişim kalıpları çok farklı olduğundan, bu uygulama gönderileri normalden çıkarıp ayrı draft ve published koleksiyonlara ayırır. Örneğin, yayınlanan gönderiler herkes tarafından okunabilir ancak kalıcı olarak silinemezken, taslaklar silinebilir ancak yalnızca yazar ve moderatörler tarafından okunabilir. Bu uygulamada, bir kullanıcı taslak bir blog gönderisi yayınlamak istediğinde, yayınlanan yeni gönderiyi oluşturacak bir işlev tetiklenir.

Ardından, yayınlanan gönderiler için kuralları yazacaksınız. Yazılması gereken en basit kurallar, yayınlanan gönderilerin herkes tarafından okunabilmesi ve hiç kimse tarafından oluşturulamaması veya silinememesidir. Bu kuralları ekleyin:

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

Bunları mevcut kurallara eklediğinizde, kural dosyasının tamamı şu hale gelir:

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

Testleri yeniden çalıştırın ve başka bir testin geçtiğini onaylayın.

8. Yayınlanan gönderileri güncelleme: Özel işlevler ve yerel değişkenler

Yayınlanmış bir gönderiyi güncelleme koşulları şunlardır:

  • yalnızca yazar veya moderatör tarafından yapılabilir ve
  • gerekli tüm alanları içermelidir.

Yazar veya moderatör olmak için zaten koşullar yazdığınızdan, koşulları kopyalayıp yapıştırabilirsiniz, ancak zamanla bunları okumak ve sürdürmek zorlaşabilir. Bunun yerine, yazar veya moderatör olma mantığını özetleyen özel bir işlev oluşturacaksınız. Ardından, onu birden fazla koşuldan arayacaksınız.

Özel bir işlev oluşturun

Taslaklar için eşleştirme ifadesinin üzerinde, isAuthorOrModerator adında, bir gönderi belgesini (bu, taslaklar veya yayınlanmış gönderiler için çalışır) ve kullanıcının auth nesnesini bağımsız değişken olarak alan yeni bir işlev oluşturun:

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

Yerel değişkenleri kullan

İşlev içinde, isAuthor ve isModerator değişkenlerini ayarlamak için let anahtar sözcüğünü kullanın. Tüm işlevler bir dönüş ifadesiyle bitmelidir ve bizimki, değişkenlerden birinin doğru olup olmadığını gösteren bir boole döndürür:

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

işlevi çağır

Şimdi, ilk bağımsız değişken olarak resource.data iletmeye dikkat ederek, bu işlevi çağırmak için taslaklar kuralını güncelleyeceksiniz:

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

Artık, yeni işlevi de kullanan yayınlanmış gönderileri güncellemek için bir koşul yazabilirsiniz:

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

Doğrulama ekle

Yayınlanmış bir gönderinin bazı alanları değiştirilmemelidir, özellikle url , authorUID publishedAt alanları sabittir. Diğer iki alan, title ve content ve visible bir güncellemeden sonra da mevcut olmalıdır. Yayınlanan gönderilerdeki güncellemeler için bu gereksinimleri uygulamak üzere koşullar ekleyin:

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

Kendi başınıza özel bir işlev oluşturun

Ve son olarak, başlığın 50 karakterin altında olması koşulunu ekleyin. Bu yeniden kullanılan bir mantık olduğundan, bunu, titleIsUnder50Chars adlı yeni bir işlev oluşturarak yapabilirsiniz. Yeni işlevle, yayınlanan bir gönderiyi güncelleme koşulu şu hale gelir:

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

Ve tam kural dosyası:

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

Testleri tekrar çalıştırın. Bu noktada, 5 geçme ve 4 başarısız olmanız gerekir.

9. Yorumlar: Alt koleksiyonlar ve oturum açma sağlayıcı izinleri

Yayınlanan gönderiler yorumlara izin verir ve yorumlar yayınlanan gönderinin bir alt koleksiyonunda saklanır ( /published/{postID}/comments/{commentID} ). Varsayılan olarak bir koleksiyonun kuralları alt koleksiyonlar için geçerli değildir. Yayınlanan gönderinin üst belgesi için geçerli olan kuralların yorumlara da uygulanmasını istemezsiniz; farklı olanları yapacaksın.

Yorumlara erişim kuralları yazmak için, eşleşme ifadesiyle başlayın:

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

Yorumları okumak: Anonim olamaz

Bu uygulama için, anonim bir hesap değil, yalnızca kalıcı bir hesap oluşturan kullanıcılar yorumları okuyabilir. Bu kuralı uygulamak için, her auth.token nesnesinde bulunan sign_in_provider özniteliğine bakın:

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

Testlerinizi yeniden çalıştırın ve bir testin daha geçtiğini onaylayın.

Yorum oluşturma: Reddetme listesini kontrol etme

Yorum oluşturmak için üç koşul vardır:

  • bir kullanıcının doğrulanmış bir e-postası olmalıdır
  • yorum 500 karakterden az olmalı ve
  • bannedUsers deposunda banli Kullanıcılar koleksiyonunda depolanan yasaklı kullanıcılar listesinde olamazlar. Bu koşulları birer birer ele alarak:
request.auth.token.email_verified == true
request.resource.data.comment.size() < 500
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));

Yorum oluşturmak için son kural şudur:

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

Tüm kurallar dosyası şimdi:

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

Testleri yeniden çalıştırın ve bir testin daha geçtiğinden emin olun.

10. Yorumları güncelleme: Zamana dayalı kurallar

Yorumlar için iş mantığı, yorum yazarı tarafından oluşturulduktan sonra bir saat boyunca düzenlenebilmesidir. Bunu uygulamak için, createdAt zaman damgasını kullanın.

İlk olarak, kullanıcının yazar olduğunu belirlemek için:

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

Ardından, yorumun son bir saat içinde oluşturulduğu:

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

Bunları mantıksal AND operatörüyle birleştirerek, yorumları güncelleme kuralı şu hale gelir:

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

Testleri yeniden çalıştırın ve bir testin daha geçtiğinden emin olun.

11. Yorumları silme: ebeveyn sahipliğini kontrol etme

Yorumlar, yorum yazarı, moderatör veya blog gönderisinin yazarı tarafından silinebilir.

İlk olarak, daha önce eklediğiniz yardımcı işlev, bir gönderide veya yorumda bulunabilecek bir authorUID alanını kontrol ettiğinden, kullanıcının yazar mı yoksa moderatör mü olduğunu kontrol etmek için yardımcı işlevi yeniden kullanabilirsiniz:

isAuthorOrModerator(resource.data, request.auth)

Kullanıcının blog gönderisinin yazarı olup olmadığını kontrol etmek için get kullanarak Firestore'da gönderiyi arayın:

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

Bu koşullardan herhangi biri yeterli olduğundan, aralarında mantıksal bir VEYA işleci kullanın:

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;

Testleri yeniden çalıştırın ve bir testin daha geçtiğinden emin olun.

Ve tüm kurallar dosyası:

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. Sonraki adımlar

Tebrikler! Tüm testlerin geçmesini sağlayan ve uygulamayı güvence altına alan Güvenlik Kurallarını yazdınız!

Sırada incelenecek bazı ilgili konular şunlardır:

  • Blog gönderisi : Kod incelemesi Güvenlik Kuralları nasıl yapılır?
  • Codelab : Öykünücüler ile yerel ilk geliştirmeyi gözden geçirmek
  • Video : GitHub Eylemlerini kullanarak öykünücü tabanlı testler için CI kurulumu nasıl kullanılır?