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 parçalarından biridir. Ve bu codelab size bu konuda 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 ç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 codelab'de Firestore üzerine kurulu basit bir blog platformunu güvence altına alacaksınız. Güvenlik Kurallarına göre birim testleri çalıştırmak ve kuralların beklediğiniz erişime izin verip vermediğinden emin olmak için Firestore öykünücüsünü kullanacaksınız.

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

  • Ayrıntılı izinler verme
  • Verileri ve tür doğrulamalarını zorunlu kılın
  • Öznitelik Tabanlı Erişim Denetimini 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. Uygulama işlevselliğinin üst düzey bir özetini burada bulabilirsiniz:

Taslak blog gönderileri:

  • Kullanıcılar, drafts koleksiyonunda yer alan taslak blog gönderileri oluşturabilir.
  • Yazar, yayınlanmaya hazır olana kadar 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, 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 reddedilen grupta olmaması gerekir.
  • Yorumlar yalnızca yayınlandıktan sonraki 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ı uygulayan Güvenlik Kuralları oluşturacaksınız.

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

Kaynak kodunu alın

Bu codelab'de, Güvenlik Kurallarına yönelik testlerle başlayacaksınız, ancak Güvenlik Kuralları minimum düzeyde olacaktır; 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 başlangıç ​​durumu dizinine geçin:

$ cd codelab-rules/initial-state

Şimdi testleri çalıştırabilmek için bağımlılıkları yükleyin. 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 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 doğrulayın. Bu codelab'in 8.4.0 veya üzeri sürümlerle çalışması gerekir 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ı özelliklerini ve diğer koşulları tanımlayan Güvenlik Kurallarına yönelik birim testleriyle birlikte gelir. Bu testlerin başarılı olabilmesi için Güvenlik Kurallarını yazacaksınız.

Başlangıç ​​olarak 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ı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 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 ilerleyin:

$ 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. Kural dosyasını oluştururken daha fazla testin geçiş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 /drafts adlı ayrı bir koleksiyonda saklar. Taslaklara yalnızca yazar veya moderatör tarafından erişilebilir ve gerekli ve değiştirilemez alanlar için doğrulamalar bulunur.

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 yinelemeli olarak uygulamak için ** sözdizimini kullanıyor. Ve en üst düzeyde olduğu için, şu anda aynı genel kural, isteği yapanın kim olduğuna veya hangi veriyi okumaya veya yazmaya çalıştığına bakılmaksızın tüm istekler için geçerlidir.

En içteki match ifadesini kaldırıp onu match /drafts/{draftID} ile değiştirerek başlayın. (Belgelerin yapısına ilişkin yorumlar kurallar açısından yararlı 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 kimin oluşturabileceğini kontrol edecektir. Bu uygulamada taslaklar yalnızca yazar olarak belirtilen kişi tarafından oluşturulabilir. Talepte bulunan kişinin UID'sinin belgede listelenen UID ile aynı olup olmadığını kontrol edin.

Yaratmanı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çeriyorsa oluşturulabilir. (Kullanıcı createdAt alanını sağlamaz; bu, uygulamanın bir belge oluşturmaya çalışmadan önce bu alanı eklemesini zorunlu kılar.) Yalnızca niteliklerin oluşturulduğunu kontrol etmeniz gerektiğinden, request.resource tüm özelliklere sahip olup olmadığını kontrol edebilirsiniz. şu anahtarlar:

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

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 tekrar çalıştırın ve ilk testin geçtiğini doğrulayın.

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

Daha sonra yazarlar taslak blog gönderilerini hassaslaştırdıkça taslak belgeleri düzenleyeceklerdir. Bir gönderinin güncellenebileceği koşullar için bir kural oluşturun. Öncelikle taslaklarını yalnızca yazar güncelleyebilir. Burada önceden yazılmış olan UID'yi resource.data.authorUID kontrol ettiğinizi unutmayın:

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

Güncellemenin ikinci gereksinimi, authorUID ve createdAt adlı iki özelliğin 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;

Kuralların tamamı şö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 doğrulayın.

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

Yazarlar taslak oluşturup güncelleyebildiği gibi taslakları da silebilir.

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

Ayrıca, kimlik doğrulama belirteçlerinde isModerator niteliğine sahip yazarların taslakları silmelerine izin verilir:

request.auth.token.isModerator == true

Bu koşullardan herhangi biri silme işlemi için yeterli olduğundan, bunları mantıksal OR operatörü || ile 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

Artık kuralların tamamı şöyle:

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 geçtiğini doğrulayın.

7. Yayınlanan 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 modelleri çok farklı olduğundan, bu uygulama gönderileri ayrı draft ve published koleksiyonlar halinde normalleştirmez. Örneğin, yayınlanan gönderiler herkes tarafından okunabilir ancak kalıcı olarak silinemez; taslaklar silinebilir ancak yalnızca yazar ve moderatörler tarafından okunabilir. Bu uygulamada, bir kullanıcı taslak blog yazısı yayınlamak istediğinde, yeni yayınlanan yazıyı oluşturacak bir işlev tetiklenir.

Daha sonra yayınlanan gönderilere ilişkin kuralları yazacaksınız. Yazılması gereken en basit kural, yayınlanan gönderilerin herkes tarafından okunabilmesi ve hiç kimse tarafından oluşturulamaması veya silinememesidir. Şu 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ğimizde kurallar dosyasının tamamı şu şekilde 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;

      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 doğrulayın.

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

Yayınlanan 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 olmanın koşullarını zaten yazmış olduğunuzdan, koşulları kopyalayıp yapıştırabilirsiniz, ancak zamanla bunların okunması ve bakımı zorlaşabilir. Bunun yerine, yazar veya moderatör olmanın mantığını özetleyen özel bir işlev oluşturacaksınız. Daha sonra bunu birden fazla koşuldan arayacaksınız.

Özel bir işlev oluşturun

Taslaklar için match ifadesinin üzerinde, argüman olarak bir gönderi belgesini (bu, taslaklar veya yayınlanmış gönderiler için işe yarayacaktır) ve kullanıcının kimlik doğrulama nesnesini alan isAuthorOrModerator adında 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 return ifadesiyle bitmelidir ve bizimki, değişkenlerden birinin doğru olup olmadığını belirten bir boole değeri döndürecektir:

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

Fonksiyonu çağırın

Şimdi, ilk argüman olarak resource.data aktarmaya dikkat ederek, taslaklara ilişkin kuralı bu işlevi çağıracak şekilde 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 yayınlanmış gönderileri güncellemek için yeni işlevi de kullanan 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 alanları değiştirilemez. Diğer iki alan ( title , content ve visible ) güncellemeden sonra da mevcut olmalıdır. Yayınlanan gönderilerdeki güncellemeler için bu gereksinimlerin uygulanmasını sağlayacak 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 adında yeni bir işlev oluşturarak yapabilirsiniz. Yeni işlevle birlikte, yayınlanan bir gönderinin güncellenmesinin koşulu şöyle 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 kural dosyasının tamamı:

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 yeniden çalıştırın. Bu noktada 5 geçme testiniz 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 ( /published/{postID}/comments/{commentID} ) saklanır. Varsayılan olarak bir koleksiyonun kuralları alt koleksiyonlara uygulanmaz. Yayınlanan gönderinin ana belgesi için geçerli olan kuralların aynılarının yorumlara da uygulanmasını istemezsiniz; farklı olanları yaratacaksınız.

Yorumlara erişim kurallarını yazmak için match ifadesiyle başlayın:

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

Yorumları okuma: İsimsiz olamaz

Bu uygulama için, yorumları anonim bir hesap değil, yalnızca kalıcı bir hesap oluşturmuş olan kullanıcılar okuyabilir. Bu kuralı uygulamak için her auth.token nesnesinde sign_in_provider niteliğ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 doğrulayın.

Yorum oluşturma: Reddetme listesini kontrol etme

Yorum oluşturmanın üç koşulu vardır:

  • kullanıcının doğrulanmış bir e-posta adresi olması gerekir
  • yorum 500 karakterden az olmalı ve
  • firestore'da bannedUsers koleksiyonunda saklanan 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ş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));

Kurallar dosyasının tamamı artık şu şekildedir:

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 characters
        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ın güncellenmesi: Zamana dayalı kurallar

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

Öncelikle kullanıcının yazar olduğunu tespit etmek için:

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

Daha sonra, 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ştirdiğimizde yorumları güncelleme kuralı şöyle olur:

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 yazısının 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 Firestore'da gönderiyi aramak üzere bir get kullanın:

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

Bu koşullardan herhangi biri yeterli olduğundan, bunların arasında mantıksal OR operatörünü 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 kurallar dosyasının tamamı:

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 characters
        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 uygulamanın güvenliğini sağlayan Güvenlik Kurallarını yazdınız!

Aşağıda daha sonra incelenecek ilgili bazı konular verilmiştir:

  • Blog yazısı : Güvenlik Kuralları nasıl gözden geçirilir?
  • Codelab : Emülatörlerle 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?