Exists() not working with second variable Cloud Firestore rules - firebase

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

Related

Firebase cross-service Security Rules not working in application

I'm trying to use the new Firebase cross-service Security Rules (https://firebase.blog/posts/2022/09/announcing-cross-service-security-rules) but I having some problems with Storage Rules accessing to Firestore data.
The problem seems to be with userIsCreator() function
match /certification/{certificationId}/{fileId} {
function userIsCreator() {
let certification = firestore.get(/databases/(default)/documents/certifications/$(certificationId));
return firestore.get(certification.data.creatorRef).id == request.auth.uid;
}
allow read, write: if userIsCreator()
}
The content of the Firestore Document is:
{
"data": {
othersValues,
"creatorRef": "/databases/%28default%29/documents/users/CuutSAtFkDX2F9T8hlT4pjMUByS2"
}
"id": "3EhQakDrsKxlacUjdibs"
"__name__":
"/databases/%28default%29/documents/certifications/3EhQakDrsKxlacUjdibs"
}
The creatorRef variable is a reference to a Firestore Document to user. Inside Users collection, the doc id is the UID of an user, so I'm obtaining the creatorRef of an item and then checking if the id of that user collection referenced is the same UID that user logged in.
The same function is working for Firestore Rules to avoid updating certification document if not the creator, without any problem.
It seems to be a problem calling to firestore.get to creatorRef after obtaining it but it not make sense!
Tested:
If I use Firestore Storage Rules validator, it is not failing and it says I have access to that resource from the UID typed in the tester (for other UID is failing as expected). But in my app, even logged in with creator user is getting permission error.
If changing the function to only one call directly to the Users collection id (return firestore.get(/databases/(default)/documents/users/CuutSAtFkDX2F9T8hlT4pjMUByS2).id == request.auth.uid;), it is working in the tester and my app. But it isn't a solution because I need to get first the Users collection ref for the creator!
For original function in the tester It's getting the variables as expected and returning true if simulate the creator UID! But for any reason, in the real app access it is getting unauthorized if making both calls!
Firebaser here!
It looks like you've found a bug in our implementation of cross-service rules. With that said, your example will create two reads against Firestore but it's possible to simplify this to avoid the second read.
Removing the second read
From your post:
return firestore.get(certification.data.creatorRef).id == request.auth.uid;
This line is a bit redundant; the id field is already contained in the certification.data.creatorRef path. Assuming you are indeed using Firestore document references, the format of creatorRef will be /projects/<your-project-id>/databases/(default)/documents/users/<some-user-id>. You can therefore update your function to the following:
function userIsCreator() {
let certification = firestore.get(/databases/(default)/documents/certifications/$(certification));
let creatorRef = certification.data.creatorRef;
// Make sure to replace <your-project-id> with your project's actual ID
return creatorRef ==
/projects/<your-project-id>/databases/(default)/documents/users/$(request.auth.uid);
}
I've tested this out in the emulator and in production and it works as expected. The benefit of doing it this way is you only have to read from Firestore once, plus it works around the bug you've discovered.

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.

Firestore Update fields in nested objects with dynamic key

I need to update a field in a nested object with a dynamic key.
the path could look like this: level1.level2.DYNAMIC_KEY : updatedValue
The update-method deletes everything else on level1 instead of only updating the field in the nested object. The update() acts more like a set(). What am I doing wrong?
I tried the following already:
I read the documentation https://firebase.google.com/docs/firestore/manage-data/add-data#update-data
but that way it is a) static and b) still deletes the other fields.
Update fields in nested objects
If your document contains nested objects, you can use "dot notation" to reference nested fields within the document when you call update()
This would be static and result in
update({
'level1.level2.STATIC_KEY' : 'updatedValue'
});
Then I found this answer https://stackoverflow.com/a/47296152/5552695
which helped me to make the updatepath dynamic.
The desired solution after this could look like
field[`level1.level2.${DYNAMIC_KEY}`] = updateValue;
update(field);
But still: it'll delete the other fields in this path.
UPDATE:
The Structure of my Doc is as follows:
So inside this structure i want to update only complexArray > 0 > innerObject > age
Writing the above path into the update() method will delete everything else on the complexArray-level.
A simple update on first-level-fields works fine and lets the other first-level-fields untouched.
Is it possible, that firestore functions like update() can only act on the lowest field-level on an document. And as soon as i put complex objects into an document its not possible to select such inner fields?
I know there would be the solution to extract those "complex" objects into separate collections + documents and put these into my current lowest document level. I think this would be a more accurate way to stick to the FireStore principles. But on Application side it is easier to work with complex object than to always dig deeper in firestore collection + document structure.
So my current solution is to send the whole complex object into the update() method even though I just changed only one field on application side.
Have you tried using the { merge: true } option in your request?
db
.collection("myCollection")
.doc("myDoc")
.set(
{
level1: { level2: { myField: "myValue" } }
},
{ merge: true }
)

delete item out of firebase database

I have a restaurant bookmarks list in my firebase, but I don't know how to delete a specific restaurant in my database.
So I have the function unfavorite(favorite) where I pass the favorite restaurant as a parameter. Till here, than I want to pass this parameters id to the query to remove from the database like this:
this.afDb.list(`bookmarks/${user.uid}/restaurant/${favorite.restaurant.id})`).remove();
here is a screenshot of my database list:
How can I remove that specific restaurant out of the bookmarks list?
You will first need to add an ".indexOn": ["id"] rule to your database something like this:
"bookmarks": {
"$user_id": {
// normal reads and write rules here
},
".indexOn": ["id"]
This step is necessary for firebase database because otherwise you wont be able to use the orderByChild() and equalTo() methods.
Then, where you have your delete function, you want to instead use:
exampleRef = yourDb.ref("bookmarks/${user.uid}"); //this is just to simplify your reference a bit
exampleRef.orderByChild('id').equalTo(theDeleteIDhere).once('value').then(snapshot => {
snapshot.forEach((restaurant) => {
restaurant.ref.remove();
});
}); //this is a Promise that you can modify to return "true" if successful for example
The example I provided is just the way I have done it before (i.e. I prefer to use promises; hence the then() becuase this makes it easier to return that promise in an angular service which allows me to check whether the request was successful). You can use any variation of this so long as you have the "indexOn" rule and you use any sort of "sorting" method firebase provides here
Method 2
When I wrote this I totally glanced over the ability to map your restaurants like such:
Lets say your project is already listing those restaurants. You can therefore save each restaurant's auto generated id to a variable or map:
restaurants; // this is a map like this <your-identifier:autoID>
you can then easily just call:
exampleRef.child(restaurants[yourIdentifier]).remove();

Size of firestore rules path

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';
}

Resources