Getting Document Multiple Times in Firestore Rules - firebase

When writing rules for a Firebase Database that require fetching a document from a separate collection, is there a difference in referring to that document more than once?
For example, this will cause a single query of the otherStuff collection:
// Rules single get
match /someData/{dataId} {
allow read: if get(/databases/$(database)/documents/otherStuff/$(dataId)).data.allowRead == true
}
But will the following code actively get the same document twice? Or is it optimized, and only fetched once?
// Rules twice get
match /someData/{dataId} {
allow read: if get(/databases/$(database)/documents/otherStuff/$(dataId)).data.allowRead == true
&& get(/databases/$(database)/documents/otherStuff/$(dataId)).data.reallyAllowRead == true
}

Check out this section in the docs, https://firebase.google.com/docs/firestore/security/rules-structure#security_rule_limits
In security rule evaluation for a single rule, "multiple requests for the same document do not count as separate requests."

Related

Firestore security rule get vs. list

Suppose I'm trying to make a webpage readable only when the user has the exact link to it, could I achieve this using a combination of the document ID and security rules as follows?
eg. I have documents stored at
/posts/{postID}
I have security rules set up:
match /posts/{postID} {
  allow get, write: if true;
  allow list: if false;
}
Would simply disabling the "list" operation make it such that you can only access a document if you already know its exact ID? It looks like the case to me but I'm not sure if I'm missing any edge cases.
Would simply disabling the "list" operation make it such that you can
only access a document if you already know its exact ID?
Yes, this is right.
Any query to the posts collection will return a "Missing or insufficient permissions" error, even a query that queries with the exact ID, like:
firebase
.firestore()
.collection('posts')
.where('__name__', '==', 'exactID')
.get()

Firestore security rule that only allows empty documents

I'm basically trying to use a firestore collection as a an email list. Anyone can create a document that has their email as the id and nothing more. The tricky part is the "and nothing more" bit. When no data is provided in the request, request.resource is undefined which you can't check for in security rules to my knowledge. Is this possible? Or is it necessary to have something like one mandatory field for this use case?
Having empty documents regularly leads to issues down the line. Why not require a single marker field, and validate that in rules?
request.resource.data.keys.hasOnly("marker")
For the benefit of others looking to make an email list in firestore, this is the full rule I ended up using:
match /email-list/{email} {
allow get: if true;
allow list: if false;
allow create: if request.resource.data.keys().hasOnly(["marker"])
&& request.resource.data.marker == true
}

Firestore security rules - read count in a batch

If in a batch I update documents A and B and the rule for A does a getAfter(B) and the rule for B does a getAfter(A), am I charged with 2 reads for these or not? As they are part of the batch anyway.
Example rules:
match /collA/{docAid} {
allow update: if getAfter(/databases/$(database)/documents/collA/${docAid}/collB/{request.resource.data.lastdocBidupdated}).data.timestamp == request.time
&& ...
}
match /collA/{docAid}/collB/{docBid} {
allow update: if getAfter(/databases/$(database)/documents/collA/${docAid}).data.timestamp == request.time
&& getAfter(/databases/$(database)/documents/collA/${docAid}).data.lastdocBidupdated == docBid
&& ...
}
So are these 2 reads, 1 per rule, or no reads at all?
firebaser here
I had to check with our team for this. The first feedback is that it doesn't count against the maximum number of calls you can make in a single security rule evaluation run.
So the thinking is that it likely also won't count against documents read, since it doesn't actually read the document. That said: I'm asking around a bit more to see if I can get this confirmed, so hold on tight.
Are you using two different documents?
If it is the case, then two reads will be performed.

Firebase database rules – `data.exists()` always seems to be true, possible bug?

I am trying to secure my firebase database to allow the creation of new records, but not allow the deletion of existing records. Ultimately, I plan to utilise Firebase authentication in my app as well, and allow users to update existing records if they are the author, but I am trying to get the simple case working first.
However! No matter what I try in the database rules simulator, despite what the documentation seems to suggest, the value of data.exists() seems to always be true. From what I what I can understand from the documentation, the variable data represents a record in the database as it did before an operation took-place. That is to say, for creates, data would not exist, and for updates/deletes, data would refer to a real record that exists in the database. This does not seem to be the case, to the point where I am actually suspecting a bug in Firebase, as when setting the following rules on my database, all write operations are disallowed:
{
"rules": {
".read": true,
".write": "!data.exists()"
}
}
No matter what values I put into the simulator, be it Location or Data. I have even written a small EmberJS app to verify if the Simulator is telling the truth and it too, is denied permission for all write operations.
I really have no idea where to go from here as I am pretty much out of things to try. I tried deleting all records from my database, which lets the simulator think it can perform write operations, but my test app still gets PERMISSION_DENIED, so I don't know what's causing inconsistencies there.
Is my understanding of the predefined data variable correct? If so, why can't I write the rules I want? I have seen snippets literally trying to achieve my "create only, no-delete" rule that seem to line up with my understanding.
Last note: I am trying this in a totally new Firebase project with JUST the rules above, and only ~a few records of junk data laying around my database.
Because you have placed the !data.exists() at the root location of your database, data refers to the entire database. You will only be able to write to the database when it is completely empty.
You indicate that you run your tests with only a few records of junk data laying around my database. Those records will cause data.exists() to be true.
You can achieve your goal by placing the !data.exists() rule in your tree at the specific location where you want to require that no data already exists. This is typically done at a location with a wildcard key, as in the example you linked:
{
"rules": {
// default rules are false if not specified
"posts": {
".read": true, // everyone can read all posts
"$postId": {
// a new post can be created if it does not exist
// existing posts can only be edited by their original "author"
".write": "!data.exists() && newData.exists() || data.child('author').val() == auth.uid",
".validate": "newData.hasChildren(['title', 'author', 'timestamp'])",
}
}
}
}

How to use firebase rule to check is user group array and record group array intersect

I have a list of records in firebase which will have a group property with zero or more groups on it. I also have the firebase auth object which will also have zero or more groups on it as well. I would like to set up a .read firebase rule for my records that will check if the two have at lease one group that exists in both lists.
Put another way I have a user that has an array of groups that have been assigned to it. I have some records that also has some list of groups on them that specify what groups the user must have to access them. If the logged in user tries to access the record, I want to make sure that the user has at least one group that the record requires.
On the client I would do something like _.intersect(userGroups, recordGroups).length > 0
I'm not sure how I would do this in a firebase rule expression. It would be cool if it worked something like this.
Record:
{
someData: "test"
groups: ['foo', 'bar']
}
Firebase Auth Object:
{
userName: "Bob",
groups: ['foo', 'bar']
}
Rule Data:
{
"rules": {
"records": {
"$recordId": {
".read": "data.child('groups').intersectsWith(auth.groups)"
}
}
}
}
Thanks.
Update:
I think that if hasChildren() used || instead of && I could put the group names in they key position and check for their existence this way. Something like "data.child('groups').hasChildren(auth.groups, 'or')"
Where Record:
{
someData: "test"
groups: {
'foo': '',
'bar': ''
}
}
Update2:
Based off Kato's comment & link I realize that even if hasChildren could do OR it still wouldn't work quite right. Requests for individual records would work but requests for all records would error if the current user didn't have access to every record.
It is still not clear how you would structure data to make this work. If a record could belong to many groups how would that work? This is a very common scenario(basically how linux group permissions work) so I can't be the only one trying to do this. Anyone have any ideas/examples of how to accomplish this in firebase?
At the current moment, I believe it's impossible. There's a limited number of variables, methods, and operators allowed, listed here:
Firebase Security Rules API
Since function definitions are not allowed in the rules, you can't do anything fancy like call array.some(callback) on an array to do the matching yourself.
You have three options that I know of:
1) Copy data so you don't need to do the check. This is what I did in my project: I wanted some user data (names) available to users that shared a network in their network lists. Originally I wanted to check both member's network lists to see if there was at least one match. Eventually I realized it would be easier to just save each user's name as part of the network data so there wouldn't have to be a user look up requiring this odd permissions. I don't know enough about your data to suggest what you need to copy.
2) Use strings instead of arrays. You can turn one string into a regex (or just save it in regex format) and use it to search the other string for a match.Firebase DB Regex Docs
3) If you have enough weird cases like this, actually run a server that validates the request in a custom fashion. In the DB, just allow permissions to your server. You could use Firebase Cloud Functions or roll your own server that uses the Firebase Admin SDK
Nowadays, there's another possibility: to use Firestore to deliver your content, possibly in sync with the Realtime Database.
In Firestore, you can create rules like this:
function hasAccessTo(permissionList) {
return get(/databases/$(database)/documents/permissions/$(request.auth.uid))
.data.userPermissions.keys().hasAny(permissionList)
}
match /content/{itemId} {
allow read: if hasAccessTo(resource.data.permissions.keys());
}
The following data would allow a read of $CONTENTID by $UID, because the user permissions set intersects with the possible permissions required to access the content (with access123). My scenario is that a piece of content can be unlocked by multiple In-App Purchases.
{
permissions: {
$UID: { userPermissions: { access123:true, access456:true } },
...
},
content: {
$CONTENTID: { ..., permissions: { access123, access789 } },
...
}
}
For a progressive migration, you can keep data in sync between the Realtime Database and Firestore by using a one-way Cloud Function like this for example:
exports.fsyncContent = functions.database
.ref("/content/{itemId}")
.onWrite((snapshot, context) => {
const item = snapshot.after.val();
return admin
.firestore()
.collection("content")
.doc(context.params.itemId)
.set(item);
});

Resources