Firestore verilerinizi Firebase Güvenlik Kuralları ile koruyun

Koleksiyonlar ile düzeninizi koruyun İçeriği tercihlerinize göre kaydedin ve kategorilere ayırın.

1. Başlamadan önce

Cloud Firestore, Firebase için Cloud Storage 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 işlevi de görebilir. Uygulamanızı geliştirmenin en önemli parçalarından biridir. Ve bu kod laboratuvarı size yol gösterecek.

Ö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 kullanın ; sürümünüzü kontrol etmek için node --version komutunu çalıştırın)
  • Java 7 veya üstü (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 karşı birim testleri yapmak için Firestore öykünücüsünü kullanacak ve kuralların beklediğiniz erişime izin verdiğinden ve izin vermediğinden emin olacaksınız.

Şunları nasıl yapacağınızı öğreneceksiniz:

  • Ayrıntılı izinler verin
  • Verileri ve tür doğrulamalarını uygula
  • Özniteliğe Dayalı Erişim Denetimi 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 uygulayın ve geçici silmeler
  • Birden çok erişim modelini karşılamak için verilerin ne zaman normal dışı hale getirileceğ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 yaşayan 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şturulabilir.
  • Yalnızca geçici olarak silinebilirler, bu da visible bir özelliğ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 yorum bırakabilmeleri için doğrulanmış bir e-posta adresine sahip olmaları ve reddetme durumunda bulunmamaları gerekir.
  • Yorumlar ancak yayınlandıktan sonra 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ın

Bu kod laboratuvarında, Güvenlik Kuralları için testlerle başlayacaksınız, ancak Güvenlik Kurallarının kendisini en aza indireceksiniz, 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 kod laboratuvarının geri kalanı için çalışacağınız ilk durum dizinine gidin:

$ 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 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 yüklenebilen 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 üzeri 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ın

Çalışacağınız uygulamanın üç ana Firestore koleksiyonu vardır: drafts , devam eden 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 gereken kullanıcı özniteliklerini ve diğer koşulları tanımlayan Güvenlik Kuralları için birim testleri ile birlikte gelir. Bu testleri geçmek için Güvenlik Kurallarını yazacaksınız.

Başlamak için, veritabanınız kilitlenir: veritabanına okuma ve yazma işlemleri evrensel olarak reddedilir ve tüm testler başarısız olur. Güvenlik Kuralları yazdıkça testler geçecektir. Testleri görmek için editörünüzde functions/test.js 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 başarısızlık var. Kurallar 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ştirilemez alanlar için doğrulamaları vardır.

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

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

Match ifadesi, match /{document=**} , alt koleksiyonlardaki tüm belgelere özyinelemeli 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 aynı genel kural tüm istekler için geçerlidir.

En içteki eşleşme ifadesini kaldırarak ve bunu match /drafts/{draftID} ile değiştirerek 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 sadece yazar olarak listelenen kişi tarafından oluşturulabilir. Talepte bulunan kişinin UID'sinin belgede listelenen UID ile aynı olup olmadığını kontrol edin.

Oluşturma için ilk koşul şöyle olacaktır:

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

Ardından, belgeler yalnızca, authorUID , createdAt ve title olmak üzere üç zorunlu alanı içeriyorsa oluşturulabilir. (Kullanıcı createdAt alanını sağlamaz; bu, uygulamanın bir belge oluşturmaya çalışmadan önce onu eklemesini zorunlu kılar.) Yalnızca niteliklerin oluşturulup oluşturulmadığını kontrol etmeniz gerektiğinden, request.resource tümünün olup olmadığını kontrol edebilirsiniz. bu anahtarlar:

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

Bir blog yazısı oluşturmanın son şartı, 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 operatörü, && 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 hassaslaştırdıkça 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 gereksinim, iki özniteliğin, authorUID ve createdAt değişmemesidir:

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

Son olarak, başlık 50 karakter veya daha az 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: Niteliğe Dayalı Erişim Kontrolü

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

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

Ek olarak, kimlik doğrulama belirteçlerinde isModerator özniteliğine sahip yazarların taslakları silmelerine izin verilir:

request.auth.token.isModerator == true

Bu koşullardan herhangi biri silme için yeterli olduğundan, bunları mantıksal bir VEYA operatörüyle birleştirin, || :

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

Tüm 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 şimdi başka bir testin geçtiğini onaylayın.

7. Yayınlanmış gönderileri okur, oluşturur ve siler: farklı erişim kalıpları için normalleştirme

Yayınlanan gönderiler ve taslak gönderiler için erişim kalıpları çok farklı olduğundan, bu uygulama gönderileri ayrı draft ve published koleksiyonlar olarak normalleştirir. Ö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ı bir taslak blog gönderisi yayınlamak istediğinde, yeni yayınlanan gönderiyi oluşturacak bir işlev tetiklenir.

Ardından, yayınlanan gönderiler için kuralları yazacaksınız. Yazılması en basit kurallar, yayınlanan gönderilerin herkes tarafından okunabilmesi ve 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 eklendiğinde, tüm kurallar dosyası ş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önderilerin güncellenmesi: Özel işlevler ve yerel değişkenler

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

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

Yazar veya moderatör olmak için koşulları zaten yazmış olduğunuzdan, koşulları kopyalayıp yapıştırabilirsiniz, ancak zamanla bunun okunması ve sürdürülmesi zorlaşabilir. Bunun yerine, yazar veya moderatör olma mantığını kapsayan özel bir işlev oluşturacaksınız. Ardından, onu birden çok koşuldan arayacaksınız.

Özel bir işlev oluşturun

Taslaklar için eşleşme ifadesinin üzerinde, bir gönderi belgesini (bu, taslaklar veya yayınlanan gönderiler için işe yarar) ve kullanıcının yetkilendirme nesnesini argüman olarak alan isAuthorOrModerator adlı 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

İşlevin 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üş ifadesi ile bitmelidir ve bizimki, herhangi bir değişkenin doğru olup olmadığını gösteren bir boole değeri 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 argüman olarak resource.data iletmeye dikkat ederek, bu işlevi çağırmak için taslaklar için kuralı 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ınlanan bir gönderinin bazı alanları değiştirilmemelidir, özellikle url , authorUID publishedAt değişmezdir. Diğer iki alan, title ve content ve visible alan, güncellemeden sonra da mevcut olmalıdır. Yayınlanan gönderilerdeki güncellemeler için bu gereksinimleri uygulamak için 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

Son olarak, başlığın 50 karakterin altında olması koşulu ekleyin. Bu yeniden kullanılan 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 şekilde olur:

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 başarılı ve 4 başarısız testiniz olmalıdır.

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 koleksiyonlara uygulanmaz. Yayınlanan gönderinin üst belgesi için geçerli olan kuralların yorumlara da uygulanmasını istemezsiniz; farklı işler yapacaksın.

Yorumlara erişmek için 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, yalnızca kalıcı bir hesap oluşturan kullanıcılar, anonim bir hesap değil, 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-posta adresi olmalıdır
  • yorum 500 karakterden az olmalı ve
  • yasaklı Kullanıcılar koleksiyonundaki firestore'da depolanan yasaklı kullanıcılar listesinde bannedUsers . Bu koşulları birer birer alarak:
request.auth.token.email_verified == true
request.resource.data.comment.size() < 500
-yer tutucu36 l10n-yer
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));

Yorum oluşturmanın 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üzenlenebilmeleridir. 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ğunu:

(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ın silinmesi: ebeveyn sahipliğinin kontrol edilmesi

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önderisi yazarı olup olmadığını kontrol etmek için, gönderiyi Firestore'da aramak için bir get kullanı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 operatörü 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 testleri geçen ve uygulamanın güvenliğini sağlayan Güvenlik Kurallarını yazdınız!

Daha sonra dalmak için ilgili bazı konular şunlardır:

  • Blog gönderisi : Kod incelemesi nasıl yapılır Güvenlik Kuralları
  • Codelab : Emülatörlerle yerel ilk geliştirmede ilerleme
  • Video : GitHub Eylemleri kullanılarak öykünücü tabanlı testler için kurulum CI nasıl kullanılır?