Firestore won't let me add one more map to they array - firebase

In my Firestore, I have a collection of documents named "items". Each document in my collection has a random ID and some properties. One of these properties might be "reviews", which is added by my client-side. I have a function that adds "reviews" array with one map (which would be one review). Each user should be able to add only one review on each of the items. Here's the function itself:
const handleSubmit = async () => {
const itemRef = doc(db, "items", itemId);
await updateDoc(itemRef, {
reviews: arrayUnion({
userId,
rating,
review,
userName,
}),
});
};
And here are the rules of the Firestore:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /items/{itemId} {
allow read;
allow create, update: if request.auth != null
&& request.resource.data.reviews.size() == 1
&& 'reviews' in request.resource.data
&& request.resource.data.reviews[0].keys().hasAll(['userId', 'rating', 'review', 'userName'])
&& request.resource.data.reviews[0].size() == 4
&& request.resource.data.reviews[0].userId == request.auth.uid
&& !(exists(/databases/$(database)/documents/items/$(itemId)/reviews/$(request.resource.data.reviews[0].userId)))
}
}
}
For some odd reason, this ruleset would only let me add one review (along with creating the array itself).
Why wouldn't it let me add one more map (which is one review) to my "reviews" array? I've been struggling with this for couple of day and I couldn't find the answer in the documentation, so I have no idea what am I doing wrong.
EDIT:
As #l1b3rty suggested, I've changed my data structure from array to map as it is impossible to do what I need with array. I still struggle with one thing: checking if user has already left a review and deny doing so if user did already left a review.
My handleSubmit():
const handleSubmit = async () => {
const itemRef = doc(db, "items", itemId);
const reviewData = {
rating,
review,
userName,
};
const reviewPath = `reviews.${userId}`;
await updateDoc(itemRef, {
[reviewPath]: reviewData,
});
};
My rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /items/{itemId} {
allow read: if true;
allow write: if request.auth != null
&& request.resource.data.reviews.keys().hasAll([request.auth.uid])
&& request.resource.data.reviews[request.auth.uid].rating is int
&& request.resource.data.reviews[request.auth.uid].rating >=1
&& request.resource.data.reviews[request.auth.uid].rating <= 5
&& request.resource.data.reviews[request.auth.uid].review is string
&& request.resource.data.reviews[request.auth.uid].userName is string
}
}
}

You are valdiating for the review array to be of size 1:
request.resource.data.reviews.size() == 1
So once created, you wont be able to add any reviews, logical.
If you want to be able to add a new review, split the create and update methods:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /items/{itemId} {
allow read;
allow create: if request.auth != null
&& request.resource.data.reviews.size() == 1
&& 'reviews' in request.resource.data
&& request.resource.data.reviews[0].keys().hasAll(['userId', 'rating', 'review', 'userName'])
&& request.resource.data.reviews[0].size() == 4
&& request.resource.data.reviews[0].userId == request.auth.uid
&& !(exists(/databases/$(database)/documents/items/$(itemId)/reviews/$(request.resource.data.reviews[0].userId)))
allow update: if request.auth != null
&& 'reviews' in request.resource.data
}
}
}
Note that this will not validate the new review, I am pretty sure you will have to change your data model to do that: using an array will not let you access the review being added or removed by the current user. Use a map instead:
reviews
uid1
rating
review
userName
uid2
rating
review
userName
That way you can:
Ensure a user is only adding/removing/updating his own review
Validate the added/removed/updated review
Use the map.diff to identify what's being changed.
Your requirements are unclear to me but here is an untested example that should be a good start:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /items/{itemId} {
allow read;
allow create: if request.auth != null
&& request.resource.data.reviews is map
&& request.resource.data.reviews.size() == 1
&& request.auth.uid in request.resource.data.reviews
&& validatereview(request.resource.data.reviews[request.auth.uid])
allow update: if request.auth != null
&& request.resource.data.diff(resource.data).affectedKeys().hasOnly(["reviews"]) // Ensures only reviews are modified
&& !(request.auth.uid in resource.data.reviews) // Ensures a review does not already exist for the current user
&& request.resource.data.reviews.diff(resource.data.reviews).affectedKeys().hasOnly([request.auth.uid]) // Ensures only the review for the current user is modified
&& validatereview(request.resource.data.reviews[request.auth.uid]);
function validatereview(review) {
return review.keys().hasAll(['rating', 'review', 'userName'])
&& review.size() == 3
&& review.rating is int
&& review.rating >=1
&& review.rating <= 5
&& review.review is string // You may want to check for the string max length here
&& review.userName is string // Same
}
}
}
}
Here a user can only add reviews, no delete or update are allowed. Tweak as you wish.

Related

How do I access the document ID in firestore rules

I am seemingly unable to access the resource.id value when trying queries using these rules. when I manually enter the schools id (the commented out line) the data returns fine. I only have 1 school and the doc ID definitely matches the string. but when I ask to match to the resource.id value, my rules return an 'insufficient permissions' error.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
//functions
function signedIn() {
return request.auth.uid != null;
}
function returnUID(){
return request.auth.uid;
}
function getUserData() {
return get(/databases/$(database)/documents/All%20Users/$(request.auth.uid)).data;
}
match /All%20Users/{userID} {
allow read,write: if
signedIn() && returnUID() == userID;
}
match /All%20Schools/{schoolID}{
allow read, write: if
// signedIn() && getUserData().school == "f7asMxUvTs3uFhE08AJr"
signedIn() && getUserData().school == resource.id
}
}
}
my structure is like this
All Schools / school (document) / Classrooms (subcollection)
All Users / User (document) (each user doc has a classroomID associated to it)
as a point of reference this is a query that is successful
var docRef = db.collection("All Users").doc(uid).get()
and the one that is failing
db.collection("All Schools/" + properties.schoolid + "/Classrooms").onSnapshot()
[update]
the working set of rules!
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
//functions
function signedIn() {
return request.auth.uid != null;
}
function returnUID(){
return request.auth.uid;
}
function getUserData() {
return get(/databases/$(database)/documents/All%20Users/$(request.auth.uid)).data;
}
match /All%20Users/{userID} {
allow read,write: if
signedIn() && returnUID() == userID;
}
match /All%20Schools/{schoolID}{
allow read, write: if schoolID == 'f7asMxUvTs3uFhE08AJr'
}
match /All%20Schools/{schoolID}/Classrooms/{classId} {
allow read, write: if getUserData().school == schoolID;
}
match /All%20Schools/{schoolID}/Student%20List/{student} {
allow read, write: if getUserData().school == schoolID;
}
match /All%20Schools/{schoolID}/Staff/{staff} {
allow read, write: if getUserData().school == schoolID;
}
}
}
The following rules will be effective on documents of 'All Schools' collection only and not documents of 'Classrooms' sub-collection:
match /All%20Schools/{schoolID} {
// ...
}
That's why db.collection("All Users").doc(uid).get() works and fetching 'Classrooms' collection fail since you do not have any rules specified for it. Although you had a recursive wildcard earlier (before editing the question), resource object contains data of those documents being matched in 'Classrooms' sub-collection and hence getUserData().school == resource.id failed too.
That being said, try specifying rules for 'Classrooms' sub-collection as well:
match /All%20Schools/{schoolID}/Classrooms/{classId} {
allow read, write: if getUserData().school == schoolID;
}
match /All%20Schools/{schoolID}/Classrooms/{classID} {
// schoolID is the documentId
allow read, write: if signedIn() && getUserData().school == schoolID
}
If this was my code, I would not use spaces in my collection or field names. Rather I will use snake_case or camelCase.
So instead of All Schools, I will use either all_schools or allSchools.

Firestore security rule: Null value error when using Set.hasAny()

I have a user collection with two fields memberOf and managerOf (that is, of an organisation; both are arrays of doc id).
I would like to restrict a manager to list only users that are members of an organisation they managed.
In JS, it would be something like this:
const memberOf = [1, 2, 3, 4, 5]
const managerOf = [6, 7, 1, 9, 0]
console.log(memberOf.some(el => managerOf.includes(el))) // 👈 returns true
This is what I have so far:
function isSignedIn() {
return request.auth.uid != null
}
function isAdmin() {
return isSignedIn() && 'admin' in request.auth.token && request.auth.token.admin
}
match /users/{userId} {
allow get: if isSignedIn() && (request.auth.uid == userId || isAdmin());
allow list: if isAdmin() || ???; // 👈 how can I express the above condition?
allow write: if isAdmin();
}
And that's the query:
const unsubscribe = db.collection('users')
.where('memberOf', 'array-contains', organisationId)
.orderBy('email', 'asc')
.onSnapshot(snap => {
console.log(`Received query snapshot of size ${snap.size}`)
var docs = []
snap.forEach(doc => docs.push({ ...doc.data(), id: doc.id }))
actions.setMembers(docs)
}, error => console.error(error))
First, I wanted to use the organisationId from the request in the security rule, but it's not available as it's not a write operation (https://firebase.google.com/docs/reference/rules/rules.firestore.Request#resource)
I thought about:
function hasMemberManagerRelationship(userId) {
return isSignedIn() && get(/databases/$(database)/documents/users/$(userId)).data.memberOf in get(/databases/$(database)/documents/users/$(request.auth.uid)).data.managerOf
}
match /users/{userId} {
allow get: if isSignedIn() && (request.auth.uid == userId || isAdmin());
allow list: if isAdmin() || hasMemberManagerRelationship(userId);
allow write: if isAdmin();
}
or
function hasMemberManagerRelationship(userId) {
return isSignedIn() && get(/databases/$(database)/documents/users/$(userId)).data.memberOf.toSet().hasAny(get(/databases/$(database)/documents/users/$(request.auth.uid)).data.managerOf.toSet())
}
(https://firebase.google.com/docs/reference/rules/rules.Set#hasAny)
But it's not working and I have the error FirebaseError: Null value error. for 'list' # L27. AND on top of that, that could generate a lot of extra read operations (not billing-wise optimised).
I could do something like the following:
allow list: if isAdmin() || (isManagerOf('jJXLKq7p9wWSNLsHcVIn') && 'jJXLKq7p9wWSNLsHcVIn' in resource.data.memberOf);
where jJXLKq7p9wWSNLsHcVIn is the id of an organisation (and used in the query), but I don't know how I can retrieve the id from the request "context"..
Any help would be appreciated!
Ok. First, thank you #Doug Stevenson for mentioning debug() in another post! I didn't know it exists, and it rocks!
The result of debug(resource.data.memberOf) in the debug log was:
constraint_value {
simple_constraints {
comparator: LIST_CONTAINS
value {
string_value: "jJXLKq7p9wWSNLsHcVIn"
}
}
}
LIST_CONTAINS forced me to have a look at List: https://firebase.google.com/docs/reference/rules/rules.List#hasAny
toSet() does not apply to a list, but a list has already the hasAny() function.
(in fact, it does exist but it didn't work in my case 🤔)
In the end, this rule works:
function hasMemberManagerRelationship() {
return isSignedIn() && resource.data.memberOf.hasAny(getUser(request.auth.uid).data.managerOf)
}
Now I'm just wondering if getUser(request.auth.uid).data.managerOf is somehow cached (1 read for multiple user entries) or re-run every time (100 users, 100 extra reads).
Any thoughts on that?
I sincerely hope this is the first case ^^
I tested rules which are pretty similar to your attempt using the firestore "Rules Playground" and it seems to be working.
You do not need to get() the current userId because you already have it in the resource object.
I am not sure it would generate a lot more read operations because we are only using get() for request.auth.uid.
function getUser(userId) {
return get(/databases/$(database)/documents/users/$(userId));
}
function isSignedIn() {
return request.auth.uid != null;
}
function isAdmin() {
return isSignedIn() && 'admin' in request.auth.token && request.auth.token.admin;
}
function hasMemberManagerRelationship() {
return isSignedIn() && resource.data.memberOf.toSet().hasAny(getUser(request.auth.uid).data.managerOf.toSet());
}
match /users/{userId} {
allow read: if isAdmin() || hasMemberManagerRelationship();
}
Where exactly are you getting the FirebaseError: Null value error?

Making sense of Firestore Security Rules - only allowing update for certain fields

I am trying to use the Firestore security rules to edit some data, as follows, with a transaction in Flutter:
Future sendRequest(uidSend, uidRec, pid, title) async {
final crSend = ChatRequest(uid: uidRec, pid: pid, title: title);
var _lsSend = List();
_lsSend.add(crSend.toJson());
final crRec = ChatRequest(uid: uidSend, pid: pid, title: title);
var _lsRec = List();
_lsRec.add(crRec.toJson());
final uMSendDocref = userMetadataCollection.document(uidSend);
final uMRecDocref = userMetadataCollection.document(uidRec);
Firestore.instance.runTransaction((transaction) async {
await transaction.update(uMSendDocref, <String, dynamic>{
"sentRequests": FieldValue.arrayUnion(
_lsSend,
),
});
await transaction.update(uMRecDocref, <String, dynamic>{
"receivedRequests": FieldValue.arrayUnion(
_lsRec,
),
});
});
}
Notice that user1 is trying to update both his/her own data, as well as user2's. However, I only want user1 to be able to update this single field of user2's. I make my Firestore rules as such:
match /userMetadata/{uid} {
allow read: if uid == request.auth.uid || uid == 'PREVIEW';
allow write: if uid == request.auth.uid && uid != 'PREVIEW';
match /receivedRequests {
allow read: if uid == request.auth.uid;
allow write: if request.auth != null && request.auth.uid != 'PREVIEW';
}
match /sentRequests {
allow read: if uid == request.auth.uid;
allow write: if request.auth != null && request.auth.uid != 'PREVIEW';
}
}
receivedRequests (and sentRequests) only require that a user has a non-null auth to edit, ie, any user should be able to edit. However, I get a permissions error when running my transaction. Why is that? Perhaps I am misunderstanding Firestore rules? Perhaps the transaction is trying to do a read? Any thoughts?
UPDATE:
I tried using a batch:
Future sendRequest(uidSend, uidRec, pid, title) async {
//update own uM with post
//update other uM with user
final crSend = ChatRequest(uid: uidRec, pid: pid, title: title);
var _lsSend = List();
_lsSend.add(crSend.toJson());
final crRec = ChatRequest(uid: uidSend, pid: pid, title: title);
var _lsRec = List();
_lsRec.add(crRec.toJson());
final uMSendDocref = userMetadataCollection.document(uidSend);
final uMRecDocref = userMetadataCollection.document(uidRec);
var batch = Firestore.instance.batch();
batch.updateData(uMSendDocref, <String, dynamic>{
"sentRequests": FieldValue.arrayUnion(
_lsSend,
),
});
batch.updateData(uMRecDocref, <String, dynamic>{
"receivedRequests": FieldValue.arrayUnion(
_lsRec,
),
});
return await batch.commit();
}
Still does not work. Something is either incredibly unintuitive with Firestore, or there is a serious bug.
Another thing to note: some of the userMetadata might not currently have the fields that are being updated.
I only want user1 to be able to update this single field of user2's.
A general way of allowing updation of only certain fields can be with checking diff of request.resource.data with resource.data.
Example documents that need to be updated have 3 field: name, city, standingBalance. We want anyone to change standingBalance but only the corresponding user to change his name and city.
The below example is considering that user1 should have complete write access to user1's sendRequest & only single field update access to user2's receivedRequests, there won't be much change in other scenarios.
match /userMetadata/{uid} {
allow read: if uid == request.auth.uid || uid == 'PREVIEW';
allow write: if uid == request.auth.uid && uid != 'PREVIEW';
match /receivedRequests/{req} {
allow read: if uid == request.auth.uid;
allow write: if uid == request.auth.uid;
allow update: if uid == request.auth.uid
|| (request.resource.data.keys().hasOnly(['name','city','standingBalance'])
&& request.resource.data.name == resource.data.name
&& request.resource.data.city == resource.data.city
&& request.resource.data.standingBalance != resource.data.standingBalance
&& request.auth.uid != null
)
}
match /sentRequests/{req} {
allow read: if uid == request.auth.uid;
allow write: if request.auth != request.auth.uid || request.auth.uid != 'PREVIEW';
}
Checking with hasOnly ensures that no other key is added to the document. Also, I am unaware of what 'PREVIEW' is so do verify the rules yourself.
Having a fieldsChanged variable in firestore rules would be a good feature request ;)

Firestore security rule match path to a field in dictionary

Let's say I have this Firestore below user object with fields name, address, and cars (note only user is a collection).
user {
name: "John Smith"
address: '123 Firebase Road, Firestore CA, 10000"
cars: {
asfdfsd811r9UAdfasdf1: {
name: "Ford Explorer"
carSold: false,
salesComment: "This is the best SUV in the world"
},
12342342ADSfas! :{
name:" Testla Modal X"
carPrice:false,
salesComment: "This is the best electric car in the world"
}
}
}
I want to set a security rule to enforce the client libraries can only edit salesComment, but nothing else in this user collection object, how do I do it? I set up a matching path like below, but it doesn't work :( . Can you set up a match path to field dictionary, like in this case, cars? Does match and variableId pattern only apply to collections only.
service cloud.firestore {
match /databases/{database}/documents {
match /user/{userId} {
match /cars/{carId}/salesComment {
allow write: if request.auth.uid == userId;
}
allow read: if request.auth.uid == userId;
}
Workaround approach: Custom functions
You can't use match with internal fields, instead you must make use of the rules.List, rules.Map and rules.Set objects.
It's important to note that rules are static and do not have the ability to iterate over lists (such as using forEach, map, etc). This can be overcome by using someList.size() <= position to check if the list is long enough before performing the element comparison. Unfortunately, this must be hard-coded as you'll see below.
One goal of these rules is that they should be able to be combined with other rules on the same document. i.e. the "cars" map should be restricted but you should still be able to update the "name" and "address" fields.
Throughout this section, the variables are going to be pretty verbose for ease-of-understanding (such as including type information). Rename them to suit your style.
Disclaimer: While this first set of rules works, it's janky and overly specific - not recommended for production use.
service cloud.firestore {
match /databases/{database}/documents {
// assert no changes or that only "salesComment" was changed
function isCarEditAllowed(afterCarMap, beforeCarMap) {
return afterCarMap.diff(beforeCarMap).affectedKeys().size() == 0
|| afterCarMap.diff(beforeCarMap).affectedKeys().hasOnly(["salesComment"]);
}
// assert that if this car exists that it has allowed changes
function isCarAtPosValid(afterCarsList, beforeCarsList, position) {
return afterCarsList.size() <= position // returns true when car doesn't exist
|| isCarEditAllowed(afterCarsList[position], beforeCarsList[position])
}
function areCarEditsAllowed(afterDataMap, beforeDataMap) {
return afterDataMap.get("cars", false) != false // cars field exists after
&& beforeDataMap.get("cars", false) != false // cars field exists before
&& afterDataMap.cars.size() == beforeDataMap.cars.size() // cars field is same length
&& isCarAtPosValid(afterDataMap.cars, beforeDataMap.cars, 0)
&& isCarAtPosValid(afterDataMap.cars, beforeDataMap.cars, 1)
&& isCarAtPosValid(afterDataMap.cars, beforeDataMap.cars, 2)
&& isCarAtPosValid(afterDataMap.cars, beforeDataMap.cars, 3)
&& isCarAtPosValid(afterDataMap.cars, beforeDataMap.cars, 4)
}
match /carUsers/{userId} {
allow read: if request.auth.uid == userId;
allow write: if request.auth.uid == userId
&& areCarEditsAllowed(request.resource.data, resource.data)
}
}
}
Now that the above rules work, they can be improved by abstracting the steps into a set of reusable custom functions.
service cloud.firestore {
match /databases/{database}/documents {
/* Custom Functions: Restrict map changes */
function mapHasAllowedChanges(afterMap, beforeMap, setOfWhitelistedKeys) {
return afterMap.diff(beforeMap).affectedKeys().size() == 0 // no changes
|| setOfWhitelistedKeys.hasAll(afterMap.diff(beforeMap).affectedKeys()) // only named keys may be changed
}
function mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, position) {
return afterList.size() <= position // returns true when element doesn't exist
|| mapHasAllowedChanges(afterList[position], beforeList[position], setOfWhitelistedKeys)
}
function listOfMapsHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys) {
return mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 0)
&& mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 1)
&& mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 2)
&& mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 3)
&& mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 4)
}
function largeListOfMapsHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys) {
return mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 0)
&& mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 1)
&& mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 2)
&& mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 3)
&& mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 4)
&& mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 5)
&& mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 6)
&& mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 7)
&& mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 8)
&& mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 9)
}
function namedListWithSameSizeExists(listPath) {
return request.resource.data.get(listPath, false) != false
&& resource.data.get(listPath, false) != false
&& request.resource.data.get(listPath, {}).size() == resource.data.get(listPath, {}).size()
}
function namedListOfMapsWithSameSizeExistsWithAllowedChanges(listPath, setOfWhitelistedKeys) {
return namedListWithSameSizeExists(listPath)
&& listOfMapsHasAllowedChanges(request.resource.data.get(listPath, {}), resource.data.get(listPath, {}), setOfWhitelistedKeys)
}
/* Rules */
match /carUsers/{userId} {
allow read: if request.auth.uid == userId;
allow write: if request.auth.uid == userId
&& namedListOfMapsWithSameSizeExistsWithAllowedChanges("cars", ["salesComment"].toSet())
}
}
}
Note: The above rules do not assert that the whitelisted keys were not deleted. To ensure that the listed keys are present after the change, you will need to replace the mapHasAllowedChanges function with:
function mapHasAllowedChanges(afterMap, beforeMap, setOfWhitelistedKeys) {
return afterMap.diff(beforeMap).affectedKeys().size() == 0 // no changes
|| (setOfWhitelistedKeys.hasAll(afterMap.diff(beforeMap).affectedKeys()) // only named keys may be changed
&& afterMap.keys().toSet().hasAll(setOfWhitelistedKeys)) // all named keys must exist
}
Recommended approach: Move to cars to subcollection
The above rules are quite complex, and can be simplified if you move the cars to their own collection and make use of rules.Map#diff.
The below code will only allow a write to take place if the user owns that car document and is also modifying only the salesComment key (modify = add/change/remove).
service cloud.firestore {
match /databases/{database}/documents {
match /user/{userId} {
allow read, write: if request.auth.uid == userId;
match /cars/{carId} {
allow read: if request.auth.uid == userId; // Firestore rules don't cascade to subcollections, so this is also needed
allow write: if request.auth.uid == userId
&& request.resource.data.diff(resource.data).affectedKeys().hasOnly(["salesComment"]);
}
}
}
}
If you require that salesComment must be present after a write (add/change allowed - but not remove), you can also ensure that is still present using k in x.
service cloud.firestore {
match /databases/{database}/documents {
match /user/{userId} {
allow read, write: if request.auth.uid == userId;
match /cars/{carId} {
allow read: if request.auth.uid == userId; // Firestore rules don't cascade to subcollections, so this is also needed
allow write: if request.auth.uid == userId
&& "salesComment" in request.resource.data
&& request.resource.data.diff(resource.data).affectedKeys().hasOnly(["salesComment"]);
}
}
}
}

FireStore Rules check if record is being inserted or updated

I set the following firestore rule on my firebase project
service cloud.firestore {
match /databases/{database}/documents {
match /news/{news} {
allow read: if true;
allow write: if request.auth != null
// && (
// (resource == null && request.resource.data.author == request.auth.uid) ||
// request.auth.uid == resource.data.author == request.resource.data.author ||
// get(/users/$(request.auth.uid)).role in ["editor", "admin"]
// )
&& request.resource.data.keys().hasAll(['title', 'body', 'images', 'tags', 'source', 'web_link', 'created_at', 'updated_at', 'author'])
&& request.resource.data.title is string && request.resource.data.title.size() >= 10 && request.resource.data.title.size() <= 100
&& request.resource.data.body is string && request.resource.data.body.size() >= 300 && request.resource.data.body.size() <= 500
&& request.resource.data.images is list && request.resource.data.images.size() >= 0
&& request.resource.data.tags is list && request.resource.data.tags.size() >= 0
&& request.resource.data.source is string && request.resource.data.source.size() > 0 && request.resource.data.title.size() <= 100
&& request.resource.data.web_link is string && request.resource.data.web_link.size() > 0
// && request.resource.data.created_at is timestamp
// && request.resource.data.updated_at is timestamp
;
}
match /users/{userId} {
allow read, write: if request.auth.uid == userId || get(/users/$(request.auth.uid)).role == "admin"
}
match /{document=**} {
allow read, write: if false;
}
}
}
The lines commented above don't work
I need to check if the resource is being inserted or updated, the resource == null part doesn't work
created_at and updated_at contain timestamp, and is timestamp doesn't work
In case anyone is wondering, resource.data.author contains uid of a user
From the documentation, writes can be subdivided into:
create
update
delete
So, in this case, you would want something like:
service cloud.firestore {
match /databases/{database}/documents {
match /news/{news} {
//read rules here...
allow create: if true;
allow update: if true;
allow delete: if true;
}
}
//other rules here...
}
There is also good documentation for validating timestamps. I would strongly advise making use of firebase.firestore.FieldValue.serverTimestamp() where possible.
I was looking for the same details in the documentation, the following works for me using timestamps and fails if I try send a string (where "lastUpdated" should be swapped with whatever your date property is)
request.resource.data.lastUpdated.date() is timestamp

Resources