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ı olarak adlandırılan bu yapılandırma, uygulamanız için bir tür şema görevi de görebilir. Bu, uygulamanızı geliştirmenin en önemli kısımlarından biridir. Bu codelab'de bu 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 daha yeni bir sürüm (Node.js'yi yüklemek için nvm'yi kullanın; sürümünüzü kontrol etmek için
node --version
komutunu çalıştırın) - Java 7 veya daha yeni bir sürüm (Java'yı yüklemek için bu talimatları kullanın; sürümünüzü kontrol etmek için
java -version
komutunu çalıştırın)
Yapacaklarınız
Bu codelab'de, Firestore üzerinde oluşturulmuş basit bir blog platformunun güvenliğini sağlayacaksınız. Güvenlik kurallarına karşı birim testleri çalıştırmak ve kuralların beklediğiniz erişime izin verip vermediğini kontrol etmek için Firestore emülatörünü kullanacaksınız.
Öğrenecekleriniz:
- Ayrıntılı izinler verme
- Veri ve tür doğrulamalarını zorunlu kılma
- Özellik tabanlı erişim denetimini uygulama
- Kimlik doğrulama yöntemine göre erişim izni verme
- Özel işlevler oluşturma
- Zamana dayalı güvenlik kuralları oluşturma
- Reddetme listesi ve geçici silme işlemlerini uygulama
- Birden fazla erişim kalıbını karşılamak için verilerin ne zaman normalleştirilmemesi gerektiğini anlama
2. Kur
Bu bir blog uygulamasıdır. Uygulama işlevinin genel bir özeti aşağıda verilmiştir:
Blog yayını taslakları:
- Kullanıcılar,
drafts
koleksiyonunda bulunan taslak blog yayınları oluşturabilir. - Yazar, taslağı yayınlanmaya hazır olana kadar güncelleyebilir.
- Yayınlanmaya hazır olduğunda,
published
koleksiyonunda yeni bir doküman oluşturan bir Firebase işlevi tetiklenir. - Taslaklar yazar veya site moderatörleri tarafından silinebilir.
Yayınlanan blog yayınları:
- 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 işlem,
visible
özelliğini false olarak günceller.
Yorumlar
- Yayınlanan gönderilerde yorumlara izin verilir. Yorumlar, yayınlanan her gönderideki bir alt koleksiyondur.
- Kötüye kullanımı azaltmak için kullanıcıların yorum bırakabilmesi için e-posta adreslerinin doğrulanmış olması ve izin verilmeyenler listesinde olmaması gerekir.
- Yorumlar, yayınlandıktan sonraki bir saat içinde güncellenebilir.
- Yorumlar, yorum 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ı oluşturursunuz.
Her şey Firebase Emulator Suite kullanılarak yerel olarak gerçekleşir.
Kaynak kodu alma
Bu codelab'de, güvenlik kuralları için testlerle başlayacaksınız ancak güvenlik kuralları minimum düzeyde olacak. Bu nedenle, testleri çalıştırmak için yapmanız gereken ilk şey kaynağı klonlamak:
$ git clone https://github.com/FirebaseExtended/codelab-rules.git
Ardından, bu codelab'in geri kalanında çalışacağınız initial-state dizinine gidin:
$ cd codelab-rules/initial-state
Şimdi testleri çalıştırabilmek için bağımlılıkları yükleyin. Yavaş bir internet bağlantısı kullanıyorsanız 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'yı edinme
Testleri çalıştırmak için kullanacağınız Emulator Suite, Firebase CLI'nın (komut satırı arayüzü) bir parçasıdır. Bu arayüzü aşağıdaki komutla makinenize yükleyebilirsiniz:
$ npm install -g firebase-tools
Ardından, CLI'nın en son sürümüne sahip olduğunuzu onaylayın. Bu codelab, 8.4.0 veya daha yeni sürümlerde çalışır ancak daha yeni sürümlerde daha fazla hata düzeltmesi bulunur.
$ firebase --version 9.10.2
3. Testleri çalıştırma
Bu bölümde testleri yerel olarak çalıştıracaksınız. Bu, Emulator Suite'i başlatma zamanı geldiği anlamına gelir.
Emülatörleri başlatma
Çalışacağınız uygulamada üç ana Firestore koleksiyonu vardır: drafts
koleksiyonu, devam eden blog yayınlarını içerir. published
koleksiyonu, yayınlanmış blog yayınlarını içerir. comments
koleksiyonu ise yayınlanmış yayınlardaki alt koleksiyondur. Depoda, 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ına yönelik birim testleri bulunur. Bu testlerin başarılı olması için güvenlik kurallarını yazarsınız.
Başlangıçta veritabanınız kilitlenir: Veritabanında okuma ve yazma işlemleri evrensel olarak reddedilir ve tüm testler başarısız olur. Güvenlik kurallarını yazarken testler başarılı olur. Testleri görmek için düzenleyicinizde functions/test.js
simgesini 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"
Çıkışın en üstüne kaydırın:
$ 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. Kurallar dosyasını oluştururken daha fazla testin başarılı olduğunu görerek ilerleme durumunu ölçebilirsiniz.
4. Blog yayını taslakları oluşturabilirsiniz.
Taslak blog yayınlarına erişim, yayınlanmış blog yayınlarına erişimden çok farklı olduğundan bu blog uygulaması, taslak blog yayınlarını ayrı bir koleksiyonda (/drafts
) saklar. Taslaklara yalnızca yazar veya moderatör erişebilir. Ayrıca, zorunlu ve değişmez alanlar için doğrulama yapılır.
firestore.rules
dosyasını açtığınızda varsayılan kurallar dosyasını görürsünüz:
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 belgelere yinelemeli olarak uygulanmak için **
söz dizimini kullanıyor. En üst düzeyde olduğundan, şu anda isteği kimin yaptığına veya hangi verileri okumaya ya da yazmaya çalıştığına bakılmaksızın tüm istekler için aynı genel kural geçerlidir.
En içteki eşleşme ifadesini kaldırıp match /drafts/{draftID}
ile değiştirerek başlayın. (Dokümanların yapısıyla ilgili yorumlar kurallarda faydalı olabilir ve bu codelab'e dahil edilecektir. Bu yorumlar 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 kontrol eder. Bu uygulamada taslaklar yalnızca yazar olarak listelenen kişi tarafından oluşturulabilir. İstekte bulunan kişinin UID'sinin, belgede listelenen UID ile aynı olup olmadığını kontrol edin.
Oluşturma için ilk koşul şudur:
request.resource.data.authorUID == request.auth.uid
Ayrıca, dokümanlar yalnızca gerekli üç alan olan authorUID
, createdAt
ve title
'yi içeriyorsa oluşturulabilir. (Kullanıcı createdAt
alanını sağlamaz. Bu, uygulamanın belge oluşturmayı denemeden önce bu alanı eklemesini zorunlu kılar.) Yalnızca özelliklerin oluşturulup oluşturulmadığını kontrol etmeniz gerektiğinden request.resource
öğesinin tüm bu anahtarlara sahip olup olmadığını kontrol edebilirsiniz:
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
])
Blog yayını oluşturmayla ilgili son şart, başlığın 50 karakterden uzun olmamasıdır:
request.resource.data.title.size() < 50
Bu koşulların tümü doğru olmalıdır. Bu nedenle, bunları mantıksal VE operatörü &&
ile birleştirin. İlk kural şu şekilde değişir:
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 doğrulayın.
5. Blog yayını taslaklarını güncelleyin.
Ardından, yazarlar taslak blog yayınlarını iyileştirirken taslak dokümanları düzenler. Bir yayının güncellenebileceği koşullar için kural oluşturun. Öncelikle, taslaklarını yalnızca yazar güncelleyebilir. Burada,zaten yazılmış olan UID'yi kontrol ettiğinizi unutmayınresource.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;
Bu 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;
Kuralları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;
}
}
}
Testlerinizi yeniden çalıştırın ve başka bir testin geçtiğini onaylayın.
6. Taslakları silme ve okuma: Özellik tabanlı erişim denetimi
Yazarlar, taslak oluşturup güncelleyebildikleri gibi taslakları da silebilir.
resource.data.authorUID == request.auth.uid
Ayrıca, kimlik doğrulama jetonlarında isModerator
özelliği bulunan yazarlar taslakları silebilir:
request.auth.token.isModerator == true
Silme işlemi için bu koşullardan herhangi birinin karşılanması yeterli olduğundan bunları mantıksal VEYA operatörüyle (||
) birleştirin:
allow delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true
Okuma işlemleri için de aynı koşullar geçerlidir. Bu nedenle, kurala izin eklenebilir:
allow read, delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true
Kuralların tam metni ş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 yeniden çalıştırın ve başka bir testin artık geçtiğini onaylayın.
7. Yayınlanmış gönderiler için okuma, oluşturma ve silme: 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ına normalleştirir. Örneğin, yayınlanmış gönderiler herkes tarafından okunabilir ancak kalıcı olarak silinemez. Taslaklar ise silinebilir ancak yalnızca yazar ve moderatörler tarafından okunabilir. Bu uygulamada, bir kullanıcı taslak blog yayınını yayınlamak istediğinde yeni yayınlanmış yayını oluşturacak bir işlev tetiklenir.
Ardından, yayınlanan gönderilerle ilgili kuralları yazarsınız. Yayınlanan gönderilerin herkes tarafından okunabileceği ve herkes tarafından oluşturulamayacağı veya silinemeyeceği, yazılacak en basit kurallardır. Ş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 başarılı olduğunu doğrulayın.
8. Yayınlanan gönderileri güncelleme: Özel işlevler ve yerel değişkenler
Yayınlanmış bir yayını güncellemek için gereken koşullar şunlardır:
- Bu işlem yalnızca yazar veya moderatör tarafından yapılabilir.
- Gerekli tüm alanları içermelidir.
Yazar veya moderatör olmanın koşullarını daha önce yazdığınız için bu koşulları kopyalayıp yapıştırabilirsiniz ancak zamanla bu koşulların okunması ve bakımı zorlaşabilir. Bunun yerine, yazar veya moderatör olma mantığını kapsayan özel bir işlev oluşturacaksınız. Ardından, bu işlevi birden fazla koşuldan çağırabilirsiniz.
Özel işlev oluşturma
Taslaklar için eşleşme ifadesinin üzerinde, isAuthorOrModerator
adlı yeni bir işlev oluşturun. Bu işlev, bağımsız değişken olarak bir gönderi belgesi (bu, taslaklar veya yayınlanmış gönderiler için geçerli olur) ve kullanıcının kimlik doğrulama nesnesini alır:
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 kullanma
İşlevin içinde, let
ve isModerator
değişkenlerini ayarlamak için let
anahtar kelimesini kullanın.isAuthor
Tüm işlevler bir return ifadesiyle sona ermelidir. Bizim işlevimiz, değişkenlerden herhangi birinin doğru olup olmadığını belirten bir boolean değeri döndürecek:
function isAuthorOrModerator(post, auth) {
let isAuthor = auth.uid == post.authorUID;
let isModerator = auth.token.isModerator == true;
return isAuthor || isModerator;
}
İşlevi çağırma
Şimdi, taslaklar için kuralı bu işlevi çağıracak şekilde güncelleyeceksiniz. İlk bağımsız değişken olarak resource.data
değerini iletmeye dikkat edin:
// 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 ekleme
Yayınlanmış bir yayının bazı alanları değiştirilmemelidir. Özellikle url
, authorUID
ve publishedAt
alanları sabittir. Diğer iki alan olan title
ve content
ile visible
, güncellemeden sonra da mevcut olmalıdır. Yayınlanan gönderilerdeki güncellemeler için bu şartları zorunlu kılacak 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 özel işlevinizi oluşturma
Son olarak, başlığın 50 karakterden kısa olması koşulunu ekleyin. Bu mantık yeniden kullanıldığı için yeni bir işlev (titleIsUnder50Chars
) oluşturarak bunu yapabilirsiniz. Yeni işlevle birlikte, yayınlanmış bir gönderinin güncellenmesi için gereken koşul şu şekilde değişir:
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);
Tam kural dosyası ise şöyledir:
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 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önderilerde yorumlara izin verilir ve yorumlar, yayınlanan gönderinin alt koleksiyonunda (/published/{postID}/comments/{commentID}
) saklanır. Varsayılan olarak, koleksiyon kuralları alt koleksiyonlar için geçerli değildir. Yayınlanan gönderinin üst dokümanı için geçerli olan kuralların yorumlar için de geçerli olmasını istemiyorsanız farklı kurallar oluşturabilirsiniz.
Yorumlara erişimle ilgili 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
Yorum okuma: Anonim olamaz
Bu uygulamada, yorumları yalnızca anonim hesap oluşturmamış, kalıcı hesap oluşturmuş kullanıcılar okuyabilir. Bu kuralı zorunlu kılmak 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şturmak için üç koşul vardır:
- Kullanıcının doğrulanmış bir e-posta adresi olmalıdır.
- Yorum 500 karakterden kısa olmalı ve
bannedUsers
koleksiyonundaki firestore'da depolanan yasaklı kullanıcılar listesinde olmamalıdır. Bu koşulları tek tek ele alalım:
request.auth.token.email_verified == true
request.resource.data.comment.size() < 500
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
Yorum oluşturmayla ilgili 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 başarılı olduğundan emin olun.
10. Yorumları güncelleme: Zamana dayalı kurallar
Yorumlarla ilgili iş mantığı, yorumların oluşturulduktan sonraki bir saat içinde yorum yazarı tarafından düzenlenebilmesidir. Bunu uygulamak için createdAt
zaman damgasını kullanın.
Öncelikle, kullanıcının yazar olduğunu doğrulamak için:
request.auth.uid == resource.data.authorUID
Ardından, yorumun son bir saat içinde oluşturulduğunu doğrulayın:
(request.time - resource.data.createdAt) < duration.value(1, 'h');
Bunları AND mantıksal operatörüyle birleştirdiğimizde, yorumları 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 yeniden çalıştırın ve bir testin daha başarılı olduğundan emin olun.
11. Yorumları silme: üst öğe sahipliği kontrol ediliyor
Yorumlar, yorumu yazan kullanıcı, moderatör veya blog yayınının yazarı tarafından silinebilir.
İlk olarak, daha önce eklediğiniz yardımcı işlev, gönderide veya yorumda bulunabilecek bir authorUID
alanı kontrol ettiğinden, kullanıcının yazar veya moderatör olup olmadığını kontrol etmek için yardımcı işlevi yeniden kullanabilirsiniz:
isAuthorOrModerator(resource.data, request.auth)
Kullanıcının blog yayını yazarı olup olmadığını kontrol etmek için Firestore'da yayını aramak üzere 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 başarılı olduğundan emin olun.
Kurallar dosyasının tamamı ise şöyledir:
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 uygulamayı güvenli hale getiren güvenlik kurallarını yazdınız.
İnceleyebileceğiniz diğer ilgili konular:
- Blog yayını: Güvenlik Kuralları'nın kod incelemesini yapma
- Codelab: Emulator'lerle yerel cihaza dayalı geliştirme sürecini adım adım inceleme
- Video: GitHub Actions'ı kullanarak emülatör tabanlı testler için CI'yı ayarlama