Firebase Güvenlik Kuralları ile Firestore verilerinizi koruma

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ını kullanır. Güvenlik Kuralları adı verilen bu yapılandırma, uygulamanız için bir tür şema işlevi de kullanabilir. Uygulamanızı geliştirmenin en önemli bölümlerinden biridir. Bu codelab'de süreç adım adım açıklanmaktadır.

Ön koşullar

  • Visual Studio Code, Atom veya Sublime Text gibi basit bir düzenleyici
  • Node.js 8.6.0 veya sonraki sürümleri (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ı uygulayın; sürümünüzü kontrol etmek için java -version komutunu çalıştırın)

Yapacaklarınız

Bu codelab'de, Firestore'da oluşturulmuş basit bir blog platformunu güvenli hale getireceksiniz. Güvenlik Kurallarına göre birim testleri çalıştırmak için Firestore emülatörünü kullanacaksınız ve kuralların beklediğiniz erişime izin verip vermediğinden emin olmalısınız.

Öğrenecekleriniz:

  • Ayrıntılı izinler verin
  • Veri ve tür doğrulamalarını zorunlu kılma
  • Özellik Tabanlı Erişim Denetimi Uygulama
  • Kimlik doğrulama yöntemine göre erişim izni ver
  • Özel işlevler oluşturma
  • Zamana Dayalı Güvenlik Kuralları oluşturma
  • Reddetme listesi ve geri yüklenebilir şekilde silme özellikleri uygulama
  • Birden çok erişim kalıbını karşılamak için verilerin ne zaman normalleştirileceğini anlama

2. Ayarla

Bu bir blog uygulamasıdır. Aşağıda, uygulama işlevinin genel bir özeti verilmiştir:

Taslak blog yayınları:

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

Yayınlanan blog yayınları:

  • Yayınlanan yayınlar kullanıcılar tarafından değil, yalnızca bir işlev aracılığıyla oluşturulabilir.
  • Yalnızca geri yüklenebilir şekilde silinebilirler. Bu durumda visible özelliği false olarak güncellenir.

Yorumlar

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

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

Firebase Emulator Suite sayesinde her şey yerel olarak gerçekleştirilir.

Kaynak kodunu alma

Bu codelab'de, Güvenlik Kuralları testleriyle başlayacaksınız ancak Güvenlik Kurallarını taklit edeceksiniz. 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 kalanı için çalışacağınız ilk durum dizinine geçin:

$ cd codelab-rules/initial-state

Şimdi testleri çalıştırabilmek için bağımlılıkları yükleyin. 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'ı edinme

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

$ npm install -g firebase-tools

Ardından, KSA'nın en son sürümüne sahip olduğunuzu onaylayın. Bu codelab'in 8.4.0 veya sonraki sürümlerde çalışması gerekir. Ancak sonraki sürümlerde daha fazla hata düzeltmesi bulunmaktadır.

$ firebase --version
9.10.2

3. Testleri yapın

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

Emülatörleri başlatın

Birlikte çalışacağınız uygulamanın üç ana Firestore koleksiyonu vardır: drafts devam etmekte olan blog yayınlarını içerir, published koleksiyonu yayınlanmış blog yayınlarını içerir ve comments, yayınlanmış yayınlardaki bir alt koleksiyondur. Depo, kullanıcının drafts, published ve comments koleksiyonlarında doküman oluşturması, okuması, güncellemesi ve silmesi için gereken kullanıcı özelliklerini ve diğer koşulları tanımlayan Güvenlik Kuralları için birim testleriyle birlikte gelir. Bu testlerin başarılı olmasını sağlamak için Güvenlik Kuralları yazacaksınız.

Öncelikle veritabanınız kilitlendi: Veritabanındaki okuma ve yazma işlemleri evrensel olarak reddedilir ve tüm testler başarısız olur. Güvenlik Kuralları yazdıkça testler başarılı olur. Testleri görmek için düzenleyicinizde functions/test.js dosyasını açın.

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

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

Çıkışı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 hata var. Kural dosyasını oluştururken, başarılı olan daha fazla testi izleyerek ilerlemeyi ölçebilirsiniz.

4. Blog yayını taslakları oluşturma

Taslak blog yayınlarına erişim, yayınlanan blog yayınlarına erişimden çok farklı olduğundan, bu blog uygulaması taslak blog yayınlarını ayrı bir koleksiyonda (/drafts) depolar. Taslaklara yalnızca yazar veya moderatörler erişebilir. Bu taslaklar, zorunlu ve değiştirilemez alanlar için doğrulama içerir.

firestore.rules dosyasını açtığınızda varsayılan kural dosyası görüntülenir:

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

Eşleşme ifadesi (match /{document=**}), alt koleksiyonlardaki tüm dokümanlara yinelenen şekilde uygulamak için ** söz dizimini kullanıyor. En üst düzeyde olduğu için, şu anda aynı genel kural, isteği kimin yaptığı veya hangi verileri okumaya ya da yazmaya çalıştığına bakılmaksızın tüm istekler için geçerlidir.

İlk olarak, en içteki eşleşme ifadesini kaldırıp match /drafts/{draftID} ile değiştirin. (Belge yapısıyla ilgili yorumlar, kurallarda yararlı olabilir ve bu codelab'e dahil edilecektir. 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, dokümanları kimlerin oluşturabileceğini belirler. Bu uygulamada, taslaklar yalnızca yazar olarak listelenen kişi tarafından oluşturulabilir. İsteği yapan kişinin UID'sinin, dokümanda listelenen UID ile aynı olduğundan emin olun.

Oluşturmanın ilk koşulu şu şekilde olur:

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

Daha sonra, dokümanlar yalnızca üç zorunlu alanı (authorUID, createdAt ve title) içermeleri halinde oluşturulabilir. (createdAt alanını kullanıcı sağlamaz. Bu, uygulamanın bir doküman oluşturmaya çalışmadan önce bu alanı eklemesi zorunluluğunu zorunlu kılar.) Yalnızca özelliklerin oluşturulmakta olduğunu kontrol etmeniz gerektiğinden bu anahtarların request.resource tarafından sağlanıp sağlanmadığını kontrol edebilirsiniz:

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

Blog yayını oluşturmak için 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üyle birleştirin. İlk kural ş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;
    }
  }
}

Terminalde testleri yeniden çalıştırın ve ilk testin başarılı olduğunu onaylayın.

5. Blog yayını taslaklarını güncelleme.

Ardından, yazarlar taslak blog yayınlarını hassaslaştırdıkça taslak dokümanları düzenlerler. Yayının güncellenebileceği koşullar için bir kural oluşturun. İlk olarak, taslaklarını yalnızca yazar güncelleyebilir. Önceden yazılmış olan UID'yi burada kontrol edebilirsiniz.resource.data.authorUID:

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

Güncelleme için ikinci şart, authorUID ve createdAt özelliklerinin 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;

Şu koşulların tümünün 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;

Tüm kurallar ş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;
    }
  }
}

Testlerinizi tekrar çalıştırın ve başka bir testin başarılı olduğunu onaylayın.

6. Taslakları silme ve okuma: Özellik Tabanlı Erişim Denetimi

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

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

Ayrıca, yetkilendirme jetonunda isModerator özelliği olan yazarların taslakları silmelerine izin verilir:

request.auth.token.isModerator == true

Silme işlemi için bu koşullardan herhangi biri yeterli olduğundan bunları || mantıksal bir OR operatörüyle birleştirin:

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

Okumalar için de aynı koşullar geçerlidir. Bu nedenle izin kurala eklenebilir:

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

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

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 tekrar çalıştırın ve başka bir testin başarılı olduğunu doğrulayın.

7. Yayınlanan yayınlar için okuma, oluşturma ve silme işlemleri: Farklı erişim kalıpları için normalleştirmenin kaldırılması

Yayınlanan yayınlar ile taslak yayınların erişim kalıpları çok farklı olduğundan bu uygulama, yayınları normal draft ve published koleksiyonlarına ayırarak normalleştirir. Örneğin, yayınlanan yayınlar 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 yayını yayınlamak istediğinde, yeni yayınlanan yayını oluşturan bir işlev tetikleniyor.

Ardından, paylaşılan yayınlara ilişkin kuralları yazın. Yazılacak en basit kurallar, yayınlanan yayınların herkes tarafından okunabilmesi ve hiç kimse tarafından oluşturulamaması ya da 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ğinizde, 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 tekrar çalıştırın ve başka bir testin başarılı olduğunu doğrulayın.

8. Yayınlanmış yayınları güncelleme: Özel işlevler ve yerel değişkenler

Paylaşılan bir yayını güncelleme koşulları şunlardır:

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

Yazar veya moderatör olmak için gerekli koşulları zaten yazdığınızdan, koşulları kopyalayıp yapıştırabilirsiniz ancak zaman içinde bu koşulların okunması ve sürdürülmesi zorlaşabilir. Bunun yerine, yazar veya moderatör olma mantığını kapsayan özel bir işlev oluşturursunuz. Ardından, birçok koşuldan çağırırsınız.

Özel işlev oluşturma

Taslaklar için eşleşme ifadesinin üzerinde, bir yayın dokümanını (taslaklar veya yayınlanmış yayınlar için kullanılabilir) bağımsız değişken olarak kabul eden isAuthorOrModerator adlı yeni bir işlev ve kullanıcının kimlik doğrulama nesnesi 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ın

İşlevin içinde, isAuthor ve isModerator değişkenlerini ayarlamak için let anahtar kelimesini kullanın. Tüm işlevler bir döndürülen ifadesiyle bitmelidir; bizimki ise 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;
}

İşlevi çağırın

Şimdi, ilk bağımsız değişken olarak resource.data öğesini iletmeye dikkat ederek taslakların bu işlevi çağırmasına yönelik 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 yayınlanan yayınları güncellemek için yeni işlevi de kullanan bir koşul yazabilirsiniz:

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

Doğrulama ekle

Paylaşılan bir yayının bazı alanları değiştirilmemelidir (özellikle url, authorUID ve publishedAt alanları değiştirilemez). Diğer iki alan (title, content ve visible) güncelleme sonrasında da mevcut olmalıdır. Yayınlanan yayınlarda yapılacak güncellemelerde bu şartları zorunlu kılmak için koşul 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"
])

Kendiniz bir özel işlev oluşturun

Son olarak, başlığın 50 karakterden kısa olması koşulunu ekleyin. Bu mantık yeniden kullanıldığı için titleIsUnder50Chars adında yeni bir işlev oluşturarak bunu yapabilirsiniz. Bu yeni işlevle birlikte, yayınlanan bir yayını 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);

Kural dosyasının tamamı şu şekildedir:

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, geçen 5 test ve başarısız olan 4 testinizin olması gerekir.

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

Yayınlanan yayınlar yorumlara izin verir ve yorumlar, paylaşılan yayının alt koleksiyonunda (/published/{postID}/comments/{commentID}) saklanır. Varsayılan olarak bir koleksiyonun kuralları, alt koleksiyonlar için geçerli değildir. Yayınlanan yayının üst dokümanı için geçerli olan kuralların yorumlara da uygulanmasını istemezsiniz; farklı resimler oluşturacaksınız.

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ı okuma: Anonim olamaz

Bu uygulama için yorumları, yalnızca kalıcı hesap oluşturan kullanıcılar okuyabilir. Bu tür hesapları anonim bir hesap göremez. Bu kuralı uygulamak için her bir auth.token nesnesinde bulunan sign_in_provider özelliğini arayın:

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

Testlerinizi tekrar çalıştırın ve bir testin daha başarılı olduğunu 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 adresine sahip olması gerekir
  • Yorum 500 karakterden kısa olmalı ve
  • bannedUsers koleksiyonundaki firestore'da depolanan yasaklı kullanıcılar listesinde yer alamazlar. Aşağıdaki koşulları tek tek uygulayarak:
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ı şöyledir:

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

Artık kurallar dosyasının tamamı:

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 tekrar çalıştırın ve bir testin daha başarılı olduğundan emin olun.

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

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

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

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

Ardından, yorum son bir saat içinde oluşturulur:

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

Bunları mantıksal AND operatörüyle birleştirdiğinizde yorum güncelleme kuralı şu şekilde 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 tekrar çalıştırın ve bir testin daha başarılı olduğundan emin olun.

11. Yorumları silme: Üst sahiplikleri kontrol etme

Yorumlar; yorumun yazarı, moderatörü veya blog yayınının yazarı tarafından silinebilir.

İlk olarak, eklediğiniz yardımcı işlevi yayın 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 yayınının yazarı olup olmadığını kontrol etmek için get kullanarak yayını Firestore'da 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 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 tekrar çalıştırın ve bir testin daha başarılı olduğundan emin olun.

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 başarılı olmasını sağlayan Güvenlik Kurallarını yazdınız ve uygulamanın güvenliğini sağladınız.

Bir sonraki adımda ele alabileceğiniz ilgili bazı konular aşağıda belirtilmiştir:

  • Blog yayını: Güvenlik Kuralları'nın kod incelemesini yapma
  • Codelab: Emülatörlerle ilk yerel geliştirme sürecini adım adım öğrenin
  • Video: GitHub İşlemleri kullanılarak emülatör tabanlı testler için set CI'yı kullanma