Size of firestore rules path - firebase

I'm trying to use the size of the path in the firestore rules, but can't get anything to work, and can't find any reference in the firestore docs on how to do this.
I want to use the last collection name as a parameter in the rule, so tried this:
match test/{document=**}
allow read, write: if document[document.size() - 2] == 'subpath';
But .size() does not seem to work, neither does .length

This can be done but you first have to coerce the Path to a String.
To get the Path of the current resource, you can use the __name__ property.
https://firebase.google.com/docs/reference/rules/rules.firestore.Resource#name
For reference, resource is a general property that is available on every request that represents the Firestore Document being read or written.
https://firebase.google.com/docs/reference/rules/rules.firestore.Resource
resource['__name__']
The value returned by __name__ is a Path, which is lacking in useful methods, so before you can use size you will need to coerce the Path to a String.
https://firebase.google.com/docs/reference/rules/rules.String.html
string(resource['__name__'])
Once converted to a string, you can then split the string on the / operator and convert it into a List of String path parts.
https://firebase.google.com/docs/reference/rules/rules.String.html#split
string(resource['__name__']).split('/')
Now you can retrieve the size of the List using the List's size method.
https://firebase.google.com/docs/reference/rules/rules.List#size
string(resource['__name__']).split('/').size()
One of the challenging things about Firestore rules is that there's no support for variables so you often will repeat code when you need to use a result more than once. For instance, in this case, we need to use the result of the split twice but cannot store it into a variable.
string(resource['__name__']).split('/')[string(resource['__name__']).split('/').size() - 2]
You can DRY this up a bit by making use of functions and using the parameter as your variable.
function getSecondToLastPathPart(pathParts) {
return pathParts[pathParts.size() - 2];
}
getSecondToLastPathPart(string(resource['__name__']).split('/'))
Tying it all together for your solution, it would look like this...
function getSecondToLastPathPart(pathParts) {
return pathParts[pathParts.size() - 2];
}
match test/{document=**} {
allow read, write: if getSecondToLastPathPart(string(resource['__name__']).split('/')) == 'subpath';
}
Hope this helps!

You can learn rules here
// Allow reads of documents that begin with 'abcdef'
match /{document} {
allow read: if document[0:6] == 'abcdef';
}

Related

About tens of Firestore collections have the same access rule, must I repeat writing same `match` sentence for each collection

the match I'm using is below
match /{document=**} {
allow read: if true;
allow write: if false;
}
but firebase always sends alarm, so I want to modify rules.
if a match sentence for a collect, the repeat will be too much.
so I wonder if there is any solution to avoid repeat?
One way to add same rule to multiple collections would be to use a wildcard on collection name as shown below:
match /{collectionName}/{docId} {
allow read: if collectionName in ["colName1", "colName2"];
}
However, this rule is applied to all the collections so if you have any exceptions (e.g. colName3) make sure you define rules for those collections separately as this rule will reject reads for colName3.

Firebase, Security Rules, how to check a item of array String size

the way of getting and verifying array list size in Firestore is below
request.resource.data.arraylist.size() < 10
but I want to know the "request.resource.data.arraylist[last_index].length" not the "list size"
how do I figure out this?
The Bytes, List, Map, Set, and String variable types in the rules namespace all share the size() method which is used in place of length.
So, using that, you would assert the last element has a size of less than 10 using:
request.resource.data.arraylist[request.resource.data.arraylist.size()-1].size() < 10
Because that looks messy (especially amongst other conditions), you could rewrite it as a custom function like so:
function doesLastElementOfListHaveSizeLessThan(num, list) {
return list[list.size()-1].size() < num;
}
doesLastElementOfListHaveSizeLessThan(10, request.resource.data.arraylist);
Notes:
You can name the function whatever you want, I just used a sentence for clarity and it makes rereading your rules later easier.
While this rule asserts the length of the last index, other indexes could have been modified to be longer than your threshold in the same write.
For performance reasons, security rules do not support iteration, you must hard code in each check for an index manually. If you need to check a range of indexes, you should implement a custom function that checks a small range of indexes that takes a starting offset. If you implement such a function, make sure that it short-circuits to true/false as soon as possible and doesn't throw errors such as the index being out of bounds.

Compare Arrays / Sets in Firestore Security Rules

In my usecase a document is worked on by multiple users of a specific group.
The document holds (besides other entries) the two relevant fields for:
ownerUID (String Value - represents the UID of the document owner)
members (Array of Strings - represents the UIDs of all members allowed to work on this document)
I want to ensure (using security rules), that (besides the owner of a document) no member is able to remove another member from the members array - except themselves.
To evaluate this I tried using below function in my Firestore Security Rules
function onlyOwnMemberUIDRemovableOrOwner(){
return request.auth.uid == resource.data.ownerUID ||
resource.data.members.toSet().difference(request.resource.data.members.toSet()) == request.auth.uid.toSet();
}
First statement is pretty obvious and is working fine for other rules in my setup to allow owners to modify documents without restrictions:
request.auth.uid == resource.data.ownerUID
The second statement is causing problems for me. The idea was, to evaluate missing values in the members field by using the .difference() function for sets and to compare it with the set only containing the own UID. Only if the missing UID is the own UID, the function should return true. Unfortunately even when trying to remove the own UID, the statement will still return false.
resource.data.members.toSet().difference(request.resource.data.members.toSet()) == request.auth.uid.toSet()
Are you able to tell me what I was doing wrong here?
Do you know a better approach to solve the problem?
Thank you very much for your support.
Two issues with your code:
toSet() works on list and not on string
You have to make sure ownerUID is not modified
Here is the rule corrected:
function onlyOwnMemberUIDRemovableOrOwner(){
return request.resource.data.ownerUID == resource.data.ownerUID
&& (request.auth.uid == resource.data.ownerUID ||
resource.data.members.toSet().difference(request.resource.data.members.toSet()) == [request.auth.uid].toSet());
}

Firestore Access Rules that Rely on a Document Reference

Firestore has a DocumentReference type, which is a "pointer" to another firestore document. Using the firebase JavaScript client, you can access properties (e.g. document "id"), directly on the reference.
For example, if there is a document with a docRef property that is a firestore DocumentReference:
const retrievedDoc = await getFirestoreDocument();
console.log(retrievedDoc.docRef.id); // "jRmSeMYDMKiOPGsmkdaZ"
I am trying to accomplish the same thing within firestore rules. There is a custom function named isOwner. It uses the firestore rules get call on a document path, and then attempts to access the docRef.id just as if it were the JavaScript client above.
get(/databases/$(database)/documents/path/to/$(id)).data.docRef.id
The value of the document's id is compared against the current user's. But when I test this using the simulator and in real code, it fails. I feel like this should work, but it doesn't.
What does work is to store and use the id value directly as a string (e.g. get(/path/id).docId) instead of a DocumentReference.
Should I be able to access the id value of a DocumentReference within the firestore rules? Am I doing something wrong?
I want to avoid doing a second document get within the rule as described in this SO answer. That's a second "read" for each trigger of this rule. And I don't think the document id (which is what I need) will be available on the get call anyway.
Based on documentation:
https://firebase.google.com/docs/reference/rules/rules.firestore#.get
https://firebase.google.com/docs/reference/rules/rules.firestore.Resource
get() method is supposed to returns a Resource object which is supposed to contains a .id property (as well as .data).
For example, to restrict write access to an authenticated user which is the author of a book document (authors documents are identified with the user uid), you would do:
service cloud.firestore {
match /databases/{database}/documents {
match /books/{document=**} {
allow write: if get(resource.data.authorReference).id == request.auth.uid;
}
}
}
Yet I'm always having the error property id is undefined on object on trying.
.data is accessible so I suppose there is an issue in the api.
Update
Actually, a reference is a Path object in Firestore rules as documented here. So you access the id by the index of the part of the path you need.
In this example I use the incoming document's data which has a reference object to lookup a property on another document from a get()
match /databases{database}/documents {
match /contacts/{contact} {
allow create: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.relatedRules[request.resource.data.relation.path[4]].canBeRelated
// the [4] assumes the path to be `databases/$(database)/documents/contacts/contactId`
// your exact index would vary for your data structure
}
}
First Answer
This only works in the Firestore dashboard rules simulator, it is not a working example for either the local emulation or production Firestore.
This is a year old but I encountered this same puzzling issue, but not on the data from a get(), just on the data of the request.resource.data. I'm not sure what ought to be available (not even __name__ is available) in the rules but if you're accessing a resource reference on the data and you have a predictable id size (say, 20 characters) you could simply get the range of the path on the resource to check against
match /databases{database}/documents {
match /contacts/{contact} {
allow create: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.relatedRules[request.resource.data.relation.path[9:29]].canBeRelated
// the [9:29] assumes the path to be `/contacts/20characterLongIdStr`
// your exact range would vary for your data structure
}
}
Feels like a resource reference object should have at least the id since the path is there. It appears Firestore won't support this for whatever reason.

Exists() not working with second variable Cloud Firestore rules

In my project, I want to control access to a document by checking if the user's uid is part of a subcollection (that holds all of the members of that document) of that document.
When I want to check this with the exists() method, it does not grant permission when it's supposed to.
match /events/{season}/events/{code} {
function isVV (season, code) {
return exists(/databases/$(database)/documents/events/$(season)/events/$(code)/vv/$(request.auth.uid));
}
allow read: if isVV(season, code);
When I replace the $(code) with the value I'm currently testing, the rule passes and everything works as expected. When I'm using a variable it does not.
Any Cloud Firestore Rules experts that can help me out?
Maybe there is a better way to do this?
For some reason exists does not like to evaluate variables other than database in the Firestore rules. Looking at the documentation I was able to find that the exists function actually takes in a Path, described here: https://firebase.google.com/docs/reference/rules/rules.Path
Using this information I was able to achieve something similar to what you want above by first constructing the path as a string using concatenation and then passing this to the exists function.
For your example above this would look like:
match /events/{season}/events/{code} {
function isVV (database, season, code) {
return exists(path("/databases/" + database + "/documents/events/" + season + "/events/" + code + "/vv/" + request.auth.uid));
}
allow read: if isVV(database, season, code);
}
Note: you have to pass the database as a function param as I found that when using string concatenation in this fashion, it is not auto populated like it is when using something like exists(/databases/$(database))

Resources